NodeJS driver for Piconet
Piconet allows modern computers (MacOS, PC and Linux) to talk to Acorn Econet networks using a board which interfaces the ADF10 Econet module (BBC Master/Archimedes Econet board) to a Raspberry Pi Pico. The Pico can then be connected to your machine via USB.
This library simplifies the use of Piconet for NodeJS apps written in JavaScript or TypeScript. It handles serial communication with the board and provides facilities to make life easier when dealing with asynchronous events. That said, it is actually a really thin layer over the board's own protocol.
Prerequisites
Piconet works with JavaScript or TypeScript NodeJS apps with:
- Node version 14 and above
- NPM version 6 and above
Installing
Add the piconet driver to your project like this:
npm i --save @jprayner/piconet-nodejs
Getting started
Check out the examples for some simple Econet apps or the source code of the ecoclient project.
The following TypeScript code sends a text message to station 127 using the NOTIFY
immediate operation:
import { driver } from '@jprayner/piconet-nodejs';
const main = async () => {
await driver.connect();
await driver.setEconetStation(32); // make sure this is unused on your network
await sendNotify(127, 'Mary had a little lamb');
await driver.close();
}
const sendNotify = async (station: number, str: string) => {
for (const char of str) {
const scoutExtraData = Buffer.from([0x00, 0x00, char.charCodeAt(0), 0x00]);
await driver.transmit(
station, // destination station number
0, // destination network number
0x85, // control byte for NOTIFY
0x00, // port for immediate operation
Buffer.from(char), // contents of data frame
scoutExtraData, // NOTIFY has unusual extra data in scout
);
}
};
main();
The above is available demo is available as an installable project under the examples folder.
Fault-finding
By default, the connect
function will attempt to autodetect the correct serial device for the Pi Pico using the USB vendor ID and product ID. If this doesn't work — for example, you have multiple Picos attached to your machine — then you can explicitly pass it a device string such as /dev/ttyACM0
for a Linux serial device or COM4
for a Windows serial port.
The connect
function will throw an error if it fails to communicate with the board. Some common reasons for this:
- Piconet board is not connected
- Pico device has not been flashed with the Piconet firmware — see Getting started
- The firmware version loaded into the Pico is not compatible with device driver version (i.e. major/minor version parts don't match) - update the firmware and/or driver version to make them match
The error description should provide additional clues.
Econet protocol essentials
Econet network interfaces (like the ADF10 card) are based upon the MC68B54 Advanced Data Link Controller (ADLC). This handles the business of serialising data onto the wire, performing CRC checks etc. Layered upon this is the Econet protocol which defines the format of discrete messages sent over the network (known as "frames") via the ADLC.
Rather than IP addresses, Econet uses two 8-bit numbers to uniquely identify a device on the network: the station and network numbers. The station number is in the range 1-254. Station 254 is normally reserved for the default fileserver and station 255 is used to indicate a broadcast frame, rather than a actual machine. A network number of zero indicates the local network, with other values used to address machines on other, "bridged" networks.
The TRANSMIT operation & the four-way handshake
The Econet TRANSMIT
operation is a key building block for a wide variety of Econet communications. During a TRANSMIT
operation, four frames are exchanged between client and server in a sequence known as "the four-way handshake".
- the client station sends a "scout" frame to the server
- the server sends an acknowledgement frame back to the client ("ok to proceed")
- the client sends a "data" frame containing the body of the message
- the server sends another acknowledgement frame
This provides a reasonably robust mechanism for sending data. A failure of the client to receive an "ACK" in steps [2] or [3] will often result in a limited number of back-offs and retries, although such mechanisms are application-dependant and out-of-scope here.
To illustrate this, the following *DIR
CLI call was captured using ecoclient monitor
:
MonitorEvent 0.168 --> 0.1
00000000: 01 00 a8 00 80 99 |..¨... |
MonitorEvent 0.1 --> 0.168
00000000: a8 00 01 00 |¨... |
MonitorEvent 0.168 --> 0.1
00000000: 01 00 a8 00 90 00 01 02 04 44 49 52 0d |..¨......DIR. |
MonitorEvent 0.1 --> 0.168
00000000: a8 00 01 00 |¨... |
Each frame begins with the same two bytes for the destination station/network and a further two bytes for the source station/network.
The client semds a scout frame which always has at least two additional bytes following the destination/source addresses:
- the control byte (0x80 in this case, indicating a fileserver operation)
- the port numbber (0x99 here, the well-known fileserver
COMMAND
port) - note that some scout frames contain additional data (e.g. for the
NOTIFY
orPOKE
"immediate" operations)
The server sends a scout acknowledgement back to the client:
- ACK frames only contain the four bytes of destination/source addresses
- a scout ACK means that the server is listening and ready to handle the type of data expected for the control byte/port combination
The client proceeds by sending the body of the message to the server in a data frame:
- the data frame starts with the 4-bytes of destinstation/source addresses
- the format of the remainder of the frame is dependent on the operation being performed (note the text
DIR
of the CLI command) - data frames are typically a couple of kB or less in length
- some operations such as
PEEK
orPOKE
can generate much larger data frames (e.g. 20kB for a screen grab, depending on video mode)
The server sends a further acknowledgement frame back to the client:
- the data ACK indicates that the data frame was received successfully
Broadcast frames
A broadcast frame looks much like a scout. The destination station and network numbers are set to 255 to indicate a broadcast, and a control byte and port indicate the disposition of the data. Upto 8 bytes of data follow (larger amounts of data cause problems on certain Econet stacks).
Here's a capture of a bridge solicitation broadcast message, sent by a BBC Master when BREAK
is pressed:
MonitorEvent 0.168 --> 255.255
00000000: ff ff a8 00 82 9c 42 52 49 44 47 45 9c 00 |ÿÿ¨...BRIDGE.. |
Further reading
The following are recommended reading to learn more about Econet programming and file server protocols:
- Acorn Econet Advanced User Guide
- The Econet System User Guide
- Source code for the ecoclient project
Sending data
The transmit function implements the Econet TRANSMIT
operation by carrying out the sender's role in the four-way handshake. It can be used for regular and immediate operations and it handles sending the scout and data frames, and listening out for acknowledgements.
Handling events
Events are generated in response to network traffic or as a result of certain calls made to the driver.
There are three ways to receive events in your application: waitForEvent
, EventQueue
or a simple event listener. The best option will depend on what you're trying to achieve but waitForEvent
is the simplest to use and is usually adequate.
The first two methods require you to supply an EventMatcher to specify what type of events you are interested in.
To create a matcher which matches all events:
// JavaScript
const matcher = (event) => true;
// TypeScript
const matcher = (event: EconetEvent) => true;
Matchers can be as complex as you like. Perhaps you are only interested in TRANSMIT
operations received from a particular station with a particular control byte and port:
const matcher = (event : EconetEvent) => { // leave off ": EconetEvent" if you're using JavaScript rather than TypeScript
const result =
event instanceof RxTransmitEvent &&
event.scoutFrame.length >= 6 &&
event.scoutFrame[2] === sourceStation &&
event.scoutFrame[3] === sourceNetwork &&
event.scoutFrame[4] === controlByte &&
event.scoutFrame[5] === port
return result;
};
Note that, because events are defined as classes, you can use the instanceof
operator to differentiate them at runtime.
Tip: received data events (subclasses of RxDataEvent) provide hex dumps of scout/data frames, as appropriate, in their toString()
implementations. So doing console.log(event.toString());
can be a useful debugging tool!
waitForEvent
waitForEvent is an async function (returns a promise of an EconetEvent
) which waits for a certain period of time for a matching event to occur. It is simple to use and perfect for simple request/response type operations.
import { driver } from '@jprayner/piconet-nodejs';
... initialise board etc. ...
// wait for upto 1s for a matching event
const event = await waitForEvent(matcher, 1000);
EventQueue
EventQueue
s store matching events in order. An advantage over waitForEvent
is that you won't miss events that occur whilst your app is busy doing other things. This is useful for situations where frames arrive quickly such as Econet fileserver LOAD
operations.
You can wait for events to appear on a queue using eventQueueWait. You can also do a quick synchronous check for new events using eventQueueShift.
Here's an example:
import { driver, RxTransmitEvent } from '@jprayner/piconet-nodejs';
... initialise board etc. ...
const queue = driver.eventQueueCreate(
(event) => event instanceof RxTransmitEvent
);
while (!done) {
const rxTransmitEvent = await driver.eventQueueWait(queue, 1000);
// do some processing
}
driver.eventQueueDestroy(queue);
Note that if you have multiple queues which match a particular event, that event will be appended to each queue.
Simple listener
In most situations the previous two methods are the most convenient way to receive events. However, if you have special requirements (or want to avoid Promises and async/await) then the option of using a simple listener callback is available to you too.
import { driver, EconetEvent, ErrorEvent, RxDataEvent } from '@jprayner/piconet-nodejs';
... initialise board etc. ...
const listener = (event: EconetEvent) => { // leave off ": EconetEvent" if you're using JS
if (event instanceof ErrorEvent) {
console.error(`ERROR: ${event.description}`);
} else if (event instanceof RxDataEvent) {
console.log(`Received ${event.constructor.name}`);
console.log(event.toString());
}
};
driver.addListener(listener);
... do stuff ...
driver.removeListener(listener);
... close driver etc. ...