Read the article...
MakeCode for BBC micro:bit is an awesome creation that's way ahead of its time (7 years ago!)
-
TypeScript Compiler in the Web Browser (in JavaScript!)
-
Bespoke Arm Assembler that runs in the Web Browser (also JavaScript!)
-
Bespoke Embedded OS for BBC micro:bit (CODAL / Mbed OS)
-
UF2 Bootloader with flashing over WebUSB
-
micro:bit Simulator in JavaScript
-
All this for an (underpowered) BBC micro:bit with Nordic nRF51 (Arm Cortex-M0, 256 KB Flash, 16 KB RAM!)
Today 7 years later: How would we redo all this? With a bunch of Open Source Packages?
-
Hardware Device: Ox64 BL808 64-bit RISC-V SBC (64 MB RAM, Unlimited microSD Storage, only $8!)
-
Embedded OS: Apache NuttX RTOS
-
JavaScript Engine: QuickJS for NuttX
-
Web Emulator: TinyEMU WebAssembly for NuttX
-
C Compiler + Assembler: TCC WebAssembly for NuttX (but we probably won't need this since we have JavaScript on NuttX)
-
Device Control: Web Serial API for controlling Ox64 over UART
Which will look like this...
Read the article...
MakeCode was created with Blockly, we'll stick with Blockly.
Based on the Blockly Instructions...
npx @blockly/create-package app nuttx-blockly --typescript
npm run build
Try the Blockly Demo: https://lupyuen.github.io/nuttx-blockly/
Read the article...
To send a command to NuttX Emulator: jslinux.js
let send_str = "";
function send_command(cmd) {
if (cmd !== null) { send_str = cmd; }
if (send_str.length == 0) { return; }
console_write1(send_str.charCodeAt(0));
send_str = send_str.substring(1);
window.setTimeout(()=>{ send_command(null); }, 10);
}
const cmd = [
`qjs`,
`function main() { console.log(123); }`,
`main()`,
``
].join("\r");
window.setTimeout(()=>{ send_command(cmd); }, 10000);
Which will start QuickJS and run a JavaScript Function:
https://lupyuen.github.io/nuttx-tinyemu/blockly/
NuttShell (NSH) NuttX-12.4.0-RC0
nsh> qjs
QuickJS - Type "\h" for help
qjs > function main() { console.log(123); }
undefined
qjs > main()
123
undefined
qjs >
Read the article...
Based on the Blockly Developer Tools, we add the POSIX Blocks for open()
, close()
, ioctl()
and sleep()
...
Then we build and deploy our Blockly Website...
npm run build && rm -r docs && mv dist docs
Let's test it...
Read the article...
Click this link: https://lupyuen.github.io/nuttx-blockly/
Then Drag-n-Drop this NuttX App...
var ULEDIOC_SETALL, fd, ret;
ULEDIOC_SETALL = 7427;
fd = os.open('/dev/userleds');
for (var count = 0; count < 20; count++) {
ret = os.ioctl(fd, ULEDIOC_SETALL, 1);
os.sleep(20000);
ret = os.ioctl(fd, ULEDIOC_SETALL, 0);
os.sleep(20000);
}
os.close(fd);
Click the "Run on Ox64 Emulator" button.
Our Drag-n-Drop NuttX App runs automatically in the Emulator yay!
NuttShell (NSH) NuttX-12.4.0-RC0
nsh> qjs
QuickJS - Type "\h" for help
qjs > var ULEDIOC_SETALL, fd, ret;
undefined
qjs >
qjs >
qjs > ULEDIOC_SETALL = 7427;
7427
qjs > fd = os.open('/dev/userleds');
3
qjs > for (var count = 0; count < 20; count++) {
{ ... ret = os.ioctl(fd, ULEDIOC_SETALL, 1);
{ ... os.sleep(20000);
{ ... ret = os.ioctl(fd, ULEDIOC_SETALL, 0);
{ ... os.sleep(20000);
{ ... }
bl808_gpiowrite: regaddr=0x20000938, set=0x1000000
bl808_gpiowrite: regaddr=0x20000938, clear=0x1000000
How did Blockly pass the Generated JavaScript to NuttX Emulator?
When we click the "Run on Emulator" button, our Blockly Website saves the Generated JavaScript to the Web Browser Local Storage: index.ts
function runEmulator() {
// Save the Generated JavaScript Code to LocalStorage
const code = javascriptGenerator.workspaceToCode(ws);
window.localStorage.setItem("runCode", code);
// Set the Timestamp for Optimistic Locking (later)
window.localStorage.setItem("runTimestamp", Date.now() + "");
// Open the NuttX Emulator. Reuse the same tab.
window.open("https://lupyuen.github.io/nuttx-tinyemu/blockly/", "Emulator");
}
In the NuttX Emulator: We read the Generated JavaScript from the Web Browser Local Storage. And feed it (character by character) to the NuttX Console: jslinux.js
// QuickJS Command to be sent
const cmd = [
`qjs`,
window.localStorage.getItem("runCode"),
``
].join("\r");
// Wait for NuttX to boot in 5 seconds. Then send the QuickJS Command.
window.setTimeout(()=>{ send_command(cmd); }, 5000);
// Send a Command to NuttX Console, character by character
let send_str = "";
function send_command(cmd) {
if (cmd !== null) { send_str = cmd; }
if (send_str.length == 0) { return; }
console_write1(send_str.charCodeAt(0));
send_str = send_str.substring(1);
window.setTimeout(()=>{ send_command(null); }, 10);
}
Read the article...
From NuttX Emulator to a Real NuttX Device! Click this link: https://lupyuen.github.io/nuttx-blockly/
Then Drag-n-Drop the same NuttX App (see the previous section)...
var ULEDIOC_SETALL, fd, ret;
ULEDIOC_SETALL = 7427;
fd = os.open('/dev/userleds');
for (var count = 0; count < 20; count++) {
ret = os.ioctl(fd, ULEDIOC_SETALL, 1);
os.sleep(20000);
ret = os.ioctl(fd, ULEDIOC_SETALL, 0);
os.sleep(20000);
}
os.close(fd);
-
Click the "Run on Ox64 Device" button
-
Click the "Connect" button to connect to our Ox64 BL808 SBC
-
Power on our Ox64 SBC. The Web App waits for the "nsh>" prompt.
-
Then our Drag-n-Drop NuttX App runs automatically on a Real Ox64 BL808 SBC yay!
NuttShell (NSH) NuttX-12.4.0-RC0
nsh> qjs
QuickJS - Type "\h" for help
qjs > var ULEDIOC_SETALL, fd, ret;
undefined
qjs >
qjs >
qjs > ULEDIOC_SETALL = 7427;
7427
qjs > fd = os.open('/dev/userleds');
3
qjs > for (var count = 0; count < 20; count++) {
{ ... ret = os.ioctl(fd, ULEDIOC_SETALL, 1);
{ ... os.sleep(20000);
{ ... ret = os.ioctl(fd, ULEDIOC_SETALL, 0);
{ ... os.sleep(20000);
{ ... }
bl808_gpiowrite: regaddr=0x20000938, set=0x1000000
bl808_gpiowrite: regaddr=0x20000938, clear=0x1000000
How did Blockly pass the Generated JavaScript to Ox64 SBC?
When we click the "Run on Device" button, our Blockly Website saves the Generated JavaScript to the Web Browser Local Storage: index.ts
// Run on Ox64 Device
function runDevice() {
// Save the Generated JavaScript Code to LocalStorage
const code = javascriptGenerator.workspaceToCode(ws);
window.localStorage.setItem("runCode", code);
// Set the Timestamp for Optimistic Locking (later)
window.localStorage.setItem("runTimestamp", Date.now() + "");
// Open the WebSerial Monitor. Reuse the same tab.
window.open("https://lupyuen.github.io/nuttx-tinyemu/webserial/", "Device");
}
In the WebSerial Monitor: We read the Generated JavaScript from the Web Browser Local Storage. And feed it (character by character) to the NuttX Console: webserial.js
// Control Ox64 over UART
// https://developer.chrome.com/docs/capabilities/serial
async function control_device() {
if (!navigator.serial) { const err = "Web Serial API only works with https://... and file://...!"; alert(err); throw new Error(err); }
// Prompt user to select any serial port.
const port = await navigator.serial.requestPort();
term.write("Power on our NuttX Device and we'll wait for \"nsh>\"\r\n");
// Get all serial ports the user has previously granted the website access to.
// const ports = await navigator.serial.getPorts();
// Wait for the serial port to open.
// TODO: Ox64 only connects at 2 Mbps, change this for other devices
await port.open({ baudRate: 2000000 });
// Prepare to write to serial port
const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
const writer = textEncoder.writable.getWriter();
// Read from the serial port
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable.getReader();
// Wait for "nsh>"
let nshSpotted = false;
let termBuffer = "";
// Listen to data coming from the serial device.
while (true) {
const { value, done } = await reader.read();
if (done) {
// Allow the serial port to be closed later.
reader.releaseLock();
break;
}
// Print to the Terminal
term.write(value);
// console.log(value);
// Wait for "nsh>"
if (nshSpotted) { continue; }
termBuffer += value;
if (termBuffer.indexOf("nsh>") < 0) { continue; }
// NSH Spotted!
console.log("NSH Spotted!");
nshSpotted = true;
// Send a command to serial port. Newlines become Carriage Returns.
const code = window.localStorage.getItem("runCode")
.split('\n').join('\r');
const cmd = [
`qjs`,
code,
``
].join("\r");
window.setTimeout(()=>{ send_command(writer, cmd); }, 1000);
}
}
// Send a Command to serial port, character by character
let send_str = "";
async function send_command(writer, cmd) {
if (cmd !== null) { send_str = cmd; }
if (send_str.length == 0) { return; }
// Get the next character
const ch = send_str.substring(0, 1);
send_str = send_str.substring(1);
// Slow down at the end of each line
const timeout = (ch === "\r")
? 3000
: 10;
// Send the character
await writer.write(ch);
window.setTimeout(()=>{ send_command(writer, null); }, timeout);
}
More about the Web Serial API...
Read the article...
Let's connect to Ox64 BL808 SBC in our Web Browser via the Web Serial API...
-
Getting started with the Web Serial API
(Very similar to what we're doing)
Beware, Web Serial API is only available...
-
Over HTTPS: https://...
-
Or Local Filesystem: file://...
-
It won't work over HTTP! http://...
We create a button: index.html
<button id="connect" onclick="control_device();">
Connect
</button>
Which connects to Ox64 over UART: jslinux.js
// Control Ox64 over UART
// https://developer.chrome.com/docs/capabilities/serial
async function control_device() {
if (!navigator.serial) { const err = "Web Serial API only works with https://... and file://...!"; alert(err); throw new Error(err); }
// Prompt user to select any serial port.
const port = await navigator.serial.requestPort();
// Get all serial ports the user has previously granted the website access to.
// const ports = await navigator.serial.getPorts();
// Wait for the serial port to open.
// TODO: Ox64 only connects at 2 Mbps, change this for other devices
await port.open({ baudRate: 2000000 });
// Read from the serial port
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable.getReader();
// Listen to data coming from the serial device.
while (true) {
const { value, done } = await reader.read();
if (done) {
// Allow the serial port to be closed later.
reader.releaseLock();
break;
}
// value is a string.
console.log(value);
}
And Ox64 NuttX appears in our JavaScript Console yay!
Starting kernel ...
ABC
bl808_gpiowrite: regaddr=0x20000938, clear=0x1000000
bl808_gpiowrite: regaddr=0x20000938, set=0x1000000
bl808_gpiowrite: regaddr=0x20000938, clear=0x1000000
NuttShell (NSH) NuttX-12.4.0-RC0
nsh>
Read the article...
This is how we send a command to Ox64 BL808 SBC via Web Serial API: jslinux.js
// Wait for the serial port to open.
// TODO: Ox64 only connects at 2 Mbps, change this for other devices
await port.open({ baudRate: 2000000 });
// Send a command to serial port
const cmd = [
`qjs`,
`function main() { console.log(123); }`,
`main()`,
``
].join("\r");
const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
const writer = textEncoder.writable.getWriter();
await writer.write(cmd);
// Read from the serial port
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable.getReader();
// Listen to data coming from the serial device.
...
And it works! Says the JavaScript Console...
function main() { console.log(123); }
main()
123
undefined
qjs >
Read the article...
This is how we load the Blocks for a Blockly App: index.ts
// Select a Demo
function selectDemo(ev: Event) {
const storageKey = 'mainWorkspace';
const target = ev?.target as HTMLSelectElement;
const value = target.value;
// Set the Blocks in Local Storage
switch (value) {
case "LED Blinky":
window.localStorage?.setItem(storageKey, '{"blocks":{"languageVersion":0,"blocks":[{"type":"variables_set","id":"Nx6o0xVxp@qzI_(vRd.7","x":60,"y":33,"fields":{"VAR":{"id":":,DB,f}1q3KOBim#j66["}},"inputs":{"VALUE":{"block":{"type":"math_number","id":"enmYd`#z_G1k5Pvv*x(G","fields":{"NUM":7427}}}},"next":{"block":{"type":"variables_set","id":"f#C+(eT=naKZzr%/;A.P","fields":{"VAR":{"id":"A/TX@37C_h*^vbRp@1fz"}},"inputs":{"VALUE":{"block":{"type":"posix_open","id":"^$p+x^F[mQ;grqANDtO}","inputs":{"FILENAME":{"shadow":{"type":"text","id":"nz;|U#KPVW$$c0?W0ROv","fields":{"TEXT":"/dev/userleds"}}}}}}},"next":{"block":{"type":"controls_repeat_ext","id":"0{4pA@{^=ks|iVF.|]i#","inputs":{"TIMES":{"shadow":{"type":"math_number","id":"=o3{$E2c=BpwD0#MR3^x","fields":{"NUM":20}}},"DO":{"block":{"type":"variables_set","id":"l;AmIPhJARU{C)0kNq6`","fields":{"VAR":{"id":"xH3`F~]tadlX:/zKQ!Xx"}},"inputs":{"VALUE":{"block":{"type":"posix_ioctl","id":"0i!pbWJ(~f~)b^@jt!nP","inputs":{"FD":{"block":{"type":"variables_get","id":"QMGa_}UmC$b[5/Bh^f${","fields":{"VAR":{"id":"A/TX@37C_h*^vbRp@1fz"}}}},"REQ":{"block":{"type":"variables_get","id":"dZ5%B_rcbVb_o=v;gze-","fields":{"VAR":{"id":":,DB,f}1q3KOBim#j66["}}}},"ARG":{"block":{"type":"math_number","id":"9UA!sDxmf/=fYfxC6Yqa","fields":{"NUM":1}}}}}}},"next":{"block":{"type":"posix_sleep","id":"ruh/q4F7dW*CQ,5J]E%w","inputs":{"MS":{"block":{"type":"math_number","id":"9~q0@ABEg4VXP:1HN-$1","fields":{"NUM":5000}}}},"next":{"block":{"type":"variables_set","id":"e;BNsjvbN}9vTTc[O#bY","fields":{"VAR":{"id":"xH3`F~]tadlX:/zKQ!Xx"}},"inputs":{"VALUE":{"block":{"type":"posix_ioctl","id":"-G5x~Y4iAyVUAWuwNh#H","inputs":{"FD":{"block":{"type":"variables_get","id":"vtt5Gid0B|iK![$4Ct*D","fields":{"VAR":{"id":"A/TX@37C_h*^vbRp@1fz"}}}},"REQ":{"block":{"type":"variables_get","id":"pd~f}Oqz2(`o3Oz;8ax`","fields":{"VAR":{"id":":,DB,f}1q3KOBim#j66["}}}},"ARG":{"block":{"type":"math_number","id":"OS(uQV)!%iqZ=N}s1H(L","fields":{"NUM":0}}}}}}},"next":{"block":{"type":"posix_sleep","id":"{X9leD=Rgr4=o5E2(#Z,","inputs":{"MS":{"block":{"type":"math_number","id":"eEq(yXcGPbVtZT|CunT0","fields":{"NUM":5000}}}}}}}}}}}}},"next":{"block":{"type":"posix_close","id":"+%kD6{Xa@#BOx}a^Jbup","inputs":{"FD":{"block":{"type":"variables_get","id":"nu)^gdR-9QV71GSI7#(l","fields":{"VAR":{"id":"A/TX@37C_h*^vbRp@1fz"}}}}}}}}}}}}]},"variables":[{"name":"fd","id":"A/TX@37C_h*^vbRp@1fz"},{"name":"ULEDIOC_SETALL","id":":,DB,f}1q3KOBim#j66["},{"name":"ret","id":"xH3`F~]tadlX:/zKQ!Xx"}]}');
break;
default:
break;
}
// Refresh the Blocks
if (ws) {
load(ws);
runCode();
}
}
To see the Blocks for a Blockly App: Browse to https://lupyuen.github.io/nuttx-blockly/
Select "Menu > More Tools > Developer Tools > Application > Local Storage > lupyuen.github.io > mainWorkspace"
Or do this from the JavaScript Console...
// Display the Blocks in JSON Format
localStorage.getItem("mainWorkspace");
// Set the Blocks in JSON Format.
// Change `...` to the JSON of the Blocks to be loaded.
localStorage.setItem("mainWorkspace", `...`);