Introduction
WebUSB is an HTML5 API for browser interactions with USB devices. WebUSB provides developers a simple cross platform for programming devices and for users easy accesses and installations of applications for their devices. The W3C Community Group claims that a major benefit of WebUSB is educating high school and middle school students about computers and conducting science experiments using the new devices. In fact, this lecture will use the micro:bit. Micro:bit is a small processor board designed for middle and high school education.
The W3C proposed API documentation is at:
https://wicg.github.io/webusb/
MDN documentation for USB and USBDevice classes are at:
https://developer.mozilla.org/en-US/docs/Web/API/USB & https://developer.mozilla.org/en-US/docs/Web/API/USBDevice
If you wish to learn more detail about USB in general, I suggest reading “USB in a NutShell”:
https://www.beyondlogic.org/usbnutshell/usb1.shtml
I don’t expect you to read these references at this time.
Using WebUSB differs from using native USB because of security concerns that USB access could exploit. Consequently, the browser requires user interaction to confirm interfacing with the USB device and the webpage server via https protocol. When the JavaScript (JS) code request a device, the browser alerts the user and request that the user selects the device. After the USB device is selected, the JS code chooses a configuration and claims an interface for communication. After the setup, the JS code can communicate with the USB device using transfers in and out. The direction of the transfer is from the perspective of the host, the browser. So transferring out is from the host to the USB device and transferring in is from the USB device to the host.
This lecture will demonstrate reading the micro:bit serial bus through the USB. We will need to write a short program on the micro:bit that writes a random sequence. We will need to flash the micro:bit, and will do this using the browser. WebUSB is being used in the background during the flashing. We then make a webpage which will connect to the micro:bit and configure the communication. Finally, our web page will read and display a random sequence on the page.
Before we get started programming, we should check which browser support WebUSB.
https://caniuse.com/#feat=webusb
Only Chrome and Opera browser support WebUSB. The rest of this lecture assumes that you are using the latest version of chrome.
Programming the Micro:bit
The micro:bit, https://microbit.org/, is a small microprocessor board with a nRF51822 ARM microprocessor. It has a 5×5 LED matrix, two user buttons, motion sensor, Bluetooth, USB communication and much more.
https://tech.microbit.org/hardware/
You can program the micro:bit on the browser using a variant of JavaScript or a visual code editor by MicroSoft called MakeCode.
https://makecode.microbit.org/
Flashing the micro:bit is through the USB either by dragging and dropping the compile file via the file explorer or using the MakeCode webpage which is using WebUSB. You’ll experience both.
Get Started
Be sure that you are using the latest chrome browser.
To get started programming follow the instructions at
https://microbit.org/guide/quick/
At Step 2, make sure to select “MakeCode Editor”. The button should be purple as opposed to yellow. Click the orange “Get Coding” button to get to your project page. At the bottom of the project page is a list of Tutorials. Do the “Flashing Heart” tutorial. The tutorial will demonstrate using the visual editor to control the LED array and flash the micro:bit using the file explorer. The rest of this lecture assumes that you have done the tutorial. I encourage you to experiment with programming the micro:bit.
JavaScript Programming
To start a new project, in the MakeCode editor, click the “Home” button in the navigation bar at the upper left. On the project page, you should see a list of your projects. There should be the “Flashing Heart”. Click the large purple “New Project” button just to left of your project list. This takes you to an empty MakeCode editor page.
Let us use the JavaScript to program the micro:bit.
Switch from display the code in “Blocks” to JavaScript by clicking “{ } JavaScript” toggle in the center of the top navigation bar. Copy the code below and paste it into the editor window.
input.onButtonPressed(Button.A, function () {
serial.writeLine("event:door")
})
basic.forever(function () {
serial.writeValue("temp", Math.randomRange(150, 350) / 10)
basic.pause(1000)
})
The code sets up a handler for button presses which writes “event: door” to the serial buffer. It then creates a loop that runs forever that writes a random variable to the serial bus and then pauses for a second before writing another random value.
Test your code by viewing simulator console, click the “Show Console Simulator” button just below image of the micro:bit on the right. Note the browser window needs to be sufficiently large for the “Show …” button to be visible.
The temp variable is displayed over time in graph. Note that the graph displays only 30 seconds of data. Below the graph is a console of what is written to the serial buffer. Try pushing the buttons in the image of the micro:bit on the right and watch the console.
Let us flash the micro:bit using WebUSB.
Leave the simulator by clicking the gray “Go back” button above the graph. Click the gear on the right in the top navigation bar, select “Pair Device” and follow the instructions, which is to connect the micro:bit and click the green “Pair device” button at the bottom right of the modal.
To flash the micro:bit, click the purple “Download” button at the bottom left. A spinner replaces the button and may take a while to finish downloading the code to the micro:bit. A new “Show console Device” button should appear below image of the micro:bit. Click it. The view is the same as the simulator console. To test that what you are seeing in the console is really from the micro:bit, try clicking the buttons on the micro:bit and watch the console.
There is no button to “disconnect” the micro:bit on the MakeCode page. They only way to disconnect the micro:bit is by closing the MakeCode browser window. Be sure that you do that before you start programming the web page to connect to the micro:bit. Only one browser window at a time can access the USB device.
Programming the Browser
Read the tutorials:
Access USB Devices on the Web by François Beaufort: https://developers.google.com/web/updates/2016/03/access-usb-devices-on-the-web uses arduino and programming with promises and then calls.
WebUSB by example by Gergana Young: https://medium.com/@gerybbg/webusb-by-example-b4358e6a133c uses nRF52 dongle and programming with async functions.
You probably do not have the devices, so you’ll not be able to actually perform the tutorial. The rest of this lecture assumes that you have studied the tutorials.
Let us review what we have learned from the tutorial. François describes that for security the browser can only access the USB using a HTTPS server. For development, we do not need a HTTPS to access the web page using WebUSB. The browser will permit access to the USB via localhost. We will need a webpage, so we will need to write an index.html file and a JavaScript file.
The JS code will setup the device by first requesting the device:
navigator.usb.requestDevice(...);
Which will return an USBDevice, device, after the user selects the USB device in the modal. Then the JS code selects a configuration for a device:
device.selectConfiguration(1);
Although USB devices can have more than one configurations, most only have one, typically named 1. A configurations can have multiple interfaces for communication. The JS code claims an interface with:
device.claimInterface(anInterger);
An interface defines the communication and typically two or no end points. End points typically define the direction of communication. The USB device is now ready for communication with the host.
There are 4 general modes for USB comminctions
- CONTROL transfers, used to send or receive configuration or command parameters to a USB device are handled with controlTransferIn(setup, length) and controlTransferOut(setup, data).
- INTERRUPT transfers, used for a small amount of time sensitive data are handled with the same methods as BULK transfers with transferIn(endpointNumber, length) and transferOut(endpointNumber, data).
- ISOCHRONOUS transfers, used for streams of data like video and sound are handled with isochronousTransferIn(endpointNumber, packetLengths) and isochronousTransferOut(endpointNumber, data, packetLengths).
- BULK transfers, used to transfer a large amount of non-time-sensitive data in a reliable way are handled with transferIn(endpointNumber, length) and transferOut(endpointNumber, data).
The direction of transfer is specified by the succeeding “In” or “Out” in the transform call. The direction of data transfer is with respect to the host, in our case the browser. Transfer out is from the browser to the device and transfer in is from the device to the browser. Note that the control transfer is the only transfer that does not specify an endpoint. The setup in the controlTransfer call does specify the interface.
Browser JS code
Make a directory for your project and create three files:
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>WebUSB Micro:bit</title>
<link rel="stylesheet" href="styles.css" />
<link
href="https://fonts.googleapis.com/css?family=Orbitron"
rel="stylesheet"
/>
</head>
<body>
<h1>Micro:bit USB</h1>
<input class="button" type="button" id="connectButton" value="Connect" />
<input class="button" type="button" id="disconnectButton" value="Disconnect" />
<hr/>
<br/>
<div id="connected">
<div id ="connected-text">
</div>
<div id="usb-output">
</div>
</div>
<script src="index.js" type="module"></script>
</body>
</html>
styles.css
body {
text-align: center;
font-family: 'Orbitron', sans-serif;
}
.button {
background-color: black;
border: none;
color: #ffc000;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
margin: 4px 2px;
cursor: pointer;
font-size: 24px;
}
#connected, #disconnectButton {
font-size: 24px;
}
#disconnectButton {
display: none;
}
#pinNumber {
font-size: 24px;
width: 50px;
}
index.js
/*
* DOM elments
*/
const connectButton = document.getElementById("connectButton");
const disconnectButton = document.getElementById("disconnectButton");
const connected = document.getElementById("connected");
const connectedText = document.getElementById("connected-text");
const usbOutput = document.getElementById("usb-output");
// initial text
connectedText.innerText = "Micro:bit not connected."
/**
* Parser with buffer (Array) for parsing the micro:bit sieral port.
* Assumes:
* * Sequence of ascii characters, Uint8.
* * Data organized by key-value pair.
* * Sequences of key-values are separated by CR followed by LF.
* * Key and values are separated by a colon.
* * Leading and trailing spaces are not important.
* * Also assumes that the op and length have been stripped off.
*
* The instance should be created by passing a handler for the parsed
* key-value pair in the constructor. Use the class instance
* by calling parse(input).
**/
class BufferParser {
// Key-value sequence separator
#CR = 0xd; // 13
#LF = 0xa; //10
// CR proceeds LF
// key-value separator
#COLON = 0x3a; // 58
// Spaces pad the message
#SPACE = 0x20; // 32
// Buffer from the characters from the serial port
#buffer = new Array();
// An array is used because it does not have a fixed length.
constructor(handler){
if (handler != undefined ) this.handle = handler;
}
parse(input){
// input should be an Array with Uint8 elements
// add input to the end of buffer
this.#buffer.push(...input);
while (this.#buffer.length > 0) { // Parsing while loop.
// Find CR index separating the key-value sequence.
// and the COLON index separatinng the key and value.
// console.log("parse: in while.");
const crIndex = this.#buffer.findIndex(e => e == this.#CR);
// console.log("CR: "+crIndex);
const colonIndex = this.#buffer.findIndex(e => e == this.#COLON);
// console.log("COLON: "+colonIndex);
// This is the exit from the while loop.
if (crIndex == -1 || colonIndex == -1) break;
// The buffer is not large enough to parse.
if (colonIndex > crIndex){
console.log("Pasing: Buffer corrupted to the left of CR. Throwing it away.");
this.#buffer.splice(0, crIndex+1);
continue; // try parsing the rest of the buffer.
}
else{ // colonIndex < crIndex. There is a key value pair.
let lfIndex = this.#buffer.findIndex(e => e == this.#LF);
// console.log("LF: "+lfIndex);
// Ideally LF index should be at the left of the key and at 0 index.
// LF does not exist, we will assume that the data is OK.
if(lfIndex == -1) console.log("Parsing: LF was not found. Setting LF is -1.");
// LF to the right of CR. LF noramlly follows CR.
// We do not want it. Next iteration this LF will be at index 0.
if (lfIndex > crIndex ) {
console.log("Parsing: No LF before CR. Setting LF to -1.");
lfIndex = -1;
}
// This should never happen. So if it does, throw it awway.
if (lfIndex > colonIndex && lfIndex < crIndex){
console.log("Parising: LF > COLON. Key-value pair is corrupted. Throwing it away.");
this.#buffer.splice(0, crIndex+1);
continue; // try parsing next section.
}
// This is just a warning that part of the data will be lost.
if (lfIndex > 0 && lfIndex < crIndex)
console.log("Parising: LF > 0. Throwing away left portion.");
// This shoud be the typical case, LF at -1 or 0.
// Get the key and the value.
const key = this.#buffer.slice(lfIndex + 1, colonIndex);
const value = this.#buffer.slice(colonIndex + 1, crIndex);
// Dispose of the parsed section from the buffer.
// There is a LF after CR it will be located at 0 after the splice.
this.#buffer.splice(0, crIndex+1);
// Handle the key and value pair.
this.handle(key, value);
} // end else colonIndex < crIndex.
} // end Parsing while loop.
} // end parse method.
// Default Handler
handle(key, value){
console.log("key: "+String.fromCharCode(...key)+" | value: "+String.fromCharCode(...value));
}
} // end class BufferParser
let myHandler = (key, value) => {
const keyString = String.fromCharCode(...key).trim();
const valueString = String.fromCharCode(...value).trim();
console.log("myHandler: key: "+keyString+" | value: "+valueString);
switch (keyString) {
case "temp":
// Display the temperature on the display
const temp = Number.parseFloat(valueString).toFixed(2);
usbOutput.innerText = "Temperature is "+temp+" C";
break;
case "event":
// display the event
usbOutput.innerText = "Button Pressed";
break;
default:
console.log("Unexpected key: "+keyString+"the value was: "+valueString);
}
}
let parser = new BufferParser(myHandler);
/**
* Initialize communication by:
* 1. Request device
* 2. Select a configuration
* 3. Claim an interface
**/
// Vendor id
const microbitId = 0x0d28;
let device; // This is the USBDevice selected by the user.
connectButton.onclick = async () => {
device = await navigator.usb.requestDevice({
filters: [{ vendorId: microbitId }]
});
console.log(device);
await device.open();
console.log("opened");
await device.selectConfiguration(1);
console.log("configuration selected");
await device.claimInterface(interfaceNumber);
console.log("claimed interface "+ interfaceNumber);
// Styling after connected.
connectedText.innerText = "Micro:bit connected!"
connectedText.style.display = "block";
usbOutput.style.display = "block";
connectButton.style.display = "none";
disconnectButton.style.display = "initial";
listen();
};
// vendor specific settings
const controlTransferGetReport = 0x01;
const controlTransferSetReport = 0x09;
const controlTransferOutReport = 0x200;
const controlTransferInReport = 0x100;
// Interface for the controlled transfers. There are no endpoints.
const interfaceNumber = 4;
// Settings for controlled Transefer into host.
const controlTransferInSettings = {
requestType: "class",
recipient: "interface",
request: controlTransferGetReport,
value: controlTransferInReport,
index: interfaceNumber
};
// Settings of controlled transfered out from the host.
const controlTransferOutSettings = {
requestType: "class",
recipient: "interface",
request: controlTransferSetReport,
value: controlTransferOutReport,
index: interfaceNumber
};
// This is the number of bytes in the serial buffer.
const serialBufferSize = 64;
// I'm not sure if this acknownledgement or command to write serial message.
// This is used in controlTransferOut. Needs to be a BufferArray.
const serialMsg = 0x83;
const cmdTypeArray = new Uint8Array(1);
cmdTypeArray.set([serialMsg]);
// This is the delay between listen calls,
// so that listen does not still all the processor cycles.
const listenDelay = 300;
/**
* This function is the serial read loop:
* 1. Make a controlTransferOut polling the device on the USB
* 2. Make a controlTransferIn reading the device buffer
* 3. Check that the data from the buffer is from the correct command
* 4. Get the data length
* 5. Send the parser the data if there is data
* 6. Delay the next call to listen.
* Note that the function recursive.
**/
const listen = async () => {
// Send "write" message to the device
const resultOut = await device.controlTransferOut(controlTransferOutSettings, cmdTypeArray);
// console.log(resultOut);
if(resultOut.status != "ok"){
console.log("controlTransferOut failed!");
setTimeout(listen, listenDelay);
}
// Read the serial buffer on the device
const resultIn = await device.controlTransferIn(controlTransferInSettings, serialBufferSize);
// console.log(resultIn);
if(resultIn.status != "ok") console.log("controlTransferIn failed.");
else{
const resultArray = new Uint8Array(resultIn.data.buffer); // This is the serial buffer.
// console.log(resultArray);
const len = resultArray[1]; // This is the length of data. It is sometimes zero and sometimes 62.
// console.log(len);
const opt = resultArray[0]; // This the command that the result is responding to.
if (opt == serialMsg && len > 0){ // Check result is responding to correct command.
const data = resultArray.slice(2, len+2); // The rest of the arrray is the data.
parser.parse(data); // parse the data.
}
} // else
setTimeout(listen, listenDelay);
}; // end listten function
// Shut downs the device.
disconnectButton.onclick = async () => {
await device.close();
// Disconnect styling and text
connectedText.style.display = "block";
connectedText.innerText = "Micro:bit not connected."
usbOutput.style.display = "none";
connectButton.style.display = "initial";
disconnectButton.style.display = "none";
};
Copy and paste the above code into editor and save or download the files in zip file at:
Run and Visit the Webpage
Start the HTTP server
We will use python to make the http server. If you do not already have python on your development machine, download it at: https://www.python.org/
Open a terminal in the project directory and check what python version you have by entering the terminal:
>python --version
If you have python 3 enter:
>python -m http.server
If you have python 2 enter:
>python -m SimpleHTTPServer
Be sure to leave the terminal open. The HTTPServer is ready for requests and serve the webpage. To shutdown the server enter control-c in the terminal. Be sure you do this before you close the terminal otherwise the server will keep running on port 8000.
Visit the Webpage
Connect the micro:bit to the USB port of your development machine.
Open a chrome browser and navigate to
localhost:8000
The HTTP server should display the webpage. Click the “connect” button and the browser will open the modal requesting that you select a device. The name of the device in the modal changes periodically from “DAP…” to “BBS micro:bit…”, I believe it depends on when you flashed the device at MakeCode. If you have only one micro:bit connected to the USB, you should see only one device. So just select it and click the “connect” button.
The webpage should declare that it connected to the micro:bit and display the random sequence. Try clicking the micro:bit buttons and watch the webpage.
Use Google Developer Tools
We will use google developer tools in the chrome browser to inspect the page and JS code. If you have not used developer tools extensively, read these tutorials about how to use the tool.
Get Started with Debugging JavaScript in Chrome DevTools by Kayce Basques: https://developers.google.com/web/tools/chrome-devtools/javascript/
How To Pause Your Code With Breakpoints In Chrome DevTools by Kayce Basques: https://developers.google.com/web/tools/chrome-devtools/javascript/breakpoints
JavaScript Debugging Reference by Kayce Basques: https://developers.google.com/web/tools/chrome-devtools/javascript/reference
Sources Panel Overview by Kayce Basques: https://developers.google.com/web/tools/chrome-devtools/sources
Open the developer tools by right clicking the page and selecting “inspect” or click on the three vertical dots at the upper right of the page, select “more tools” and then “developer tools”.
Open the tool panel to the source panel. You should be able to view the console and see the logs from the webpage. Scroll to the top of the console to where you should see the entry USBDevice. This is the log of the device after the return from requestDevice call. Open the USBDevice by clicking on the horizontally triangle. You should see the device descriptor parsed into the USBDevice.
Inspect the configurations by opening “configurations: Array(1)”. It is an array of USBConfigurations. In the case of the micro:bit, there is only one configuration with configurationValue of 1.
Inspect the USBConfiguration by opening “0: USBConfiguration”. It has 5 interfaces.
Inspect the interfaces by opening “interfaces: (5) …”. The entries are USBInterfaces objects. Note that only one of the USBInterfaces has been claimed.
Inspect each of the USBInterfaces. You should note the interfaceNumber and the alternates. The endpoints are found in the alternates. Open the endpoints array and you will see the description of the endpoint. It gives the direction, endpointNumber, packetSize and type.
In fifth the USBInterface, interfaceNumber = 4, you should notice that the endpoints array is empty. This is a hint that communication to this interface requires control transfers because control transfer is the only transfer call that does not use an endpoint argument.
When I developed the index.js code, I made the inspections above of part of what I did to learn how to code with WebUSB. In addition, I ran MakeCode and used Developer Tools to inspect the code, and track the code using breakpoints to inspect the variables. By the way most of the activities on MakeCode occurs in pxtapp.js (which contains the code for interfacing with the USB) and editor.js (which contain the code for interacting with the editor on MakeCode). The code is well written, most of it is not minified, and worth the inspection. Inspecting USBDevice and pxtapp.jss on MakeCode is how I determined the interface to claimed and the constants for setting up the communication.
Study index.js
Open index.js in your favorite editor. I use Atom. You could use developer tools in the browser, but then syntax will not be highlightinged.
File Layout
The JS, index.js is organized by:
- At the top of the file are constants that point to DOM elements on the webpage and initial text for a DOM element.
- Next is the BufferParser class. An instance of the BufferParser is what passers the serial output. The BufferParser is located near the top of the file because unlike variables and functions declarations, JS does not hoist class declarations. So, the class must be defined before its use.
- After the BufferParser class declaration, the parser function is declared and used to instantiate the parser.
- Then follows the connectButton.onclick event handler. The handler is an anomalous function which opens the selected device, selects the configuration and claims the interface.
- After the eventhandler for the setup, the JS code defines constants used for communication.
- Then the listen function is defined. It is a recursive function that defines the loop that reads the serial buffer.
- Finally the disconnectButton.onclick event handler is defined.
We’ll study the interesting parts of the code in detail.
Setting Up USBDevice
In your editor, navigate to connectButton.onclick event handler:
connectButton.onclick = async () => {
device = await navigator.usb.requestDevice({
filters: [{ vendorId: microbitId }]
});
...
};
Note that the event handler declaration uses a fat arrow syntax for anomalous function. Read about the fat arrow syntax at
Arrow function expressions in MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
In also defines the functions to be asynchronous by the keyword “async”. All the calls to the USBDevice are asynchronous. Modern JavaScript uses promises to defer the results from the asynchronous rather then callback functions. Read about JavaScript promises in
JavaScript Promises: an Introduction by Jake Archibald: https://developers.google.com/web/fundamentals/primers/promises
By the way, Archibald is one my favorite blogger about JavaScript. I highly recommend reading his bloggs. The “promise – then” syntax is only a small improvement to the callback syntax. The “async – await” syntax is a bigger improvement. Read about async functions in
Async functions – making promises friendly by Jake Archibald: https://developers.google.com/web/fundamentals/primers/async-functions
Declaring the event handler to be “async” means that the function body can use “await” to replace the “then” callbacks. The code is much cleaner. We need each call to the device to follow each other sequentially, so we can just list the calls sequentially. At each await, the rest of the function body will not be processed until the promise is fulfilled.
At the end of the connect onclick event handler, the serial read loop is initiated by calling the listen function.
Communicating with USBDevice
I’ll not say much about the constants used in communication except that communication is by bytes, so a messages to the device must be in a Uint8Aarry.
const serialMsg = 0x83;
const cmdTypeArray = new Uint8Array(1);
cmdTypeArray.set([serialMsg]);
Read about Uint8Array at:
Uint8Array in MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
Uint8Array has a buffer and the Uint8Array buffer size must be declared at instantiation. The Uint8Array can hold only a fixed amount of data.
Navigate to the beginning of the listen function:
const listen = async () => {
// Send "write" message to the device
const resultOut = await device.controlTransferOut(controlTransferOutSettings, cmdTypeArray);
// console.log(resultOut);
if(resultOut.status != "ok"){
console.log("controlTransferOut failed!");
setTimeout(listen, listenDelay);
}
…
};
The listen function first sends a command to the device over the serial bus and then reads the serial buffer on the device. This is called serial polling communication.
The listen function is asynchronous. In the controlTransferOut call, we see how to use the await syntax to get a result from an asynchronous call.
Scroll down to the controlTransferIn call:
The result of a successful control transfer is a Uint8Array. The first element of the array is the command sent to the device. The second element of the array is the length of the valid data on the buffer. I learned the hard way that the length of the array should be checked before using the data on the buffer. The code uses Array.slice to extract the valid portion of the buffer. Finally, we call the parser to parse the data.
Before we study the parser, notice that the serial read is continued by
setTimeout(listen, listenDelay);
Read about setTimeout in
WindowOrWorkerGlobalScope.setTimeout() in MDN: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout
We should delay the call to listen because otherwise the serial read loop will steal all the processor cycles. The micro:bit only writes to the serial buffer every second, so inspecting the serial buffer every 300 ms is more than enough. In fact, even at this rate only one out of 3 or 4 transfers have valid data.
Parsing the Data
The BufferParser uses an array for a buffer. Although we are using serial polling to communicate with the device, we need to use a buffer because writing to the buffer is not packaged. The data sent is sent serially as a sequence of key-value pairs separated by a colon:
key: value
At any given buffer read, there can be, one, two, none, or even part of a key value pair. We need to collect the output from the serial reads into a buffer and attempt to parse buffer periodically for a key-value pair.
I choose to use an array for the buffer because the JavaScript array does not have fixed length, so I did not have to make the hard decision of the buffer size.
Besides the colon (COLON) used to separate the key-value pair. The micro:bit adds a carriage return (CR) followed by a line feed (LF) to the end of each serial writes. I am not sure why, but the micro:bit adds spaces after the key-value pairs.
We will use the parsing buffer by filling the end of the array, from the right side of the array, and try parsing the buffer from the beginning of the array, from the left side of the array. We’ll parse the buffer until there is not a complete key-value pair in the parsing buffer.
The code makes extensive use of JS Array methods to parse the buffer. Documentation of the Array is at
Array in MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array
Let us try to draw a picture for parsing buffer (from now on I’ll just call it the buffer) at any given time. Imagine there is a complete key-value pair.
Assumed state:
| ?? | ?? | k | e | y | : | v | a | l | u | e | | | | | CR | LF | ?? | ?? | …
where:
- | divides the buffer array into elements.
- ?? means we are not sure what is at the array element.
- The array elements contain the sequence k, e, and y is the key.
- : is the COLON that divides the key-value pair.
- | | is an array element with a space.
- CR is the carriage return.
- LF is the linefeed.
I set as a goal to get the buffer into the state depicted below as soon as possible.
Goal State:
| LF | k | e | y | : | v | a | l | u | e | | | | | CR | ?? | ?? | ?? | …
But we cannot assume that we are ever in the goal state.
Reference the code in your editor as we step through the code. The steps of parsing are:
Step 1) Find the index of the first occurence of CR and COLON.
From now on I will use CR and COLON to mean either the symbol or the index. The code uses crIndex and colonIndex.
If LF and CR can not be found then LF = -1 and/or CR = -1.
Step 2) The code then checks the existence of CR and COLON compares the location of CR and COLON.
The possible states for COLON and CF are (1) LF = -1 and/or CR = -1, (2) COLON > CR, (3) COLON < CR.
- If either CR or COLON are not found in the buffer then we know that there is not a complete key-value pair in the buffer, so we break from the while loop. This is the exit from the while loop.
- If COLON > CR, then something is wrong to the left of the CR. We can never parse it because the COLON separating the key-value pair must be to the left of the CR. The COLON might be separator for the next key-value pair. So we throw away the CR and everything to the left and continue to the top of the while loop and try parsing again.
- In the else, we know that COLON < CR, so there is a possibility that there is a key value pair that we can parse.
Step 3) The code finds the index of the first occurence LF.
Remember that COLON < CR.
Step 4) The code then check the existence of LF and compares LF with COLON and CR.
The possible states for LF are (1) LF = -1, (2) COLON < CR < LF, (3) COLON < LF < CR, (4) 0 < LF < COLON < CR, (5) Goal state: LF = 0 < COLON < CR.
- LF = -1, meaning it does not exist in the buffer. We assume that this is OK and because of the syntax of Array.splice we can leave LF as -1.
- LF could be to the right of CR, meaning LF > CR. This is OK. The LF is probably the LF that follows the CR, so we set LF to -1.
- LF could be located between COLON and CR, meaning COLON < LF < CR. This should never happen. If it does it means everything to the left of CR is corrupted. We can not parse it, so we throw the CR and everything to the left of CR away and continue to the top of the while loop and try parsing again.
- The last condition is that 0 < LF < COLON. We can parse, but we will throw away everything to the left of LF so we write a log message saying so for debugging purposes.
- LF is at 0, the goal state. The code does not explicitly check for this state.
Step 5) At this point, we are safe to parse the key and value using Array.slice.
Step 6) Throw away the parsed section. That is the CR and and everything to the left. The buffer should be in the goal state or empty.
Step 7) Finally we pass the key and value to the handler.
Step 9) At the end of the while loop, we go to the top of the loop and try to parse again.
The code checks all the possible conditions for COLON, CR, and LF. Funny thing is that during normal code running, the only condition encountered except for the goal state is LF > CR. This occurs only for the first call to the BufferParser which implies that buffer is in the goal state after the first iteration. To get to the other states, I had to multiple times pause and continue the JavaScript.
The final word about the BufferParser is that it has a handler for the parsed key and value that can be overwritten by the constructor when BufferParser is instantiated. Note that the handler is called with two separate arrays, one for the keys and the other for the value. You can assume that they are byte arrays, so the first step is to get strings by calling String.fromCharCode
on the spread arrays and then trim the spaces.
Note that a switch statement is used on the key string to handle the different keys.
Conclusion
You may use the code index.js to implement your project, but I recommend putting the USB setup and communication in its own module.
JavaScript modules in V8: https://v8.dev/features/modules
You could also make the USB setup and communication into two promises, one for setup and the other for communication. To make promises, see
JavaScript Promises: an Introduction by Jake Archibald: https://developers.google.com/web/fundamentals/primers/promises
Async functions – making promises friendly by Jake Archibald: https://developers.google.com/web/fundamentals/primers/async-functions
The listen function would not be in a promise, rather the body of the function and the two controlTransfers would be turned into one promise. The listen function would still use the delay between pairs of controlTransfer calls.
You’ll also need an https server to deploy to. You client will provide you GitHub Page to push to.
Resources
Tutorials
WebUSB by example by Gergana Young: https://medium.com/@gerybbg/webusb-by-example-b4358e6a133c
Access USB Devices on the Web by François Beaufort: https://developers.google.com/web/updates/2016/03/access-usb-devices-on-the-web
Explanations
USB: a web developer perspective by Gergana Young: https://medium.com/@gerybbg/usb-a-web-developer-perspective-cbee13883c89
USB in a NutShell by Beyond Logic: https://www.beyondlogic.org/usbnutshell/usb1.shtml
Resources
MDN Web Docs: https://developer.mozilla.org/en-US/docs/Web/API/USB https://developer.mozilla.org/en-US/docs/Web/API/USBDevice
WebUSB API by a W3C Community Group: https://wicg.github.io/webusb/
Defined Class Codes: https://www.usb.org/defined-class-codes
ASCII Table by Richard E. Pattis at CMU: https://www.cs.cmu.edu/~pattis/15-1XX/common/handouts/ascii.html
Micro:Bit Resources
Micro:bit Doc: https://makecode.microbit.org/docs
MakeCode: https://makecode.microbit.org/
Quick Start: https://microbit.org/guide/quick/
WebUSB – Flashing Micro:bit via WebUSB: https://makecode.microbit.org/device/usb/webusb
Microbit USB VID/PID Numbers: https://support.microbit.org/support/solutions/articles/19000035697-what-are-the-usb-vid-pid-numbers-for-micro-bit
Outputting serial data from the micro:bit to a computer: https://support.microbit.org/support/solutions/articles/19000022103-outputing-serial-data-from-the-micro-bit-to-a-computer
HTTP Server
Simple HTTP server in Python by Anurag Kumar: https://www.hackerearth.com/practice/notes/simple-http-server-in-python/
HTTPS server
We do not need to do this when using localhost.
Creating an HTTPS server in Python by Martin Pitt: https://piware.de/2011/01/creating-an-https-server-in-python/
simple-https-server.py: https://piware.de/2011/01/creating-an-https-server-in-python/
Generating valid self signed certificates for localhost development by Quinton Wall: https://developer.salesforce.com/blogs/developer-relations/2011/05/generating-valid-self-signed-certificates.html
How to create a self-signed certificate with OpenSSL in stackoverflow: https://stackoverflow.com/questions/10175812/how-to-create-a-self-signed-certificate-with-openssl/27931596#27931596 See fourth answer. I used the Ubuntu terminal for Windows 10.
OpenSSL Man Page: https://www.openssl.org/docs/manmaster/man1/openssl-x509.html
JavaScript
Arrow function expressions in MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
JavaScript Promises: an Introduction by Jake Archibald: https://developers.google.com/web/fundamentals/primers/promises
Async functions – making promises friendly by Jake Archibald: https://developers.google.com/web/fundamentals/primers/async-functions
Classes in MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
Array in MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array
Uint8Array in MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
WindowOrWorkerGlobalScope.setTimeout() in MDN: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout
JavaScript modules in V8: https://v8.dev/features/modules
Tools for Web Developers
Get Started with Debugging JavaScript in Chrome DevTools by Kayce Basques: https://developers.google.com/web/tools/chrome-devtools/javascript/
How To Pause Your Code With Breakpoints In Chrome DevTools by Kayce Basques: https://developers.google.com/web/tools/chrome-devtools/javascript/breakpoints
JavaScript Debugging Reference by Kayce Basques: https://developers.google.com/web/tools/chrome-devtools/javascript/reference
Sources Panel Overview by Kayce Basques: https://developers.google.com/web/tools/chrome-devtools/sources