Copyright 2018 Moddable Tech, Inc.
Peter Hoddie
Patrick Soquet
Updated December 17, 2018
The I/O class pattern is designed to be applied to many different interfaces. These include digital, analog input, SPI, I2C, serial, TCP socket, UDP socket, etc. Each of these defines a class pattern for the interface (e.g. a Digital class pattern) that is a logical subclass of the I/O class pattern.
The I/O class pattern assumes read and write operations will not block for significant periods of time (this is deliberately not quantified). The model for this is non-blocking network sockets.
The I/O class pattern uses callback functions to deliver asynchronous events to clients. Callbacks correspond well to the underlying native APIs allowing for the most direct implementation. The design is intended to enable efficient implementation of other event delivery mechanisms including Node.js-style Promises
and Web-style EventListeners
. The I/O Examples section explores this in depth.
The I/O class pattern provides direct, efficient access to the capabilities of the underlying hardware. If the hardware does not support a particular capability, the I/O class pattern does not try to emulate the feature. The client may either adapt to the available capabilities or decline to work with the hardware. Frameworks may build on the I/O class pattern to provide uniform capabilities to client software.
The constructor defines binding, configuration, and callbacks.
Binding for hardware includes pin identifications (e.g. pin number and sometimes pin bank). It may also include the pin bus.
The configuration varies by both hardware connection type and MCU. These include the power level of the output, the use of pull up/down resistors, sample / baud rate, etc.
The callbacks are functions bound to the instance to be invoked on various events (e.g. onReadable
).
new IO_Type({
...bindings,
...configurations,
...callbacks,
})
There is no provision for changing the bindings, configuration, and callbacks of the instance. Making them fixed has the following advantages:
- The API is smaller
- The implementation may be optimized using the values provided to the constructor
- A pre-configured instance may be provided to untrusted code without concern that the bindings, configuration, or behavior will be changed.
Note: This document does not make specific recommendations for bindings and configuration. Properties are used (e.g.
pin
for Digital binding andhostName
for TCP binding) to illustrate the API. Work remains to be done in this area.
The design of the I/O Class pattern aims to make simple operations concise. For example, the following code monitors the state of a digital input to the console:
import Digital from "builtin/digital";
new Digital({
pin: 1,
mode: Digital.Input,
onReadable() {
trace(`changed to ${this.read()\n`);
}
});
The following functions are defined.
read()
- reads one or more values. A read is non-blocking. If it would block, a value ofundefined
is returned.write(data)
- writes one or more values. A write is non-blocking. If there is insufficient buffer space to complete the entire write, an exception is thrown.close()
- releases hardware resources. All calls to the instance after close is invoked will fail.
The term non-blocking here is used in a similar way to its use with Berkeley Sockets API. The call may take some time to return and when it returns the read or write operation may still be in progress.
The following properties are defined. All properties are read-only.
format
- The format of the data that the instance accepts forread
andwrite
calls. Each I/O type implements the formats that make sense for its use. The formats defined are:
"byte"
- Data for reads and writes are individual bytes represented as a JavaScriptNumber
.
"binary"
- Data for reads and writes are anArrayBuffer
."utf-8"
- Data for reads and writes are a JavaScriptString
. The implementations ofread
andwrite
convert between UTF-8 and the JavaScript engine's internal string representation.
The following callbacks are defined. None of these are required to be provided by a client.
onReadable() - invoked when new data is available to read.
onWritable() - invoked when space has been freed in the output buffer.
onError() - invoked when an error occurs (e.g. disconnect, serial data parity error, etc).
The arguments to these callbacks, if any, passed to the callbacks depend on the I/O type. For example, a Digital input provides no parameters to
onReadable
whereas a serial connection provides the number of bytes available to be read.
Each I/O type (e.g. digital, serial, I2C, TCP socket, UDP socket) that implements that I/O class pattern defines its use of the read
and write
functions and any arguments to callbacks including onReadable
, onWritable
, and onError
.
The default format
for digital is "byte"
. This is the only supported format.
The onReadable
callback have no parameters.
The onWritable
callback is not supported.
The default format
for Serial is "byte"
. Serial also supports "binary"
and "utf-8"
formats.
The onReadable
and onWritable
callbacks have a single parameter that indicates the number of byes available to read or write.
The default format
for I2C is "binary"
. This is the only supported format.
The onReadable
and onWritable
callbacks are not supported.
The default format
for a TCP socket is "binary"
. The TCP socket also supports the "utf-8"
format.
The onReadable
and onWritable
callbacks have a single parameter that indicates the number of byes available to read or write.
The onWritable
callback is first invoked when the connection is successfully established.
The onError
callback is invoked when the connection ends, whether by disconnect, timeout, or remote close.
The default format
for a UDP socket is "binary"
. The UDP socket also supports the "utf-8"
format.
The onReadable
callbacks has three parameters: the number of bytes available to read in the current packet, the IP address of the sender, and the port number the sender transmitted the packet from.
The onWritable
callback is not supported.
Work remains to explore application of the I/O class pattern to other types of connections. These include:
- SPI
- Servo
- PWM
- Analog
- Stepper
- I2S
- Bank access to digital pins
The JavaScript modules used to work with the built-in I/O of the device executing the JavaScript code are available at the path "builtin", as in "builtin/digital", "builtin/serial", "builtin/i2c", etc. The definition of built-in hardware intuitively understood but is imprecise in actual hardware. Consequently, the choice of which hardware I/O to make available at the "builtin" module path is ultimately the a decision for the implementor of the host for a given device.
The I/O class pattern may be applied to external I/O providers. An external I/O provider contains one or more I/O connections. The external I/O provider may be physically part of the device running the JavaScript software, such as a GPIO expander connected by I2C or serial, or in another part of the world, such as a remote device connected by the network.
The pattern for working with an external I/O provider is to first establish a connection. The API for establishing the connection is not part of the I/O class pattern because it is different for each provider. The provider contains constructors for the types of I/O it supports. The general pattern is here:
import External from "my external provider";
let provider = new External({...dictionary bind and configure provider..});
let digital = new External.Digital({provider, ...usual Digital dictionary..})
Applying this pattern to a GPIO Expander connected over I2C could look like this.
import {MCP23017} from "MCP230XX";
const provider = new MCP23017({address: 0x20});
let digital = new MCP23017.Digital({
provider,
pin: 2,
mode: MCP23017.Digital.Ouptut
});
digital.write(1);
Applying this pattern to an imaginary network service provider of digital could look like this.
import {WebPins} from "WebPins";
const provider = new WebPins({account: "tc53", credentials: "...."});
provider.onReady = function() {
let digital = new WebPins.Digital({
provider,
pin: 2,
mode: WebPins.Digital.Input,
onReadable() {
trace(`value is ${this.read()}\n`);
}
});
}
The following set of examples illustrate client code using digital, serial, I2C (indirectly), TCP, and UDP implementations of the I/O class pattern. Each example is presented in three forms:
- callback - This form uses the callbacks defined by the I/O class pattern:
onReadable
,onWritable
, andonError
. This form is similar to the API style implemented in the native platform code. - event - This form uses an EventListener API where handlers are dynamically added and removed. This form is commonly found on the web.
- async - This form uses JavaScript
async
functions and theawait
statement. This form is similar what has become common in Node.js.
Each example begins with a section of common code shared between the callback, event, and async forms.
These different forms are provided to show how the I/O class pattern may be adapted to match the preferred API style of various frameworks and platforms. The callback form is defined by the I/O class pattern. These event and async form APIs are provided only as examples, and are not part of the I/O class pattern.
The implementation of the event and async forms is done using mixins that are called to dynamically build subclasses of the digital, serial, TCP, and UDP classes. An explanation of the mixin together with the implementation follows the client code examples.
These examples are running code. They have been executed on an ESP8266 MCU using the Moddable SDK.
This example uses a button and LED connected over digital. The builtin/digital
module calls the onReadable
callback when the value of an input pin changes.
import Digital from "builtin/digital";
let led = new Digital({ pin: 2, mode: Digital.Output });
led.write(1); // off
let button = new Digital({ pin: 0, mode: Digital.InputPullUp,
onReadable() {
led.write(this.read());
}
});
import { EventMixin, Readable, Writable} from "event";
class EventDigital extends Readable(EventMixin(Digital)) {};
let led = new EventDigital({ pin: 2, mode: Digital.Output });
let button = new EventDigital({ pin: 0, mode: Digital.InputPullUp });
led.write(1); // off
button.addEventListener("readable", event => {
led.write(button.read());
});
import { AsyncMixin, Readable, Writable} from "async";
class AsyncDigital extends Readable(AsyncMixin(Digital)) {};
let led = new AsyncDigital({ pin: 2, mode: Digital.Output });
let button = new AsyncDigital({ pin: 0, mode: Digital.InputPullUp });
async function loop() {
await led.onWritable();
led.write(1);
for (;;) {
await button.onReadable();
let value = button.read();
await led.onWritable();
led.write(value);
}
}
loop();
This example uses a 16-bit GPIO expander chip (MCP23017) connected to the host using I2C. The expander is instantiated in the common code. To access the pins on the expander, its Digital constructor is used (MCP23017.Digital) rather than the Digital constructor for the built-in pins as done in the previous example.
The I2C expander has an interrupt which is used here to implement the onReadable
callback when the value of input pins changes.
import {MCP23017} from "MCP230XX";
import Digital from "builtin/digital";
const expander = new MCP23017({
address: 0x20,
hz: 100000,
inputs: 0b1111111111111111,
pullups: 0b1111111111111111,
interrupt: {
pin: 0,
mode: Digital.InputPullDown,
}
});
for (let i = 0; i < 8; i++) {
let led = new MCP23017.Digital({
expander,
pin: i,
mode: MCP23017.Digital.Output
});
new MCP23017.Digital({
expander,
pin: i + 8,
mode: MCP23017.Digital.InputPullUp,
onReadable() {
led.write(this.read());
}
});
}
import { EventMixin, Readable, Writable} from "event";
class EventDigital extends Readable(EventMixin(MCP23017.Digital)) {};
for (let i = 0; i < 8; i++) {
let led = new EventDigital({
expander,
pin: i,
mode: MCP23017.Digital.Output
});
let button = new EventDigital({
expander,
pin: i + 8,
mode: MCP23017.Digital.InputPullUp,
});
button.addEventListener("readable", event => {
led.write(button.read());
});
}
import { AsyncMixin, Readable, Writable} from "async";
class AsyncDigital extends Readable(AsyncMixin(MCP23017.Digital)) {};
async function loop(button, led) {
for (;;) {
await button.onReadable();
let value = button.read();
await led.onWritable();
led.write(value);
}
}
for (let i = 0; i < 8; i++) {
let led = new AsyncDigital({
expander,
pin: i,
mode: MCP23017.Digital.Output
});
let button = new AsyncDigital({
expander,
pin: i + 8,
mode: MCP23017.Digital.InputPullUp
});
loop(button, led);
}
This example uses two devices connected together over a serial connection. Each device executes this example. When the button is pressed, the device sends ASCII 33 (!) over serial and ASCII 42 (*) when the button is released. When a device receives ASCII 33 it turns on the LED and when it receives ASCII 42 it turns the light off.
The builtin/serial
implementation provides both onReadable
and onWritable
callbacks. The default I/O format for serial is "byte" allowing the client code to read and write numbers to the serial instance.
import Digital from "builtin/digital";
import Serial from "builtin/serial";
let led = new Digital({ pin: 2, mode: Digital.Output });
led.write(1); // off
let button = new Digital({ pin: 0, mode: Digital.InputPullUp,
onReadable() {
let value = button.read();
serial.write(value ? 33 : 42);
}
});
let serial = new Serial({
onReadable(count) {
while (count > 1) {
serial.read();
count--;
}
let value = serial.read();
led.write(value == 33 ? 1 : 0);
},
format: "byte"
});
import { EventMixin, Readable, Writable} from "event";
class EventDigital extends Readable(EventMixin(Digital)) {};
class EventSerial extends Readable(EventMixin(Serial)) {};
let led = new EventDigital({ pin: 2, mode: Digital.Output });
led.write(1); // off
let button = new EventDigital({ pin: 0, mode: Digital.InputPullUp });
let serial = new EventSerial({format: "byte"});
button.addEventListener("readable", event => {
let value = button.read();
serial.write(value ? 33 : 42);
});
serial.addEventListener("readable", event => {
let count = event.count;
while (count > 1) {
serial.read();
count--;
}
let value = serial.read();
led.write(value == 33 ? 1 : 0);
});
import { AsyncMixin, Readable, Writable} from "async";
class AsyncDigital extends Readable(AsyncMixin(Digital)) {};
class AsyncSerial extends Readable(AsyncMixin(Serial)) {};
let led = new AsyncDigital({ pin: 2, mode: Digital.Output });
let button = new AsyncDigital({ pin: 0, mode: Digital.InputPullUp });
let serial = new AsyncSerial({format: "byte"});
async function pollButton() {
for (;;) {
await button.onReadable();
let value = button.read();
await serial.onWritable();
serial.write(value ? 33 : 42);
}
}
async function pollSerial() {
await led.onWritable();
led.write(1);
for (;;) {
let result = await serial.onReadable();
let count = result.count;
while (count > 1) {
serial.read();
count--;
}
let value = serial.read();
await led.onWritable();
led.write(value == 33 ? 1 : 0);
}
}
pollButton();
pollSerial();
This example transmits a long section of text over serial. The onWritable
callback is used as a flow control, so each write contains the number of bytes of data corresponding to the free space in the serial output buffer.
The builtin/serial
implementation provides both onReadable
and onWritable
callbacks. In the callback example, the I/O format is configured to UTF-8 when calling the constructor allowing the client code to write JavaScript strings directly to the serial instance. In the event and async examples, the I/O format uses the default which is bytes.
import Serial from "builtin/serial";
const text = `At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum deleniti atque corrupti, quos dolores et quas molestias excepturi sint, obcaecati cupiditate non-provident, similique sunt in culpa, qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio, cumque nihil impedit, quo minus id, quod maxime placeat, facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet, ut et voluptates repudiandae sint et molestiae non-recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat`;
let offset = 0;
let serial = new Serial({
onWritable(count) {
count = Math.min(text.length - offset, count);
if (0 == count) return;
serial.write(text.slice(offset, offset + count));
offset += count;
},
format: "utf-8"
});
import { EventMixin, Readable, Writable} from "event";
class EventSerial extends Writable(EventMixin(Serial)) {};
let serial = new EventSerial({});
let offset = 0;
let size = text.length;
let listener = event => {
let count = event.count;
while (count && (offset < size)) {
serial.write(text.charCodeAt(offset));
count--;
offset++;
}
if (offset == size)
serial.removeEventListener("writable", listener);
}
serial.addEventListener("writable", listener);
import { AsyncMixin, Readable, Writable} from "async";
class AsyncSerial extends Writable(AsyncMixin(Serial)) {};
async function loop() {
let serial = new AsyncSerial({});
let offset = 0;
let size = text.length;
for (;;) {
let result = await serial.onWritable();
let count = result.count;
while (count && (offset < size)) {
serial.write(text.charCodeAt(offset));
count--;
offset++;
}
if (offset == size)
break;
}
}
loop()
This example opens a TCP connection to a web site, requests the root page, and outputs the page source code to the console.
The builtin/socket
implementation provides both onReadable
and onWritable
callbacks. The I/O format is configured to "utf-8" when calling the constructor allowing client code to read and write JavaScript strings to the socket.
import { TCP } from "builtin/socket";
const host = "example.com";
const port = 80;
const msg = "GET / HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Connection: close\r\n" +
"\r\n";
let once = false;
new TCP({ host, port, format: "utf-8",
onWritable() {
if (once) return;
once = true;
this.write(msg);
}
onReadable() {
trace(this.read(), "\n");
}
onError(message) {
trace(`error "${message}"\n`);
}
});
import { EventMixin, Readable, Writable} from "event";
class EventTCP extends Writable(Readable(EventMixin(TCP))) {};
let once = false;
let skt = new EventTCP({ host, port, format: "utf-8" });
skt.addEventListener("error", event => {
trace(`error "${event.message}"\n`);
});
skt.addEventListener("readable", event => {
trace(skt.read(), "\n");
});
skt.addEventListener("writable", event => {
if (once) return;
once = true;
skt.write(msg);
});
import { AsyncMixin, Readable, Writable} from "async";
class AsyncTCP extends Writable(Readable(AsyncMixin(TCP))) {};
async function loop() {
let skt = new AsyncTCP({ host, port, format: "utf-8" });
await skt.onWritable();
skt.write(ArrayBuffer.fromString(msg));
try {
for (;;) {
await skt.onReadable();
trace(skt.read(), "\n");
}
}
catch (e) {
trace(e + "\n");
}
}
loop();
This example allows the button on one device to control the LEDs on one or more other devices. The devices are all on the same local network and communicate with UDP packets. Each device executes the example code below.
By default, a UDP socket is configured to send and receive an ArrayBuffer
, so no configuration of the I/O mode
is necessary for this example.
The common code in this example uses mDNS to announce the devices on the local network and discover each other. The list of discovered devices is maintained in the remotes
array.
import MDNS from "mdns";
import Digital from "builtin/digital";
import { UDP } from "builtin/socket";
const hostName = "example";
const port = 1000;
let remotes = [];
const mdns = new MDNS({hostName}, function(message, value) {
if ((1 === message) && value) {
mdns.add({ name:"digital", protocol:"udp", port, txt:{} });
}
});
mdns.monitor("_digital._udp", function(service, instance) {
let remote = remotes.find(item => item == instance);
if (!remote) {
remotes.push(instance);
}
});
let led = new Digital({ pin: 2, mode: Digital.Output });
led.write(1); // off
let button = new Digital({ pin: 0, mode: Digital.InputPullUp,
onReadable() {
let data = new Uint8Array(10).fill(button.read());
remotes.forEach(remote => {
udp.write(remote.address, remote.port, data.buffer);
});
}
});
let udp = new UDP({ port,
onReadable(count, address, port) {
let data = new Uint8Array(udp.read());
led.write(data[0]);
}
});
import { EventMixin, Readable, Writable} from "event";
class EventDigital extends Readable(EventMixin(Digital)) {};
class EventUDP extends Readable(EventMixin(UDP)) {};
let led = new EventDigital({ pin: 2, mode: Digital.Output });
led.write(1); // off
let button = new EventDigital({ pin: 0, mode: Digital.InputPullUp });
let udp = new EventUDP({ port });
button.addEventListener("readable", event => {
let data = new Uint8Array(10).fill(button.read());
remotes.forEach(remote => {
udp.write(remote.address, remote.port, data.buffer);
});
});
udp.addEventListener("readable", event => {
let data = new Uint8Array(udp.read());
led.write(data[0]);
});
import { AsyncMixin, Readable, Writable} from "async";
class AsyncDigital extends Readable(AsyncMixin(Digital)) {};
class AsyncUDP extends Readable(AsyncMixin(UDP)) {};
let button = new AsyncDigital({ pin: 0, mode: Digital.InputPullUp });
let led = new AsyncDigital({ pin: 2, mode: Digital.Output });
let udp = new AsyncUDP({ port });
async function pollButton() {
for (;;) {
await button.onReadable();
let data = new Uint8Array(10).fill(button.read());
remotes.forEach(remote => {
udp.write(remote.address, remote.port, data.buffer);
});
}
}
async function pollUDP() {
await led.onWritable();
led.write(1);
for (;;) {
await udp.onReadable();
let data = new Uint8Array(udp.read());
await led.onWritable();
led.write(data[0]);
}
}
pollButton();
pollUDP();
The base I/O classes are callback based in order to build non-blocking applications that execute efficiently and have a minimal memory footprint.
To provide event based and promise based programming interfaces, two modules define mixins to transform the base I/O classes.
A mixin is just a function that extends a class into another class with more specific constructors and methods.
The EventMixin
function extends the Base
class with methods to add and remove event listeners, installs the onError
callback and prepares the error
listener array.
function onError(message) {
let event = new Error(message);
this.error.forEach(listener => listener.call(null, event));
}
export function EventMixin(Base) {
return class extends Base {
constructor(dictionary) {
if (dictionary.onError)
throw new Error("no error callback");
super({ onError, ...dictionary });
this.error = [];
}
addEventListener(event, listener) {
let listeners = this[event];
if (!listeners)
throw new Error("no such event");
listeners.push(listener);
}
removeEventListener(event, listener) {
let listeners = this[event];
if (!listeners)
throw new Error("no such event");
let index = listeners.find(item => item === listener);
if (index >= 0)
listeners.splice(index, 1);
}
}
}
The Readable
mixin installs the onReadable
callback and prepares the readable
listeners array.
function onReadable(count, ...info) {
let event = { count, info };
this.readable.forEach(listener => listener.call(null, event));
}
export function Readable(Base) {
return class extends Base {
constructor(dictionary) {
if (dictionary.onReadable)
throw new Error("no readable callback");
super({ onReadable, ...dictionary });
this.readable = [];
}
}
}
The Writable
mixin installs the onWritable
callback and prepares the writable
listeners array.
function onWritable(count, ...info) {
let event = { count, info };
this.writable.forEach(listener => listener.call(null, event));
}
export function Writable(Base) {
return class extends Base {
constructor(dictionary) {
if (dictionary.onWritable)
throw new Error("no writable callback");
super({ onWritable, ...dictionary });
this.writable = [];
}
}
}
The AsyncMixin
function extends the Base
class with onReadable
and onWritable
methods that return promises. By default such methods resolve immediately, for the sake of I/O classes without readable and writable callbacks. The extended class also installs the onError
callback and the mechanism to reject readable and writable promises.
function onError(message) {
let error = new Error(message), reject;
reject = this._onReadableReject;
if (reject) {
this._onReadableResolve = null;
this._onReadableReject = null;
reject(error);
}
else
this._onReadableError = error;
reject = this._onWritableReject;
if (reject) {
this._onWritableResolve = null;
this._onWritableReject = null;
reject(error);
}
else
this._onWritableError = error;
}
export function AsyncMixin(Base) {
return class extends Base {
constructor(dictionary) {
if (dictionary.onError)
throw new Error("no error callback");
super({ onError, ...dictionary });
this._onReadableError = null;
this._onWritableError = null;
}
onReadable() {
let error = this._onReadableError;
if (error) {
this._onReadableError = null;
return Promise.reject(error);
}
return Promise.resolve(null)
}
onWritable() {
let error = this._onWritableError;
if (error) {
this._onWritableError = null;
return Promise.reject(error);
}
return Promise.resolve(null)
}
}
}
The Readable
function installs the onReadable
callback and overrides the onReadable
method to return a promise that such callback will resolve, or that the onError
callback will reject. The promise is resolved or rejected immediately if the callbacks have already been called.
function onReadable(count, ...info) {
let result = { count, info };
let resolve = this._onReadableResolve;
if (resolve) {
this._onReadableResolve = null;
this._onReadableReject = null;
resolve(result);
}
else
this._onReadableResult = result;
}
export function Readable(Base) {
return class extends Base {
constructor(dictionary) {
if (dictionary.onReadable)
throw new Error("no readable callback");
super({ onReadable, ...dictionary });
this._onReadableResult = null;
this._onReadableResolve = null;
this._onReadableReject = null;
}
onReadable() {
if (this._onReadableResolve)
throw new Error("already reading");
let error = this._onReadableError;
if (error) {
this._onReadableError = null;
return Promise.reject(error);
}
let result = this._onReadableResult;
if (result) {
this._onReadableResult = null;
return Promise.resolve(result);
}
return new Promise((resolve, reject) => {
this._onReadableResolve = resolve;
this._onReadableReject = reject;
});
}
}
}
The Writable
function installs the onWritable
callback and overrides the onWritable
method to return a promise that such callback will resolve, or that the onError
callback will reject. The promise is resolved or rejected immediately if the callbacks have already been called.
function onWritable(count, ...info) {
let result = { count, info };
let resolve = this._onWritableResolve;
if (resolve) {
this._onWritableResolve = null;
this._onWritableReject = null;
resolve(result);
}
else
this._onWritableResult = result;
}
export function Writable(Base) {
return class extends Base {
constructor(dictionary) {
if (dictionary.onWritable)
throw new Error("no writable callback");
super({ onWritable, ...dictionary });
this._onWritableResult = 0;
this._onWritableResolve = null;
this._onWritableReject = null;
}
onWritable() {
if (this._onWritableResolve)
throw new Error("already writing");
let error = this._onWritableError;
if (error) {
this._onWritableError = null;
return Promise.reject(error);
}
let result = this._onWritableResult;
if (result) {
this._onWritableResult = null;
return Promise.resolve(result);
}
return new Promise((resolve, reject) => {
this._onWritableResolve = resolve;
this._onWritableReject = reject;
});
}
}
}
- December 17 - changed GPIO to Digital. Added note that Bank GPIO access is a to-do.