Skip to content

Instantly share code, notes, and snippets.

@phoddie
Last active August 2, 2021 10:46
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save phoddie/166c9c17b2f31d0beda9f2410a219268 to your computer and use it in GitHub Desktop.
Save phoddie/166c9c17b2f31d0beda9f2410a219268 to your computer and use it in GitHub Desktop.
For Ecma TC53

I/O class pattern

Copyright 2018 Moddable Tech, Inc.
Peter Hoddie
Patrick Soquet
Updated December 17, 2018

Overview

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.

Class Pattern

Constructor

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 and hostName for TCP binding) to illustrate the API. Work remains to be done in this area.

Introductory example

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`);
	}
});

Functions

The following functions are defined.

  • read() - reads one or more values. A read is non-blocking. If it would block, a value of undefined 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.

Properties

The following properties are defined. All properties are read-only.

  • format - The format of the data that the instance accepts for read and write 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 JavaScript Number.
  • "binary" - Data for reads and writes are an ArrayBuffer.
  • "utf-8" - Data for reads and writes are a JavaScript String. The implementations of read and write convert between UTF-8 and the JavaScript engine's internal string representation.

Callbacks

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.

Specific I/O types

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.

Digital

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.

Serial

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.

I2C

The default format for I2C is "binary". This is the only supported format.

The onReadable and onWritable callbacks are not supported.

TCP Socket

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.

UDP Socket

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.

Other I/O types

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

Built-in and external I/O providers

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`);
		}
	});
}

I/O examples

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, and onError. 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 the await 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.

Button controls an LED

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.

Common code

import Digital from "builtin/digital";

Callback code

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());
	}
});

Event code

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());
});		

Async code

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();

Eight buttons control eight LEDs through an expander

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.

Common code

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,
	}
});

Callback code

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());
		}
	});
}

Event code

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());
	});
}

Async code

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);
}

Button toggles a remote LED over a serial connection between two boards

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.

Common code

import Digital from "builtin/digital";
import Serial from "builtin/serial";

Callback code

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"
});

Event code

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);
});

Async code

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();

Long text is written to the serial connection

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.

Common code

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`;

Callback code

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"
});

Event code

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);

Async code

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()

TCP socket sends an HTTP request and logs the HTTP response

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.

Common code

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";

Callback code

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`);
	}
});

Event code

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);
});		

Async code

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();

Button toggles LEDs on remote boards thru UDP

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.

Demo video

Common code

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);
	}
});

Callback code

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]); 
	}
});

Event code

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]);
});

Async code

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();

I/O mixins

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.

Event

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 = [];
		}
	}
}

Async

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;
			 });
		}
	}
}

Change History

  • December 17 - changed GPIO to Digital. Added note that Bank GPIO access is a to-do.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment