Log In  

I've made a spike for cartriges to send http requests and receive their response.

The way it works is the cartrige communicates with the browser through gpio. In here I'm using 2 assumptions (I guess dangerous ones) that make it quite simple to send messages:

  1. We get synchronous interrupts from p8 -> JS when p8 writes to a GPIO.
  2. We read an analog value 0-255 in perfect accuracy.

So... yep, this code atm only works in a browser. And it might be posible that it breaks later on (specially for assumption 1, as it depends on the runtime implementation)

It's separated in two layers: The first one just abstracts over sending and receiving any message

html_messaging.lua

html_messaging={
  send_message=function(self, message)
    -- sends a `message` string to the host HTML
  end,

  add_listener=function(self, listener)
    -- adds the `listener` function to be called with receiving messages
  end,
  remove_listener=function(self, listener)
    -- stops sending message updates to the listener
  end,

  update=function(self)
    -- this function must be called on every update, so that html_messaging can check for new messages
  end
}

html_messaging={
  gpio=0x5f80,
  pins={
    send_req=1, -- output
    read_req=3, -- input
    clk_out=5, --input
    data_out=7, -- output
    data_in=8 -- input
  },
  send_message=function(self, message)
    if #message == 0 then return end

    poke(self.gpio + self.pins.send_req, 0xFF)

    for i=1,#message do
      poke(self.gpio + self.pins.data_out, ord(message, i))
    end

    poke(self.gpio + self.pins.send_req, 0x00)
  end,

  _listeners={},
  add_listener=function(self, listener)
    add(self._listeners, listener)
  end,
  remove_listener=function(self, listener)
    del(self._listeners, listener)
  end,
  update=function(self)
    local message = __messaging_receive_message()
    if not message then return end
    for listener in all(self._listeners) do
      listener(message)
    end
  end
}

function __messaging_receive_message()
  local gpio = html_messaging.gpio
  local pins = html_messaging.pins

  local is_sending = peek(gpio + pins.read_req) == 0xff

  if not is_sending then return false end

  local input_buffer = ""
  while peek(gpio + pins.read_req) == 0xff do
    input_buffer = input_buffer..chr(peek(gpio + pins.data_in))
    poke(gpio + pins.clk_out, 0xff)
    poke(gpio + pins.clk_out, 0x00)
  end
  return input_buffer
end

gpio_messaging.js
// This file also sets window.pico8_gpio - Make sure not to override it, or it won't get the interrupts!

window.pico8_gpioMessaging={
  sendMessage(message) {
    // sends the string `message` to p8
    // returns a promise that resolves when the message was completely sent
    // (we don't have synchronous interrupts from js -> p8)
  }
}
// pico8_gpioMessaging is an EventEmitter of 'message' events
// which have a 'message' prop with the received message

(function () {
  class GPIO extends EventTarget {
    static READ = 'read';
    static WRITE = 'write';
    pins = new Array(128);

    get interruptPins() {
      return new Proxy(this, {
        get: (object, prop) => {
          const evt = new ReadEvent(prop);
          this.dispatchEvent(evt);

          return object.pins[prop];
        },
        set: (object, prop, value) => {
          object.pins[prop] = value;

          const evt = new WriteEvent(prop, value);
          this.dispatchEvent(evt);
        }
      });
    }
  }
  window.P8GPIO = GPIO;
  class ReadEvent extends Event {
    pin = 0;

    constructor(pin) {
      super(GPIO.READ);
      this.pin = Number.parseInt(pin);
    }
  }
  class WriteEvent extends Event {
    pin = 0;
    payload = '';

    constructor(pin, payload) {
      super(GPIO.WRITE);
      this.pin = Number.parseInt(pin);
      this.payload = payload;
    }
  }

  const gpio = new GPIO();
  window.pico8_gpioBus = gpio;
  window.pico8_gpio = gpio.interruptPins;

  const HIGH = 0xFF;
  const LOW = 0x00;
  const PINS = {
    sendReq: 1, // input
    readReq: 3, // output
    clkIn: 5, // output
    dataIn: 7, // input
    dataOut: 8 // output
  }

  class GPIOMessaging extends EventTarget {
    static MESSAGE = 'message';

    constructor() {
      super();
      gpio.addEventListener(GPIO.WRITE, event => {
        if (event.pin === PINS.sendReq && event.payload === HIGH) {
          this._receiveMessage();
        }
      })
    }

    _messageQueue = new Array();
    sendMessage(message) {
      const { resolve, promise } = destructPromise();
      this._messageQueue.push({
        resolve,
        message
      });
      this._flushMessages();

      return promise;
    }

    _isSending = false;
    _flushMessages() {
      if (this._isSending || this._messageQueue.length === 0) return;

      const { resolve, message } = this._messageQueue.shift();
      if(message.length === 0) {
        return resolve();
      }
      this._isSending = true;

      let idx = 0;
      const self = this;
      function handleWrite({ pin, payload }) {
        if(pin === PINS.clkIn && payload === LOW) {
          idx++;
          if (message.length <= idx) {
            gpio.removeEventListener(GPIO.WRITE, handleWrite);
            gpio.pins[PINS.readReq] = LOW
            resolve();
            self._isSending = false;
            self._flushMessages();
          } else {
            gpio.pins[PINS.dataOut] = message.charCodeAt(idx)
          }
        }
      }
      gpio.addEventListener(GPIO.WRITE, handleWrite);

      gpio.pins[PINS.dataOut] = message.charCodeAt(idx)
      gpio.pins[PINS.readReq] = HIGH
    }

    _receiveMessage() {
      let buffer = "";
      const _self = this;
      function handleWrite({ pin, payload }) {
        switch (pin) {
          case PINS.dataIn:
            buffer += String.fromCharCode(payload);
            break;
          case PINS.sendReq:
            if (payload === LOW) {
              gpio.removeEventListener(GPIO.WRITE, handleWrite);
              const event = new MessageEvent(buffer);
              _self.dispatchEvent(event);
            }
            break;
        }
      }
      gpio.addEventListener(GPIO.WRITE, handleWrite);
      // In here we told pico8 we were ready to receive
      // Now that's not needed anymore, as p8 assumes interrupts are synchronous
      // gpio.pins[2] = HIGH
    }
  }
  window.P8GPIOMessaging = GPIOMessaging;
  class MessageEvent extends Event {
    message = ''
    constructor(message) {
      super(GPIOMessaging.MESSAGE)
      this.message = message;
    }
  }
  window.pico8_gpioMessaging = new GPIOMessaging();

  function destructPromise() {
    let resolve, reject
    const promise = new Promise((res, rej) => {
      resolve = res;
      reject = rej;
    });
    return { resolve, reject, promise };
  }
})();

The other layer needs to have that one imported first - This is the one to send and receive HTTP requests

http_request.lua

http_request={
  get=function(self,url,cb)
    -- sends a GET request to url, sending the message as a string to callback once it's received
  end
}

-- requires html_messaging.lua

http_request={
  _cid=0,
  get=function(self,url,cb)
    local cid = self._cid
    self._cid = self._cid + 1
    local handle_message = function(message)
      local match_header = "HTTP RES "..cid
      local msg_header = sub(message, 1, #match_header)
      if match_header != msg_header then return end
      local response = sub(message, #match_header + 2)
      cb(response)
      html_messaging:remove_listener(handle_message)
    end
    html_messaging:add_listener(handle_message)
    html_messaging:send_message("HTTP REQ "..cid.." GET "..url)
  end
}

http_request.js

// Exports nothing: This file listens for p8 http requests and proxies them to the real BE

// Requires gpio_messaging.js

pico8_gpioMessaging.addEventListener('message', ({ message }) => {
  if (message.startsWith("HTTP REQ")) {
    const [http, req, cid, method, url] = message.split(" ");
    fetch(url, {
      method
    })
      .then(result => result.text())
      .then(result => pico8_gpioMessaging.sendMessage(`HTTP RES ${cid} ${result}`))
  }
})

I can't embed an example cartrige here because it also needs some setup on the HTML/JS side, but a simple script that requests the datetime from worldclockapi:

#include html_messaging.lua
#include http_request.lua

function _update()
  if btnp(🅾️) then
    http_request:get("http://worldclockapi.com/api/json/utc/now",
      function (response)
        printh(response)
      end
    )
  end

  html_messaging:update()
end

The only thing the host HTML needs is importing both gpio_messaging.js and http_request.js.

Now I only need to write a JSON parser :'D (/s - I know this would be a bad idea).

I think this might be useful for simple ad-hoc services, like global highscores. Also maybe to run around the size limit in cartridge for things like level data, or maybe even to some extend turn-based multiplayer games? :exploding_head:

P#90761 2021-04-18 09:19

nice package, though I suspect that’s only working if destination url=pico web page url (browser will not allow x-site calls).

as for multiplayer & leaderboard, sure, and have already been done (still quite rare as the dev workflow is quite tedious)

P#90766 2021-04-18 15:38 ( Edited 2021-04-18 15:38)
:: merwok

> (browser will not allow x-site calls).

Yes it will, but request success depends on CORS configuration in the target server.

P#90768 2021-04-18 16:01

@freds72 x-site calls are enforced by the browser as you said, but it's the server who defines what policies apply.

For instance, the call to the worldclockapi (http://worldclockapi.com/api/json/utc/now) replies with a header "Access-Control-Allow-Origin: *", which means it can be requested from any domain, so the browser will allow that one through. Most public APIs have open CORS requests, and if you build a server for your own game you can also set it up so it's available for all domains or a whitelist.

P#90770 2021-04-18 16:07

right - I was too paranoid!

P#90774 2021-04-18 16:42

[Please log in to post a comment]

Follow Lexaloffle:        
Generated 2021-11-29 23:38:35 | 0.021s | Q:13