stm32f4-emu 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
- # STM32F4 Emulator
1
+ # STM32F4 Emulator (`stm32f4-emu`)
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/stm32f4-emu.svg)](https://www.npmjs.com/package/stm32f4-emu)
4
+ [![npm downloads](https://img.shields.io/npm/dm/stm32f4-emu.svg)](https://www.npmjs.com/package/stm32f4-emu)
5
+ [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE)
3
6
  [![CI](https://github.com/danish9661/stm32F4-emulator/actions/workflows/ci.yml/badge.svg)](https://github.com/danish9661/stm32F4-emulator/actions/workflows/ci.yml)
4
7
  [![Pages](https://github.com/danish9661/stm32F4-emulator/actions/workflows/pages.yml/badge.svg)](https://github.com/danish9661/stm32F4-emulator/actions/workflows/pages.yml)
5
8
 
@@ -72,6 +75,10 @@ npm run serve # then open http://127.0.0.1:8123
72
75
  # node end-to-end flow test (boot -> DHCP -> TCP -> HTTP, 2 rounds)
73
76
  npm test # == node site/test_flow.mjs
74
77
 
78
+ # websocket bridge: headless Node serves the emulator, browser is a thin UI
79
+ npm run bridge -- blinky/blinky.bin --port 8234
80
+ # then open http://127.0.0.1:8123?bridge=ws://127.0.0.1:8234
81
+
75
82
  # gateway-backed run: firmware talks to a REAL network stack (gVisor)
76
83
  cd stm32-periph-wasm/pkg
77
84
  node cli.mjs ../../eth_http/eth_http.bin 10000000 \
@@ -126,6 +133,29 @@ as a library stays dependency-free; only MCP users need
126
133
  `npm i @modelcontextprotocol/sdk zod`. See [docs/mcp.md](docs/mcp.md) for
127
134
  the tool reference and client config.
128
135
 
136
+ ## WebSocket bridge (headless Node ↔ browser UI)
137
+
138
+ A binary WebSocket protocol so the browser console can drive the emulator
139
+ running headlessly in Node — all WASM execution stays in Node, the browser
140
+ is a thin UI. Zero impact on the existing local WASM path:
141
+
142
+ ```bash
143
+ # 1. Start the bridge in Node (serves the emulator over WS on port 8234)
144
+ node site/ws-bridge.mjs eth_http/eth_http.bin --port 8234
145
+
146
+ # 2. Open the browser console with the bridge URL param
147
+ open "http://127.0.0.1:8123/?bridge=ws://127.0.0.1:8234"
148
+ ```
149
+
150
+ The `RemoteEmu` adapter (`site/remote-emu.js`) is a drop-in replacement
151
+ for the local `emu` object — same `step()`/`drainUart()`/`read32()` API,
152
+ all proxied over binary WebSocket. Device stubs (OLED/TFT/etc.) run in
153
+ Node and are not visible to browser JS. Without `?fw=`, the page boots
154
+ whatever firmware the bridge was started with; with `?fw=blinky&bridge=ws://…`,
155
+ the browser sends the firmware image over the bridge.
156
+
157
+ See AGENTS.md §20 for the full binary protocol reference.
158
+
129
159
  ## Firmwares
130
160
 
131
161
  | Firmware | What it does | Success marker |
@@ -246,6 +276,7 @@ rebuild — delete it so the vendor assets stay tracked/committed.
246
276
 
247
277
  ## Documentation
248
278
 
279
+ - [site/about.html](site/about.html) — in-repo About page: what it is, architecture, featured firmwares, and how to use it (CLI / browser / Node API / MCP).
249
280
  - [docs/architecture.md](docs/architecture.md) — how the emulator is put
250
281
  together (CPU, peripheral model, drivers, ETH flow, interrupts).
251
282
  - [docs/peripherals.md](docs/peripherals.md) — all 33 peripherals and the
@@ -262,12 +293,9 @@ rebuild — delete it so the vendor assets stay tracked/committed.
262
293
  - [docs/progress-and-future.md](docs/progress-and-future.md) — status,
263
294
  known limitations, roadmap.
264
295
 
265
- ## License
266
-
267
- GPL-3.0-only. See [LICENSE](LICENSE).
296
+ ## License & Credits
268
297
 
269
- *This repository is a fork/continuation of
270
- [nviennot/stm32-emulator](https://github.com/nviennot/stm32-emulator), which
271
- emulated 3D-printer firmwares (Elegoo Saturn, Anycubic Mono X) in a native
272
- SDL app. The WASM headless emulator, network firmwares, browser demo, and npm
273
- package are new work built on that base.*
298
+ - **License**: GPL-3.0-only. See [LICENSE](LICENSE).
299
+ - **Unicorn CPU Core**: Powered by [Unicorn.js](https://github.com/AlexAltea/unicorn.js) by [Alex Altea](https://github.com/AlexAltea) (WASM/JS port of the [Unicorn Engine](https://www.unicorn-engine.org/) CPU emulator, derived from QEMU, licensed under GPLv2).
300
+ - **Heritage**: Fork and continuation of [nviennot/stm32-emulator](https://github.com/nviennot/stm32-emulator) (native SDL 3D printer emulator by Nicolas Viennot). The headless WASM peripheral model, networking stack, browser demo, virtual components API, MCP server, and npm package are new work built on that base.
301
+ - **DOOM**: Ported using [doomgeneric](https://github.com/ozkl/doomgeneric) by Ozkan Sezgin.
package/cli.mjs ADDED
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ // stm32f4-emu — headless CLI to load and run an STM32F4 firmware image.
3
+ //
4
+ // stm32f4-emu <firmware> [--inst N] [--format auto|bin|hex|elf] [--verbose]
5
+ // [--lowpower]
6
+ //
7
+ // Loads a .bin/.elf/.hex firmware, boots it in the emulator, and streams the
8
+ // guest UART to stdout. Peripheral register accesses are traced to stderr when
9
+ // --verbose is given. With --lowpower, the core halts on WFI/WFE and the
10
+ // emulator advances the virtual RTC until a wakeup source (e.g. an RTC alarm)
11
+ // fires — so firmware that enters STOP (e.g. deep_sleep_demo.bin) runs.
12
+ import { readFileSync } from 'node:fs';
13
+ import { createRequire } from 'node:module';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { dirname, resolve } from 'node:path';
16
+ import * as bindings from './site/vendor/stm32_periph_wasm.js';
17
+ import { createEmulator } from './site/emulator.js';
18
+ import { parseIntelHex, parseElf } from './site/loaders.js';
19
+
20
+ const require = createRequire(import.meta.url);
21
+ const unicornFactory = require('./site/vendor/unicorn_arm.cjs');
22
+ const __dirname = dirname(fileURLToPath(import.meta.url));
23
+ const svdXml = readFileSync(resolve(__dirname, 'site/vendor/stm32f407.svd'), 'utf8');
24
+ const wasmBytes = new Uint8Array(readFileSync(resolve(__dirname, 'site/vendor/stm32_periph_wasm_bg.wasm')));
25
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf8'));
26
+
27
+ const USAGE = `stm32f4-emu — run an STM32F4 firmware image in the emulator
28
+
29
+ Usage:
30
+ stm32f4-emu <firmware> [options]
31
+
32
+ Arguments:
33
+ <firmware> path to a firmware image (.bin, .elf, or .hex)
34
+
35
+ Options:
36
+ -n, --inst <N> instruction budget to run (default 20000000)
37
+ -f, --format <fmt> firmware format: auto|bin|hex|elf (default auto)
38
+ -v, --verbose trace peripheral register reads/writes to stderr
39
+ -l, --lowpower halt on WFI/WFE and advance the virtual RTC until wakeup
40
+ -h, --help show this help
41
+ -V, --version show version
42
+
43
+ Examples:
44
+ stm32f4-emu build/firmware.bin
45
+ stm32f4-emu app.elf --inst 5000000 --verbose
46
+ stm32f4-emu deep_sleep_demo.bin --lowpower
47
+ `;
48
+
49
+ function fail(msg) {
50
+ process.stderr.write(msg + '\n\n' + USAGE);
51
+ process.exit(2);
52
+ }
53
+
54
+ function detectFormat(buf, format) {
55
+ if (format !== 'auto') return format;
56
+ if (buf[0] === 0x7F && buf[1] === 0x45 && buf[2] === 0x4C && buf[3] === 0x46) return 'elf';
57
+ let i = 0;
58
+ while (i < buf.length && (buf[i] === 0x20 || buf[i] === 0x09 || buf[i] === 0x0D || buf[i] === 0x0A)) i++;
59
+ if (buf[i] === 0x3A) return 'hex'; // ':'
60
+ return 'bin';
61
+ }
62
+
63
+ async function main() {
64
+ const args = process.argv.slice(2);
65
+ let firmwarePath = null;
66
+ let inst = 20000000;
67
+ let format = 'auto';
68
+ let verbose = false;
69
+ let lowpower = false;
70
+ for (let i = 0; i < args.length; i++) {
71
+ const a = args[i];
72
+ if (a === '-h' || a === '--help') { process.stdout.write(USAGE); process.exit(0); }
73
+ if (a === '-V' || a === '--version') { process.stdout.write(`stm32f4-emu ${pkg.version}\n`); process.exit(0); }
74
+ if (a === '-v' || a === '--verbose') { verbose = true; continue; }
75
+ if (a === '-l' || a === '--lowpower') { lowpower = true; continue; }
76
+ if (a === '-n' || a === '--inst') {
77
+ const v = args[++i];
78
+ inst = Number(v);
79
+ if (!Number.isFinite(inst) || inst <= 0) fail(`invalid --inst value: ${v}`);
80
+ continue;
81
+ }
82
+ if (a === '-f' || a === '--format') {
83
+ format = args[++i];
84
+ if (!['auto', 'bin', 'hex', 'elf'].includes(format)) fail(`invalid --format: ${format}`);
85
+ continue;
86
+ }
87
+ if (a.startsWith('-')) fail(`unknown option: ${a}`);
88
+ if (firmwarePath) fail('multiple firmware paths given');
89
+ firmwarePath = a;
90
+ }
91
+ if (!firmwarePath) fail('no firmware path given');
92
+
93
+ let raw;
94
+ try {
95
+ raw = new Uint8Array(readFileSync(firmwarePath));
96
+ } catch (e) {
97
+ fail(`cannot read firmware '${firmwarePath}': ${e.message}`);
98
+ }
99
+
100
+ const fmt = detectFormat(raw, format);
101
+ let firmware, extra_mem = [];
102
+ try {
103
+ if (fmt === 'elf') {
104
+ const elf = parseElf(raw);
105
+ if (!elf.flash) fail('ELF contains no FLASH segment (expected a segment at 0x08000000)');
106
+ firmware = elf.flash;
107
+ extra_mem = elf.extraMem || [];
108
+ } else if (fmt === 'hex') {
109
+ const hex = parseIntelHex(Buffer.from(raw).toString('latin1'));
110
+ if (!hex.flash) fail('Intel HEX contains no FLASH records (expected data in the 0x08000000 range)');
111
+ firmware = hex.flash;
112
+ } else {
113
+ firmware = raw;
114
+ }
115
+ } catch (e) {
116
+ fail(`failed to parse ${fmt} firmware: ${e.message}`);
117
+ }
118
+
119
+ let emu;
120
+ try {
121
+ emu = await createEmulator({
122
+ firmware, bindings, unicorn: unicornFactory, svdXml, wasmInit: wasmBytes,
123
+ extra_mem, verbose, lowpower,
124
+ });
125
+ } catch (e) {
126
+ fail(`emulator failed to load firmware: ${e.message}`);
127
+ }
128
+
129
+ const STEP = 100000;
130
+ let remaining = inst;
131
+ let lastInst = 0;
132
+ try {
133
+ while (remaining > 0) {
134
+ const take = Math.min(STEP, remaining);
135
+ const r = emu.step(take);
136
+ remaining -= (r.instCount - lastInst);
137
+ lastInst = r.instCount;
138
+ const u = emu.drainUart();
139
+ if (u && u.length) process.stdout.write(u.toString());
140
+ if (r.stopped) break;
141
+ }
142
+ const tail = emu.drainUart();
143
+ if (tail && tail.length) process.stdout.write(tail.toString());
144
+ } finally {
145
+ try { emu.close(); } catch {}
146
+ }
147
+ }
148
+
149
+ main().catch((e) => {
150
+ process.stderr.write(`stm32f4-emu: ${e.message}\n`);
151
+ process.exit(1);
152
+ });
package/index.mjs CHANGED
@@ -8,6 +8,14 @@ import { createEmulator } from './site/emulator.js';
8
8
  import { createNetSim } from './site/netsim.js';
9
9
  import { FIRMWARES } from './site/firmware.js';
10
10
  import { LED, Button, Pwm, I2cRegisterDevice, Potentiometer } from './site/components.js';
11
+ import { STM32F4, GPIOPin, USART, DMAStream } from './site/stm32f4.js';
12
+
13
+ // Node consumers get a one-call setup that injects the bundled assets. The
14
+ // `firmware` option is optional (defer to loadBin/loadHex/loadELF after
15
+ // create), matching the rp2040js / avr8js ergonomics.
16
+ STM32F4.create = (opts = {}) => STM32F4._create({
17
+ bindings, unicorn: unicornFactory, svdXml, wasmInit: wasmBytes, ...opts,
18
+ });
11
19
 
12
20
  const require = createRequire(import.meta.url);
13
21
  const unicornFactory = require('./site/vendor/unicorn_arm.cjs');
@@ -36,4 +44,4 @@ export async function createSTM32F407(opts = {}) {
36
44
  return createEmulator({ ...opts, firmware: bin, bindings, unicorn: unicornFactory, svdXml, wasmInit: wasmBytes });
37
45
  }
38
46
 
39
- export { createEmulator, createNetSim, FIRMWARES, bindings, unicornFactory, svdXml, LED, Button, Pwm, I2cRegisterDevice, Potentiometer };
47
+ export { createEmulator, createNetSim, FIRMWARES, bindings, unicornFactory, svdXml, LED, Button, Pwm, I2cRegisterDevice, Potentiometer, STM32F4, GPIOPin, USART, DMAStream };
package/mcp/server.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
2
3
  // MCP server exposing the STM32F407 emulator as tools an MCP client
3
4
  // (Claude Code / Claude Desktop / any MCP host) can drive: load firmware,
4
5
  // step execution, read/write UART, poke and watch GPIO pins, inject ADC
@@ -31,6 +32,25 @@ try {
31
32
  }
32
33
 
33
34
  let session = null; // { emu, firmware, components: Map }
35
+
36
+ // ── CLI surface for the bin: stm32f4-mcp --help / --version ──
37
+ // Printed before the stdio MCP transport starts (stdout is the JSON-RPC
38
+ // channel once running, so we must exit before connecting).
39
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
40
+ process.stdout.write(
41
+ 'stm32f4-mcp — MCP server exposing the STM32F407 emulator.\n\n' +
42
+ 'Usage: stm32f4-mcp [--help] [--version]\n\n' +
43
+ 'Speaks MCP over stdio (JSON-RPC). Connect it from an MCP client such\n' +
44
+ 'as Claude Code or Claude Desktop. Optional peer deps:\n' +
45
+ ' npm install @modelcontextprotocol/sdk zod\n'
46
+ );
47
+ process.exit(0);
48
+ }
49
+ if (process.argv.includes('--version') || process.argv.includes('-V')) {
50
+ const ver = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
51
+ process.stdout.write(`stm32f4-mcp ${ver}\n`);
52
+ process.exit(0);
53
+ }
34
54
  let componentSeq = 0;
35
55
 
36
56
  const text = (s) => ({ content: [{ type: 'text', text: typeof s === 'string' ? s : JSON.stringify(s, null, 2) }] });
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "stm32f4-emu",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "description": "STM32F407 emulator: Unicorn (WASM) CPU + Rust peripheral model. Runs real Cortex-M4 firmware in Node or the browser.",
5
5
  "type": "module",
6
6
  "main": "index.mjs",
7
7
  "bin": {
8
- "stm32f4-mcp": "./mcp/server.mjs"
8
+ "stm32f4-emu": "./cli.mjs",
9
+ "stm32f4-mcp": "./mcp/server.mjs",
10
+ "stm32f4-bridge": "./site/ws-bridge.mjs"
9
11
  },
10
12
  "exports": {
11
13
  ".": "./index.mjs",
@@ -14,12 +16,16 @@
14
16
  "./netsim": "./site/netsim.js",
15
17
  "./firmwares": "./site/firmware.js",
16
18
  "./loaders": "./site/loaders.js",
19
+ "./stm32f4": "./site/stm32f4.js",
17
20
  "./vendor": "./site/vendor/stm32_periph_wasm.js",
18
21
  "./vendor/unicorn": "./site/vendor/unicorn_arm.cjs",
22
+ "./remote": "./site/remote-emu.js",
23
+ "./ws-bridge": "./site/ws-bridge.mjs",
19
24
  "./site": "./site/index.html"
20
25
  },
21
26
  "files": [
22
27
  "index.mjs",
28
+ "cli.mjs",
23
29
  "README.md",
24
30
  "site/emulator.js",
25
31
  "site/components.js",
@@ -29,6 +35,8 @@
29
35
  "site/loaders.js",
30
36
  "site/index.html",
31
37
  "site/app.js",
38
+ "site/remote-emu.js",
39
+ "site/ws-bridge.mjs",
32
40
  "site/vendor/"
33
41
  ],
34
42
  "keywords": [
@@ -55,29 +63,52 @@
55
63
  "node": ">=20"
56
64
  },
57
65
  "scripts": {
58
- "test": "node site/test_flow.mjs && node site/test_blinky.mjs && node site/test_rx_interrupt.mjs && node site/test_component_led.mjs && node site/test_component_button.mjs && node site/test_component_pwm.mjs && node site/test_component_i2cregfile.mjs && node site/test_component_adc.mjs && node site/test_multi_instance.mjs && node site/test_fsmc_dcmi.mjs && node site/test_fsmc.mjs && node site/test_dcmi.mjs",
66
+ "test": "node site/test_stm32f4_api.mjs && node site/test_stm32f4_periph.mjs && node site/test_ws_bridge.mjs && node site/test_flow.mjs && node site/test_blinky.mjs && node site/test_rx_interrupt.mjs && node site/test_component_led.mjs && node site/test_component_button.mjs && node site/test_component_pwm.mjs && node site/test_component_i2cregfile.mjs && node site/test_component_adc.mjs && node site/test_multi_instance.mjs && node site/test_fsmc_dcmi.mjs && node site/test_fsmc.mjs && node site/test_dcmi.mjs && node site/probe_freertos.mjs && node site/test_candemo.mjs && node site/test_lowpower.mjs && node site/test_edge_cases.mjs && node site/test_can_inject.mjs && node site/test_watchdog.mjs && node site/test_wwdg.mjs && node site/test_wwdg_window.mjs && node site/test_tim_capture.mjs && node site/test_qspi.mjs && node site/test_qspi_cdp.mjs && node site/test_browser.mjs",
67
+ "test:unit": "node site/test_stm32f4_api.mjs && node site/test_stm32f4_periph.mjs && node site/test_ws_bridge.mjs && node site/test_flow.mjs && node site/test_blinky.mjs && node site/test_rx_interrupt.mjs && node site/test_component_led.mjs && node site/test_component_button.mjs && node site/test_component_pwm.mjs && node site/test_component_i2cregfile.mjs && node site/test_component_adc.mjs && node site/test_multi_instance.mjs && node site/test_fsmc_dcmi.mjs && node site/test_fsmc.mjs && node site/test_dcmi.mjs && node site/probe_freertos.mjs && node site/test_candemo.mjs && node site/test_lowpower.mjs && node site/test_edge_cases.mjs && node site/test_can_inject.mjs && node site/test_watchdog.mjs && node site/test_wwdg.mjs && node site/test_wwdg_window.mjs && node site/test_tim_capture.mjs && node site/test_qspi.mjs",
68
+ "test:bridge": "node site/test_ws_bridge.mjs",
69
+ "test:bridge:browser": "node site/test_bridge_cdp.mjs",
59
70
  "test:flow": "node site/test_flow.mjs",
60
71
  "test:blinky": "node site/test_blinky.mjs",
61
72
  "test:rx": "node site/test_rx_interrupt.mjs",
62
73
  "test:multi": "node site/test_multi_instance.mjs",
74
+ "test:can": "node site/test_candemo.mjs",
75
+ "test:caninject": "node site/test_can_inject.mjs",
76
+ "test:watchdog": "node site/test_watchdog.mjs",
77
+ "test:wwdg": "node site/test_wwdg.mjs",
78
+ "test:wwdgwin": "node site/test_wwdg_window.mjs",
79
+ "test:timcap": "node site/test_tim_capture.mjs",
80
+ "test:lowpower": "node site/test_lowpower.mjs",
81
+ "test:qspi": "node site/test_qspi.mjs",
82
+ "test:qspi:browser": "node site/test_qspi_cdp.mjs",
83
+ "test:browser": "node site/test_browser.mjs",
84
+ "test:edge": "node site/test_edge_cases.mjs",
63
85
  "test:fsmc": "node site/test_fsmc.mjs && node site/test_dcmi.mjs && node site/test_fsmc_dcmi.mjs",
64
86
  "test:components": "node site/test_component_led.mjs && node site/test_component_button.mjs && node site/test_component_pwm.mjs && node site/test_component_i2cregfile.mjs && node site/test_component_adc.mjs",
65
87
  "test:mcp": "node mcp/test_mcp.mjs",
66
88
  "mcp": "node mcp/server.mjs",
89
+ "bridge": "node site/ws-bridge.mjs",
67
90
  "firmwares": "node tools/make_firmware.mjs",
68
91
  "serve": "python3 -m http.server 8123 --directory site",
69
92
  "prepack": "node tools/make_firmware.mjs"
70
93
  },
94
+ "dependencies": {
95
+ "ws": "^8.21.3"
96
+ },
71
97
  "peerDependencies": {
72
98
  "@modelcontextprotocol/sdk": "^1.30.0",
73
99
  "zod": "^3.25 || ^4.0"
74
100
  },
75
101
  "peerDependenciesMeta": {
76
- "@modelcontextprotocol/sdk": { "optional": true },
77
- "zod": { "optional": true }
102
+ "@modelcontextprotocol/sdk": {
103
+ "optional": true
104
+ },
105
+ "zod": {
106
+ "optional": true
107
+ }
78
108
  },
79
109
  "devDependencies": {
80
110
  "@modelcontextprotocol/sdk": "^1.30.0",
111
+ "playwright": "^1.62.1",
81
112
  "zod": "^3.25 || ^4.0"
82
113
  }
83
114
  }