From 0fd3b74d1fa333f39fa251d2933526fc9c639421 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Thu, 8 Sep 2022 16:45:59 +0100 Subject: [PATCH 1/5] Stop the WASM when we panic. Fixes panic HAL calls so the device is no longer operable afterwards. - Use the same logic for a normal stop/start. - Cache the WASM to avoid downloading and compiling it for each module instantiation. - Prefer using the _foo functions on the module vs cwrap. - Incidental fix to the radio message handling in demo.html --- Makefile | 2 +- README.md | 13 +++ src/Makefile | 17 ++-- src/board/accelerometer.ts | 10 +- src/board/audio/index.ts | 5 +- src/board/buttons.ts | 2 - src/board/compass.ts | 2 - src/board/conversions.ts | 25 ++--- src/board/display.ts | 2 - src/board/index.ts | 191 +++++++++++++++++++++++++++++-------- src/board/microphone.ts | 13 +-- src/board/pins.ts | 2 - src/board/radio.ts | 2 - src/board/wasm.ts | 102 ++++++++------------ src/demo.html | 17 +--- src/jshal.js | 129 +++++++++++++------------ src/main.c | 5 +- src/main.js | 31 ------ src/pre.ts | 30 ------ src/simulator.html | 3 +- src/simulator.ts | 20 ++++ 21 files changed, 334 insertions(+), 289 deletions(-) delete mode 100644 src/main.js delete mode 100644 src/pre.ts create mode 100644 src/simulator.ts diff --git a/Makefile b/Makefile index a1fcca10..60df0f9f 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ build: dist: build mkdir -p $(BUILD)/build cp -r $(SRC)/*.html $(SRC)/term.js src/examples $(BUILD) - cp $(SRC)/build/micropython.js $(SRC)/build/firmware.wasm $(BUILD)/build/ + cp $(SRC)/build/firmware.js $(SRC)/build/simulator.js $(SRC)/build/firmware.wasm $(BUILD)/build/ watch: dist fswatch -o -e src/build src | while read _; do $(MAKE) dist; done diff --git a/README.md b/README.md index ec1d2765..7048a2ad 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,19 @@ The following sections documents the messages supported via postMessage. Radio output (sent from the user's program) as bytes. If you send string data from the program then it will be prepended with the three bytes 0x01, 0x00, 0x01. + +internal_error + + +```javascript +{ + "kind": "internal_error", + "error": new Error() +} +``` + +A debug message sent for internal (unexpected) errors thrown by the simulator. Suitable for application-level logging. Please raise issues in this project as these indicate a bug in the simulator. + ## Messages supported by the iframe diff --git a/src/Makefile b/src/Makefile index 6e0899fb..2b89ae93 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,4 +1,4 @@ -# Makefile to build micropython.js +# Makefile to build WASM and simulator JS. # Build upon the codal_port code. CODAL_PORT = $(abspath ../lib/micropython-microbit-v2/src/codal_port) @@ -44,9 +44,11 @@ COPT += -O3 -DNDEBUG endif JSFLAGS += -s ASYNCIFY -JSFLAGS += -s EXPORTED_FUNCTIONS="['_mp_js_main','_mp_js_request_stop','_microbit_hal_audio_ready_callback','_microbit_hal_audio_speech_ready_callback','_microbit_hal_gesture_callback','_microbit_hal_level_detector_callback','_microbit_radio_rx_buffer']" +JSFLAGS += -s EXIT_RUNTIME +JSFLAGS += -s MODULARIZE=1 +JSFLAGS += -s EXPORT_NAME=createModule +JSFLAGS += -s EXPORTED_FUNCTIONS="['_mp_js_main','_microbit_hal_audio_ready_callback','_microbit_hal_audio_speech_ready_callback','_microbit_hal_gesture_callback','_microbit_hal_level_detector_callback','_microbit_radio_rx_buffer','_mp_js_force_stop','_mp_js_request_stop']" JSFLAGS += -s EXPORTED_RUNTIME_METHODS="['ccall', 'cwrap']" --js-library jshal.js -JSFLAGS += --pre-js $(BUILD)/pre.js ifdef DEBUG JSFLAGS += -g @@ -136,14 +138,13 @@ $(MBIT_VER_FILE): FORCE $(PYTHON) $(TOP)/py/makeversionhdr.py $(MBIT_VER_FILE).pre $(CAT) $(MBIT_VER_FILE).pre | $(SED) s/MICROPY_/MICROBIT_/ > $(MBIT_VER_FILE) -$(BUILD)/micropython.js: $(OBJ) jshal.js main.js pre-js +$(BUILD)/micropython.js: $(OBJ) jshal.js simulator-js $(ECHO) "LINK $(BUILD)/firmware.js" $(Q)emcc $(LDFLAGS) -o $(BUILD)/firmware.js $(OBJ) $(JSFLAGS) - cat main.js $(BUILD)/firmware.js > $@ -pre-js: - npx esbuild ./pre.ts --bundle --outfile=$(BUILD)/pre.js --loader:.svg=text +simulator-js: + npx esbuild ./simulator.ts --bundle --outfile=$(BUILD)/simulator.js --loader:.svg=text include $(TOP)/py/mkrules.mk -.PHONY: pre-js \ No newline at end of file +.PHONY: simulator-js \ No newline at end of file diff --git a/src/board/accelerometer.ts b/src/board/accelerometer.ts index 9237f584..2773baaa 100644 --- a/src/board/accelerometer.ts +++ b/src/board/accelerometer.ts @@ -48,8 +48,8 @@ export class Accelerometer { setValue(id: StateKeys, value: any) { this.state[id].setValue(value); - if (id === "gesture") { - this.gestureCallback!( + if (id === "gesture" && this.gestureCallback) { + this.gestureCallback( convertAccelerometerStringToNumber(this.state.gesture.value) ); } @@ -71,11 +71,9 @@ export class Accelerometer { }); } - initialize(gestureCallback: GestureCallback) { + initializeCallbacks(gestureCallback: GestureCallback) { this.gestureCallback = gestureCallback; } - dispose() { - this.gestureCallback = undefined; - } + dispose() {} } diff --git a/src/board/audio/index.ts b/src/board/audio/index.ts index 7e62d4b3..2589364e 100644 --- a/src/board/audio/index.ts +++ b/src/board/audio/index.ts @@ -23,7 +23,10 @@ export class Audio { constructor() {} - initialize({ defaultAudioCallback, speechAudioCallback }: AudioOptions) { + initializeCallbacks({ + defaultAudioCallback, + speechAudioCallback, + }: AudioOptions) { this.context = new AudioContext({ // The highest rate is the sound expression synth. sampleRate: 44100, diff --git a/src/board/buttons.ts b/src/board/buttons.ts index 7a62cf1a..21afa48e 100644 --- a/src/board/buttons.ts +++ b/src/board/buttons.ts @@ -115,8 +115,6 @@ export class Button { return result; } - initialize() {} - dispose() { this._presses = 0; } diff --git a/src/board/compass.ts b/src/board/compass.ts index 43acf937..8bfc5ae0 100644 --- a/src/board/compass.ts +++ b/src/board/compass.ts @@ -27,7 +27,5 @@ export class Compass { return Math.sqrt(x * x + y * y + z * z); } - initialize() {} - dispose() {} } diff --git a/src/board/conversions.ts b/src/board/conversions.ts index 95be4b93..392d7808 100644 --- a/src/board/conversions.ts +++ b/src/board/conversions.ts @@ -12,22 +12,10 @@ import { MICROBIT_HAL_ACCELEROMETER_EVT_TILT_LEFT, MICROBIT_HAL_ACCELEROMETER_EVT_TILT_RIGHT, MICROBIT_HAL_ACCELEROMETER_EVT_TILT_UP, - MICROBIT_HAL_MICROPHONE_EVT_THRESHOLD_HIGH, - MICROBIT_HAL_MICROPHONE_EVT_THRESHOLD_LOW, MICROBIT_HAL_MICROPHONE_SET_THRESHOLD_HIGH, MICROBIT_HAL_MICROPHONE_SET_THRESHOLD_LOW, } from "./constants"; -export function convertAudioBuffer(source: number, target: AudioBuffer) { - const channel = target.getChannelData(0); - const heap = window.HEAPU8; - for (let i = 0; i < channel.length; ++i) { - // Convert from uint8 to -1..+1 float. - channel[i] = (heap[source + i] / 255) * 2 - 1; - } - return target; -} - export function convertSoundThresholdNumberToString( value: number ): "low" | "high" { @@ -106,3 +94,16 @@ export function convertAccelerometerNumberToString(value: number): string { throw new Error(`Invalid value ${value}`); } } + +export const convertAudioBuffer = ( + heap: Uint8Array, + source: number, + target: AudioBuffer +) => { + const channel = target.getChannelData(0); + for (let i = 0; i < channel.length; ++i) { + // Convert from uint8 to -1..+1 float. + channel[i] = (heap[source + i] / 255) * 2 - 1; + } + return target; +}; diff --git a/src/board/display.ts b/src/board/display.ts index 68a1f450..7b3e5350 100644 --- a/src/board/display.ts +++ b/src/board/display.ts @@ -73,8 +73,6 @@ export class Display { } } - initialize() {} - dispose() { this.clear(); } diff --git a/src/board/index.ts b/src/board/index.ts index a6934b3c..e9f1d9be 100644 --- a/src/board/index.ts +++ b/src/board/index.ts @@ -9,6 +9,7 @@ import { MICROBIT_HAL_PIN_P1, MICROBIT_HAL_PIN_P2, } from "./constants"; +import * as conversions from "./conversions"; import { DataLogging } from "./data-logging"; import { Display } from "./display"; import { FileSystem } from "./fs"; @@ -16,21 +17,23 @@ import { Microphone } from "./microphone"; import { Pin } from "./pins"; import { Radio } from "./radio"; import { RangeSensor, State } from "./state"; -import { WebAssemblyOperations } from "./wasm"; +import { ModuleWrapper } from "./wasm"; + +export class PanicError extends Error { + constructor(public code: number) { + super("panic"); + } +} const stoppedOpactity = "0.5"; -export function createBoard( - operations: WebAssemblyOperations, - notifications: Notifications, - fs: FileSystem -) { +export function createBoard(notifications: Notifications, fs: FileSystem) { document.body.insertAdjacentHTML("afterbegin", svgText); const svg = document.querySelector("svg"); if (!svg) { throw new Error("No SVG"); } - return new Board(operations, notifications, fs, svg); + return new Board(notifications, fs, svg); } export class Board { @@ -74,12 +77,24 @@ export class Board { return result ?? id; }; + private initialModulePromise: Promise | undefined; + /** + * Defined during start(). + */ + private modulePromise: Promise | undefined; + /** + * Flag to trigger a reset after start finishes. + */ + private resetWhenDone: boolean = false; + constructor( - public operations: WebAssemblyOperations, private notifications: Notifications, private fs: FileSystem, private svg: SVGElement ) { + // We start this loading here so as not to delay loading WASM until start(). + this.initialModulePromise = this.createModule(); + this.display = new Display( Array.from(this.svg.querySelector("#LEDsOn")!.querySelectorAll("use")) ); @@ -143,6 +158,29 @@ export class Board { ); this.updateTranslationsInternal(); + this.notifications.onReady(this.getState()); + } + + private async createModule(): Promise { + const wrapped = await window.createModule({ + board: this, + fs: this.fs, + conversions, + noInitialRun: true, + instantiateWasm, + }); + const module = new ModuleWrapper(wrapped); + this.audio.initializeCallbacks({ + defaultAudioCallback: wrapped._microbit_hal_audio_ready_callback, + speechAudioCallback: wrapped._microbit_hal_audio_speech_ready_callback, + }); + this.accelerometer.initializeCallbacks( + wrapped._microbit_hal_gesture_callback + ); + this.microphone.initializeCallbacks( + wrapped._microbit_hal_audio_speech_ready_callback + ); + return module; } updateTranslations(language: string, translations: Record) { @@ -246,27 +284,6 @@ export class Board { } } - initializedWebAssembly() { - this.operations.initialize(); - this.notifications.onReady(this.getState()); - } - - initialize() { - this.epoch = new Date().getTime(); - this.audio.initialize({ - defaultAudioCallback: this.operations.defaultAudioCallback!, - speechAudioCallback: this.operations.speechAudioCallback!, - }); - this.buttons.forEach((b) => b.initialize()); - this.pins.forEach((p) => p.initialize()); - this.display.initialize(); - this.accelerometer.initialize(this.operations.gestureCallback!); - this.compass.initialize(); - this.microphone.initialize(this.operations.soundLevelCallback!); - this.radio.initialize(); - this.serialInputBuffer.length = 0; - } - ticksMilliseconds() { return new Date().getTime() - this.epoch!; } @@ -299,25 +316,71 @@ export class Board { this.stoppedOverlay.style.display = "flex"; } - private start() { - this.operations.start(); - this.displayRunningState(); + private async start() { + if (this.modulePromise) { + throw new Error("Module already exists!"); + } + // Use the pre-loaded one the first time through. + if (this.initialModulePromise) { + this.modulePromise = this.initialModulePromise; + this.initialModulePromise = undefined; + } else { + this.modulePromise = this.createModule(); + } + + const module = await this.modulePromise; + let panicCode: number | undefined; + try { + this.displayRunningState(); + await module.start(); + } catch (e: any) { + if (e instanceof PanicError) { + panicCode = e.code; + } else { + this.notifications.onInternalError(e); + } + } + try { + module.forceStop(); + } catch (e: any) { + if (e.name !== "ExitStatus") { + console.error(e); + throw new Error("Expected status message"); + } + } + this.modulePromise = undefined; + + if (panicCode !== undefined) { + this.displayPanic(panicCode); + } else { + if (this.resetWhenDone) { + this.resetWhenDone = false; + setTimeout(() => this.start(), 0); + } else { + this.displayStoppedState(); + } + } } - async stop(): Promise { + async stop(reset: boolean = false): Promise { + this.resetWhenDone = reset; if (this.panicTimeout) { clearTimeout(this.panicTimeout); this.panicTimeout = null; this.display.clear(); + this.displayStoppedState(); + } + if (this.modulePromise) { + const module = await this.modulePromise; + module.requestStop(); + this.modulePromise = undefined; + // Ctrl-C, Ctrl-D to interrupt the main loop. + this.writeSerialInput("\x03\x04"); } - const interrupt = () => this.serialInputBuffer.push(3, 4); // Ctrl-C, Ctrl-D. - await this.operations.stop(interrupt); - this.displayStoppedState(); } async reset(): Promise { - await this.stop(); - this.start(); + this.stop(true); } async flash(filesystem: Record): Promise { @@ -331,9 +394,11 @@ export class Board { return this.start(); } - panic(code: number): void { - // We should hang MicroPython here. I think ideally we'd stop it entirely so we do this without any WASM. - // For now we just do the display animation. + throwPanic(code: number): void { + throw new PanicError(code); + } + + displayPanic(code: number): void { const sad = [ [9, 9, 0, 9, 9], [9, 9, 0, 9, 9], @@ -457,7 +522,15 @@ export class Board { } writeSerialOutput(text: string): void { - this.notifications.onSerialOutput(text); + // Avoid the Ctrl-C, Ctrl-D output when we request a stop. + if (this.modulePromise) { + this.notifications.onSerialOutput(text); + } + } + + initialize() { + this.epoch = new Date().getTime(); + this.serialInputBuffer.length = 0; } dispose() { @@ -519,6 +592,10 @@ export class Notifications { this.postMessage("log_delete", {}); }; + onInternalError = (error: any) => { + this.postMessage("internal_error", { error }); + }; + private postMessage(kind: string, data: any) { this.target.postMessage( { @@ -599,3 +676,33 @@ function isFileSystem( ([k, v]) => typeof k === "string" && v instanceof Uint8Array ); } + +const fetchWasm = async () => { + const response = await fetch("./build/firmware.wasm"); + if (!response.ok) { + throw new Error(response.statusText); + } + return response.arrayBuffer(); +}; + +const compileWasm = async () => { + // Can't use streaming in Safari 14 but would be nice to feature detect. + return WebAssembly.compile(new Uint8Array(await fetchWasm())); +}; + +let compiledWasmPromise: Promise = compileWasm(); + +const instantiateWasm = function (imports: any, successCallback: any) { + // No easy way to communicate failure here so hard to add retries. + compiledWasmPromise + .then(async (wasmModule) => { + const instance = await WebAssembly.instantiate(wasmModule, imports); + successCallback(instance); + }) + .catch((e) => { + console.error("Failed to instantiate WASM"); + console.error(e); + }); + // Result via callback. + return {}; +}; diff --git a/src/board/microphone.ts b/src/board/microphone.ts index 11dea934..c3396a4a 100644 --- a/src/board/microphone.ts +++ b/src/board/microphone.ts @@ -50,19 +50,20 @@ export class Microphone { const low = this.soundLevel.lowThreshold!; const high = this.soundLevel.highThreshold!; - if (prev > low && curr <= low) { - this.soundLevelCallback!(MICROBIT_HAL_MICROPHONE_EVT_THRESHOLD_LOW); - } else if (prev < high && curr >= high!) { - this.soundLevelCallback!(MICROBIT_HAL_MICROPHONE_EVT_THRESHOLD_HIGH); + if (this.soundLevelCallback) { + if (prev > low && curr <= low) { + this.soundLevelCallback(MICROBIT_HAL_MICROPHONE_EVT_THRESHOLD_LOW); + } else if (prev < high && curr >= high!) { + this.soundLevelCallback(MICROBIT_HAL_MICROPHONE_EVT_THRESHOLD_HIGH); + } } } - initialize(soundLevelCallback: (v: number) => void) { + initializeCallbacks(soundLevelCallback: (v: number) => void) { this.soundLevelCallback = soundLevelCallback; } dispose() { this.microphoneOff(); - this.soundLevelCallback = undefined; } } diff --git a/src/board/pins.ts b/src/board/pins.ts index b6d63ed1..17a0845f 100644 --- a/src/board/pins.ts +++ b/src/board/pins.ts @@ -113,7 +113,5 @@ export class Pin { } } - initialize() {} - dispose() {} } diff --git a/src/board/radio.ts b/src/board/radio.ts index a7bf6efc..e139a0a0 100644 --- a/src/board/radio.ts +++ b/src/board/radio.ts @@ -107,8 +107,6 @@ export class Radio { } } - initialize() {} - dispose() { this.rxQueue = undefined; this.config = undefined; diff --git a/src/board/wasm.ts b/src/board/wasm.ts index cdccf830..5b4e5d0e 100644 --- a/src/board/wasm.ts +++ b/src/board/wasm.ts @@ -1,76 +1,56 @@ -/** - * The places where we call into WASM or write to WASM owned memory. - */ -export class WebAssemblyOperations { - private _requestStop: (() => void) | undefined; - private main: (() => Promise) | undefined; - private stoppedPromise: Promise | undefined; - - defaultAudioCallback: (() => void) | undefined; - speechAudioCallback: (() => void) | undefined; - gestureCallback: ((gesture: number) => void) | undefined; - soundLevelCallback: ((soundLevel: number) => void) | undefined; - private radioRxBuffer: (() => number) | undefined; - - initialize() { - const cwrap = (window as any).Module.cwrap; - this._requestStop = cwrap("mp_js_request_stop", "null", [], {}); - - this.defaultAudioCallback = cwrap( - "microbit_hal_audio_ready_callback", - "null", - [], - {} - ); - - this.speechAudioCallback = cwrap( - "microbit_hal_audio_speech_ready_callback", - "null", - [], - {} - ); - - this.gestureCallback = cwrap( - "microbit_hal_gesture_callback", - "null", - ["number"], - {} - ); - - this.soundLevelCallback = cwrap( - "microbit_hal_level_detector_callback", - "null", - ["number"], - {} - ); +import { Board, PanicError } from "."; +import * as conversions from "./conversions"; +import { FileSystem } from "./fs"; + +export interface EmscriptenModule { + cwrap: any; + ExitStatus: Error; + + // See EXPORTED_FUNCTIONS in the Makefile. + _mp_js_request_stop(): void; + _mp_js_force_stop(): void; + _microbit_hal_audio_ready_callback(): void; + _microbit_hal_audio_speech_ready_callback(): void; + _microbit_hal_gesture_callback(gesture: number): void; + _microbit_hal_level_detector_callback(level: number): void; + _microbit_radio_rx_buffer(): number; + + HEAPU8: Uint8Array; + + // Added by us at module creation time for jshal to access. + board: Board; + fs: FileSystem; + conversions: typeof conversions; +} - this.radioRxBuffer = cwrap("microbit_radio_rx_buffer", "null", [], {}); +export class ModuleWrapper { + private main: () => Promise; - const main = cwrap("mp_js_main", "null", ["number"], { + constructor(private module: EmscriptenModule) { + const main = module.cwrap("mp_js_main", "null", ["number"], { async: true, }); this.main = () => main(64 * 1024); } - start(): void { - if (this.stoppedPromise) { - throw new Error("Already started!"); - } - this.stoppedPromise = this.main!(); + /** + * Throws PanicError if MicroPython panics. + */ + async start(): Promise { + return this.main!(); + } + + requestStop(): void { + this.module._mp_js_request_stop(); } - async stop(interrupt: () => void): Promise { - if (this.stoppedPromise) { - this._requestStop!(); - interrupt(); - await this.stoppedPromise; - this.stoppedPromise = undefined; - } + forceStop(): void { + this.module._mp_js_force_stop(); } writeRadioRxBuffer(packet: Uint8Array) { - const buf = this.radioRxBuffer!(); - window.HEAPU8.set(packet, buf); + const buf = this.module._microbit_radio_rx_buffer!(); + this.module!.HEAPU8.set(packet, buf); return buf; } } diff --git a/src/demo.html b/src/demo.html index 9ae1b01a..ab8c4e68 100644 --- a/src/demo.html +++ b/src/demo.html @@ -108,7 +108,6 @@

MicroPython-micro:bit simulator example embedding

>
- @@ -154,7 +153,7 @@

MicroPython-micro:bit simulator example embedding

case "radio_output": { const text = new TextDecoder() .decode(e.data.data) - .replace(/^\x01\x01\x00/, ""); + .replace(/^\x01\x00\x01/, ""); console.log(text); break; } @@ -220,20 +219,6 @@

MicroPython-micro:bit simulator example embedding

}); fetchProgram(sample.value); - // Ideally this would just be changing main.py via some filesystem - // message, then resetting. - document.querySelector("#run").addEventListener("click", async () => { - simulator.postMessage( - { - kind: "flash", - filesystem: { - "main.py": new TextEncoder().encode(program.value), - }, - }, - "*" - ); - }); - document.querySelector("#stop").addEventListener("click", async () => { simulator.postMessage( { diff --git a/src/jshal.js b/src/jshal.js index 7059670b..6c3caf2c 100644 --- a/src/jshal.js +++ b/src/jshal.js @@ -24,34 +24,35 @@ * THE SOFTWARE. */ + mergeInto(LibraryManager.library, { mp_js_hal_init: async function () { - board.initialize(); + Module.board.initialize(); }, mp_js_hal_deinit: function () { - board.dispose(); + Module.board.dispose(); }, mp_js_hal_ticks_ms: function () { - return board.ticksMilliseconds(); + return Module.board.ticksMilliseconds(); }, mp_js_hal_stdin_pop_char: function () { - return board.readSerialInput(); + return Module.board.readSerialInput(); }, mp_js_hal_stdout_tx_strn: function (ptr, len) { - board.writeSerialOutput(UTF8ToString(ptr, len)); + Module.board.writeSerialOutput(UTF8ToString(ptr, len)); }, mp_js_hal_filesystem_find: function (name, len) { - return fs.find(UTF8ToString(name, len)); + return Module.fs.find(UTF8ToString(name, len)); }, mp_js_hal_filesystem_create: function (name, len) { const filename = UTF8ToString(name, len); - return fs.create(filename); + return Module.fs.create(filename); }, mp_js_hal_filesystem_name: function (idx, buf) { @@ -65,222 +66,224 @@ mergeInto(LibraryManager.library, { }, mp_js_hal_filesystem_size: function (idx) { - return fs.size(idx); + return Module.fs.size(idx); }, mp_js_hal_filesystem_remove: function (idx) { - return fs.remove(idx); + return Module.fs.remove(idx); }, mp_js_hal_filesystem_readbyte: function (idx, offset) { - return fs.readbyte(idx, offset); + return Module.fs.readbyte(idx, offset); }, mp_js_hal_filesystem_write: function (idx, buf, len) { const data = new Uint8Array(HEAP8.buffer, buf, len); - return fs.write(idx, data); + return Module.fs.write(idx, data); }, mp_js_hal_reset: function () { - return board.reset(); + return Module.board.reset(); }, mp_js_hal_panic: function (code) { - return board.panic(code); + Module.board.throwPanic(code); }, mp_js_hal_temperature: function () { - return board.temperature.value; + return Module.board.temperature.value; }, mp_js_hal_button_get_presses: function (button) { - return board.buttons[button].getAndClearPresses(); + return Module.board.buttons[button].getAndClearPresses(); }, mp_js_hal_button_is_pressed: function (button) { - return board.buttons[button].isPressed(); + return Module.board.buttons[button].isPressed(); }, mp_js_hal_pin_is_touched: function (pin) { - return board.pins[pin].isTouched(); + return Module.board.pins[pin].isTouched(); }, mp_js_hal_display_get_pixel: function (x, y) { - return board.display.getPixel(x, y); + return Module.board.display.getPixel(x, y); }, mp_js_hal_display_set_pixel: function (x, y, value) { - board.display.setPixel(x, y, value); + Module.board.display.setPixel(x, y, value); }, mp_js_hal_display_clear: function () { - board.display.clear(); + Module.board.display.clear(); }, mp_js_hal_display_read_light_level: function () { - return board.display.lightLevel.value; + return Module.board.display.lightLevel.value; }, mp_js_hal_accelerometer_get_x: function () { - return board.accelerometer.state.accelerometerX.value; + return Module.board.accelerometer.state.accelerometerX.value; }, mp_js_hal_accelerometer_get_y: function () { - return board.accelerometer.state.accelerometerY.value; + return Module.board.accelerometer.state.accelerometerY.value; }, mp_js_hal_accelerometer_get_z: function () { - return board.accelerometer.state.accelerometerZ.value; + return Module.board.accelerometer.state.accelerometerZ.value; }, mp_js_hal_accelerometer_get_gesture: function () { - return conversions.convertAccelerometerStringToNumber( - board.accelerometer.state.gesture.value + return Module.conversions.convertAccelerometerStringToNumber( + Module.board.accelerometer.state.gesture.value ); }, mp_js_hal_accelerometer_set_range: function (r) { - board.accelerometer.setRange(r); + Module.board.accelerometer.setRange(r); }, mp_js_hal_compass_get_x: function () { - return board.compass.state.compassX.value; + return Module.board.compass.state.compassX.value; }, mp_js_hal_compass_get_y: function () { - return board.compass.state.compassY.value; + return Module.board.compass.state.compassY.value; }, mp_js_hal_compass_get_z: function () { - return board.compass.state.compassZ.value; + return Module.board.compass.state.compassZ.value; }, mp_js_hal_compass_get_field_strength: function () { - return board.compass.getFieldStrength(); + return Module.board.compass.getFieldStrength(); }, mp_js_hal_compass_get_heading: function () { - return board.compass.state.compassHeading.value; + return Module.board.compass.state.compassHeading.value; }, mp_js_hal_audio_set_volume: function (value) { - board.audio.setVolume(value); + Module.board.audio.setVolume(value); }, mp_js_hal_audio_init: function (sample_rate) { - board.audio.default.init(sample_rate); + Module.board.audio.default.init(sample_rate); }, mp_js_hal_audio_write_data: function (buf, num_samples) { - board.audio.default.writeData( - conversions.convertAudioBuffer( + Module.board.audio.default.writeData( + Module.conversions.convertAudioBuffer( + Module.HEAPU8, buf, - board.audio.default.createBuffer(num_samples) + Module.board.audio.default.createBuffer(num_samples) ) ); }, mp_js_hal_audio_speech_init: function (sample_rate) { - board.audio.speech.init(sample_rate); + Module.board.audio.speech.init(sample_rate); }, mp_js_hal_audio_speech_write_data: function (buf, num_samples) { - board.audio.speech.writeData( - conversions.convertAudioBuffer( + Module.board.audio.speech.writeData( + Module.conversions.convertAudioBuffer( + Module.HEAPU8, buf, - board.audio.speech.createBuffer(num_samples) + Module.board.audio.speech.createBuffer(num_samples) ) ); }, mp_js_hal_audio_period_us: function (period_us) { - board.audio.setPeriodUs(period_us); + Module.board.audio.setPeriodUs(period_us); }, mp_js_hal_audio_amplitude_u10: function (amplitude_u10) { - board.audio.setAmplitudeU10(amplitude_u10); + Module.board.audio.setAmplitudeU10(amplitude_u10); }, mp_js_hal_microphone_init: function () { - board.microphone.microphoneOn(); + Module.board.microphone.microphoneOn(); }, mp_js_hal_microphone_set_threshold: function (kind, value) { - board.microphone.setThreshold( - conversions.convertSoundThresholdNumberToString(kind), + Module.board.microphone.setThreshold( + Module.conversions.convertSoundThresholdNumberToString(kind), value ); }, mp_js_hal_microphone_get_level: function () { - return board.microphone.soundLevel.value; + return Module.board.microphone.soundLevel.value; }, mp_js_hal_audio_play_expression: function (expr) { - return board.audio.playSoundExpression(UTF8ToString(expr)); + return Module.board.audio.playSoundExpression(UTF8ToString(expr)); }, mp_js_hal_audio_stop_expression: function () { - return board.audio.stopSoundExpression(); + return Module.board.audio.stopSoundExpression(); }, mp_js_hal_audio_is_expression_active: function () { - return board.audio.isSoundExpressionActive(); + return Module.board.audio.isSoundExpressionActive(); }, mp_js_radio_enable: function (group, max_payload, queue) { - board.radio.enable({ group, maxPayload: max_payload, queue }); + Module.board.radio.enable({ group, maxPayload: max_payload, queue }); }, mp_js_radio_disable: function () { - board.radio.disable(); + Module.board.radio.disable(); }, mp_js_radio_update_config: function (group, max_payload, queue) { - board.radio.updateConfig({ group, maxPayload: max_payload, queue }); + Module.board.radio.updateConfig({ group, maxPayload: max_payload, queue }); }, mp_js_radio_send: function (buf, len, buf2, len2) { const data = new Uint8Array(len + len2); data.set(HEAPU8.slice(buf, buf + len)); data.set(HEAPU8.slice(buf2, buf2 + len2), len); - board.radio.send(data); + Module.board.radio.send(data); }, mp_js_radio_peek: function () { - const packet = board.radio.peek(); + const packet = Module.board.radio.peek(); if (packet) { - return board.operations.writeRadioRxBuffer(packet); + return Module.board.module.writeRadioRxBuffer(packet); } return null; }, mp_js_radio_pop: function () { - board.radio.pop(); + Module.board.radio.pop(); }, mp_js_hal_log_delete: function (full_erase) { // We don't have a notion of non-full erase. - board.dataLogging.delete(); + Module.board.dataLogging.delete(); }, mp_js_hal_log_set_mirroring: function (serial) { - board.dataLogging.setMirroring(serial); + Module.board.dataLogging.setMirroring(serial); }, mp_js_hal_log_set_timestamp: function (period) { - board.dataLogging.setTimestamp(period); + Module.board.dataLogging.setTimestamp(period); }, mp_js_hal_log_begin_row: function () { - return board.dataLogging.beginRow(); + return Module.board.dataLogging.beginRow(); }, mp_js_hal_log_end_row: function () { - return board.dataLogging.endRow(); + return Module.board.dataLogging.endRow(); }, mp_js_hal_log_data: function (key, value) { - return board.dataLogging.logData(UTF8ToString(key), UTF8ToString(value)); + return Module.board.dataLogging.logData(UTF8ToString(key), UTF8ToString(value)); }, }); diff --git a/src/main.c b/src/main.c index e4773006..482e9696 100644 --- a/src/main.c +++ b/src/main.c @@ -52,11 +52,14 @@ void mp_js_request_stop(void) { stop_requested = 1; } +void mp_js_force_stop(void) { + emscripten_force_exit(0); +} + // Main entrypoint called from JavaScript. // Calling mp_js_request_stop allows Ctrl-D to exit, otherwise Ctrl-D does a soft reset. // As we use asyncify you can await this call. void mp_js_main(int heap_size) { - stop_requested = 0; while (!stop_requested) { microbit_hal_init(); microbit_system_init(); diff --git a/src/main.js b/src/main.js deleted file mode 100644 index 7d3288e2..00000000 --- a/src/main.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * This file is part of the MicroPython project, http://micropython.org/ - * - * The MIT License (MIT) - * - * Copyright (c) 2022 Damien P. George - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -var Module = {}; - -Module.onRuntimeInitialized = () => { - window.board.initializedWebAssembly(); -}; diff --git a/src/pre.ts b/src/pre.ts deleted file mode 100644 index d6d01cf2..00000000 --- a/src/pre.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as conversions from "./board/conversions"; -import { FileSystem } from "./board/fs"; -import { WebAssemblyOperations } from "./board/wasm"; -import { - Board, - createBoard, - createMessageListener, - Notifications, -} from "./board"; - -declare global { - interface Window { - board: Board; - fs: FileSystem; - conversions: typeof conversions; - - HEAPU8: Uint8Array; - } -} - -// Initialize the globals used by the HAL. -window.fs = new FileSystem(); -window.board = createBoard( - new WebAssemblyOperations(), - new Notifications(window.parent), - window.fs -); -window.conversions = conversions; - -window.addEventListener("message", createMessageListener(window.board)); diff --git a/src/simulator.html b/src/simulator.html index 32e408c9..263e8687 100644 --- a/src/simulator.html +++ b/src/simulator.html @@ -68,6 +68,7 @@
- + + diff --git a/src/simulator.ts b/src/simulator.ts new file mode 100644 index 00000000..c45f60e3 --- /dev/null +++ b/src/simulator.ts @@ -0,0 +1,20 @@ +import * as conversions from "./board/conversions"; +import { FileSystem } from "./board/fs"; +import { EmscriptenModule } from "./board/wasm"; +import { + Board, + createBoard, + createMessageListener, + Notifications, +} from "./board"; + +declare global { + interface Window { + // Provided by firmware.js + createModule: (args: object) => Promise; + } +} + +const fs = new FileSystem(); +const board = createBoard(new Notifications(window.parent), fs); +window.addEventListener("message", createMessageListener(board)); From 57ebc70fe4d4948244905e2298780bea158d1703 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Fri, 9 Sep 2022 15:27:41 +0100 Subject: [PATCH 2/5] Tidy. --- src/board/wasm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/board/wasm.ts b/src/board/wasm.ts index 5b4e5d0e..a0627aea 100644 --- a/src/board/wasm.ts +++ b/src/board/wasm.ts @@ -1,4 +1,4 @@ -import { Board, PanicError } from "."; +import { Board } from "."; import * as conversions from "./conversions"; import { FileSystem } from "./fs"; From 94d5256000195be811ff9754d8f964eade5c9255 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Fri, 9 Sep 2022 15:29:11 +0100 Subject: [PATCH 3/5] Tweak. --- src/board/wasm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/board/wasm.ts b/src/board/wasm.ts index a0627aea..6d5bbfbf 100644 --- a/src/board/wasm.ts +++ b/src/board/wasm.ts @@ -50,7 +50,7 @@ export class ModuleWrapper { writeRadioRxBuffer(packet: Uint8Array) { const buf = this.module._microbit_radio_rx_buffer!(); - this.module!.HEAPU8.set(packet, buf); + this.module.HEAPU8.set(packet, buf); return buf; } } From f3d6332b0c95eb893f1bf24e9484c85ff405e183 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon <44397098+microbit-matt-hillsdon@users.noreply.github.com> Date: Fri, 9 Sep 2022 15:52:22 +0100 Subject: [PATCH 4/5] Fix microphone callback Co-authored-by: Robert Knight <95928279+microbit-robert@users.noreply.github.com> --- src/board/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/board/index.ts b/src/board/index.ts index e9f1d9be..7cf2eb73 100644 --- a/src/board/index.ts +++ b/src/board/index.ts @@ -178,7 +178,7 @@ export class Board { wrapped._microbit_hal_gesture_callback ); this.microphone.initializeCallbacks( - wrapped._microbit_hal_audio_speech_ready_callback + wrapped._microbit_hal_level_detector_callback ); return module; } From b8e0b53728a7e858bb9b2328bda5f9357f638ebd Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Fri, 9 Sep 2022 16:04:16 +0100 Subject: [PATCH 5/5] Remove unnecessary init dance. We already precompile the WASM. --- src/board/index.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/board/index.ts b/src/board/index.ts index e9f1d9be..049ffe00 100644 --- a/src/board/index.ts +++ b/src/board/index.ts @@ -77,7 +77,6 @@ export class Board { return result ?? id; }; - private initialModulePromise: Promise | undefined; /** * Defined during start(). */ @@ -92,9 +91,6 @@ export class Board { private fs: FileSystem, private svg: SVGElement ) { - // We start this loading here so as not to delay loading WASM until start(). - this.initialModulePromise = this.createModule(); - this.display = new Display( Array.from(this.svg.querySelector("#LEDsOn")!.querySelectorAll("use")) ); @@ -320,14 +316,8 @@ export class Board { if (this.modulePromise) { throw new Error("Module already exists!"); } - // Use the pre-loaded one the first time through. - if (this.initialModulePromise) { - this.modulePromise = this.initialModulePromise; - this.initialModulePromise = undefined; - } else { - this.modulePromise = this.createModule(); - } + this.modulePromise = this.createModule(); const module = await this.modulePromise; let panicCode: number | undefined; try {