Log In  


The fact you can read and write PICO-8 GPIO values from the web wrapper is awesome - it means you can do things like emit vibrations according to game state, change the site theme to match different in-game levels, or maybe even develop a brand new input scheme.

However the API for reading these values is a bit basic... PICO-8 writes GPIO values to an array, and it's up to you, the developer to check those values periodically to see if they're different. What if you could just tell JavaScript to let you know whenever the GPIO pins get updated with new values?

Voilà:

var gpio = getP8Gpio();
var unsubscribe = gpio.subscribe(function(indices) {
  console.log(
    'New values at indices ' + indices.join(', ') + ': ' +
    indices.map(function(i) { return gpio[i]; }).join(', ')
  );
});
// unsubscribe later if you want...
unsubscribe();

This uses JavaScript setters under the hood to watch each index in the array, which means the watching part is shoved into a native background thread (== faster!). No need to write once-a-frame JS iterator loops or anything like that. Whenever PICO-8 writes to the GPIO array, you get notified immediately.

Of course you can also write back to the GPIO array:

gpio[3] = 255;
gpio[4] = 0;
New values at indices 3, 4: 255, 0

Your listener will be notified about your own changes as well; it's up to you if you want to do anything with that.

By default, listeners only get called if at least one value has changed in the last call stack. If you want to be notified about every update, new value or not, you can pass a second argument, verbose, which is a boolean:

gpio.subscribe(function(indices) {
  console.log(
    'The values ' +
    indices.map(function(i) { return gpio[i]; }).join(', ') +
    ' at indices ' + indices.join(', ') +
    ' probably didn\'t change, but I am logging them anyway..'
  );
}, true);

Here's the library code (~75 lines of JavaScript):

var getP8Gpio;

(function() {
  getP8Gpio = _getP8Gpio;
  var size = 8;

  // extends Array prototype
  function PicoGpioArray() {
    Array.call(this, size);
    this._data = Array(size);
    this._listeners = [];
    this._pending = {};
    this._pendingNew = {};
    this._pendingTimeout = null;
    this.dispatchPending = this.dispatchPending.bind(this);
    Object.seal(this);
  }

  PicoGpioArray.prototype = Object.create(Array.prototype);
  PicoGpioArray.prototype.constructor = PicoGpioArray;

  // listener callback is required. second argument (verbose) is a boolean
  // and assumed to be false if not provided.
  PicoGpioArray.prototype.subscribe = function subscribe(listener, verbose) {
    listener.verbose = Boolean(verbose);
    this._listeners.push(listener);
    return (function unsubscribe() {
      this._listeners.splice(this._listeners.indexOf(listener), 1);
    }).bind(this);
  };

  // alert listeners of all values changed during the last call stack
  PicoGpioArray.prototype.dispatchPending = function dispatchPending() {
    var pendingIndices = Object.keys(this._pending).map(Number);
    var pendingNewIndices = Object.keys(this._pendingNew).map(Number);
    for (var i = 0; i < size; i++) {
      delete this._pending[i];
      delete this._pendingNew[i];
    }
    if (!pendingIndices.length) {
      return;
    }
    for (var l = 0; l < this._listeners.length; l++) {
      var indices = this._listeners[l].verbose
        ? pendingIndices
        : pendingNewIndices;
      if (indices.length) {
        this._listeners[l](indices);
      }
    }
  };

  // intercept assignments to each GPIO pin to notify listeners
  for (var i = 0; i < size; i++) {
    (function(index) {
      Object.defineProperty(PicoGpioArray.prototype, index, {
        get: function() {
          return this._data[index];
        },
        set: function(value) {
          clearTimeout(this._pendingTimeout);
          this._pending[index] = true;
          if (this._data[index] !== value) {
            this._pendingNew[index] = true;
          }
          this._data[index] = value;
          this._pendingTimeout = setTimeout(this.dispatchPending);
        }
      });
    })(i);
  }

  function _getP8Gpio() {
    // initialize only once
    window.pico8_gpio = window.pico8_gpio || new PicoGpioArray();
    return window.pico8_gpio;
  }
})();

Old version (API changed):


Voilà:

var gpio = getP8Gpio();
var unsubscribe = gpio.subscribe(function(index) {
  console.log('New value at index ' + index ' + ': ' + gpio[index]);
});
// unsubscribe later if you want...
unsubscribe();

This uses JavaScript setters under the hood to watch each index in the array, which means the watching part is shoved into a native background thread (== faster!). No need to write once-a-frame JS iterator loops or anything like that. Whenever PICO-8 writes to the GPIO array, you get notified immediately.

Of course you can also write back to the GPIO array:

gpio[3] = 255;

Your listener will be notified about your own changes as well; it's up to you if you want to do anything with that.

By default, listeners only get called if a value has changed. If you want to be notified about every update, new value or not, you can pass a second argument, verbose, which is a boolean:

gpio.subscribe(function(index) {
  console.log(
    'The value ' + gpio[index] + ' at index ' + index +
    ' probably didn't change, but I am logging it anyway..'
  );
}, true);

Here's the library code (50 lines of JavaScript):

var getP8Gpio;

(function() {
  getP8Gpio = _getP8Gpio;
  var size = 8;

  // extends Array prototype
  function PicoGpioArray() {
    Array.call(this, size);
    this._data = Array(size);
    this._listeners = [];
    Object.seal(this);
  }

  PicoGpioArray.prototype = Object.create(Array.prototype);
  PicoGpioArray.prototype.constructor = PicoGpioArray;

  // listener callback is required. second argument (verbose) is a boolean
  // and assumed to be false if not provided.
  PicoGpioArray.prototype.subscribe = function subscribe(listener, verbose) {
    listener.verbose = Boolean(verbose);
    this._listeners.push(listener);
    return (function unsubscribe() {
      this._listeners.splice(this._listeners.indexOf(listener), 1);
    }).bind(this);
  };

  // intercept assignments to each GPIO pin to notify listeners
  for (var i = 0; i < size; i++) {
    (function(index) {
      Object.defineProperty(PicoGpioArray.prototype, index, {
        get: function() {
          return this._data[index];
        },
        set: function(value) {
          var isNew = this._data[index] !== value;
          this._data[index] = value;
          this._listeners.forEach(function(listener) {
            if (isNew || listener.verbose) {
              listener(index);
            }
          });
        }
      });
    })(i);
  }

  function _getP8Gpio() {
    // initialize only once
    window.pico8_gpio = window.pico8_gpio || new PicoGpioArray();
    return window.pico8_gpio;
  }
})();

11


I just revised the original post with a new version and hid the old one. Previously, listeners were notified every time an individual GPIO pin was updated, and they were called with the single index of that value. Now, the listener is deferred until the call stack has completed, and the listener is called with an array containing each index that was updated. This is probably more useful in general, since GPIO pins may tend to be updated in bulk.


Here's the project on GitHub: https://github.com/benwiley4000/pico8-gpio-listener

Feel free to open issues or pull requests!

And here's a live demo: https://benwiley4000.github.io/pico8-gpio-listener/


awesome! thanks.



[Please log in to post a comment]