stm32f4-emu 0.1.0 → 1.1.1

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,10 +1,14 @@
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
 
6
9
  An STM32F407 microcontroller emulator that runs real Cortex-M4 firmware. It
7
- combines a **Unicorn CPU core** (QEMU-derived, compiled to WASM) with a
10
+ combines a **Rust CPU core** (a WASM-native Thumb-2 interpreter with exact
11
+ Cortex-M exception entry/return, including the VFPv4-SP FPU) with a
8
12
  **Rust peripheral model** (RCC, USART, GPIO, DMA, ETH, TIM, NVIC, ...) also
9
13
  compiled to WASM — so the whole machine runs headless in **Node.js or a
10
14
  browser tab**, with no SDL, no native deps, no hardware.
@@ -29,18 +33,18 @@ scripted network (netsim) as fallback, a live UART terminal (with **UART
29
33
  RX input** — type into the console and the firmware reads it; newline
30
34
  characters excluded per HTML spec, see AGENTS.md §11), GPIO pin readout
31
35
  for banks A–E, and key peripheral registers. Interrupt-driven firmware
32
- (`rx_interrupt_test`, `rx_crypto_test`) is serviced by an opt-in guest-IRQ
33
- pump; polling firmware (the ETH demos) never uses it. For automation, a
36
+ (`rx_interrupt_test`, `rx_crypto_test`) gets inline guest-IRQ delivery;
37
+ polling firmware (the ETH demos) never uses it. For automation, a
34
38
  preset can auto-boot via the URL: `?fw=eth_http`, `?fw=blinky`, `?fw=crypto_test`, …
35
39
 
36
40
  ## DOOM (in the browser)
37
41
 
38
42
  **[`site/doom.html`](site/doom.html)** runs DOOM 1 shareware
39
- (doomgeneric, ported to the emulated F407) at ~25 FPS in a
40
- headless-Chrome-verified browser page — playable, but below DOOM's native
41
- 35 fps: at ~918k guest instructions per rendered frame, 35 fps would need
42
- ~32 MIPS and the Unicorn WASM core tops out near 20-24 (details and the
43
- measurements in AGENTS.md §16). Because the guest mixes one frame of audio
43
+ (doomgeneric, ported to the emulated F407) at the full 35 fps in a
44
+ headless-Chrome-verified browser page — playable: at ~1M guest instructions
45
+ per rendered frame, 35 fps needs ~32 MIPS and the Rust core delivers ~65
46
+ (details and the measurements in AGENTS.md §16/§22). Because the guest mixes
47
+ one frame of audio
44
48
  per rendered frame, sound below 35 fps plays slightly slow and pitched-down
45
49
  rather than breaking up — the worklet rate-matches instead of inserting
46
50
  gaps, and the stats line reports it (`audio 0.72x`). The page: 320×200
@@ -72,6 +76,10 @@ npm run serve # then open http://127.0.0.1:8123
72
76
  # node end-to-end flow test (boot -> DHCP -> TCP -> HTTP, 2 rounds)
73
77
  npm test # == node site/test_flow.mjs
74
78
 
79
+ # websocket bridge: headless Node serves the emulator, browser is a thin UI
80
+ npm run bridge -- blinky/blinky.bin --port 8234
81
+ # then open http://127.0.0.1:8123?bridge=ws://127.0.0.1:8234
82
+
75
83
  # gateway-backed run: firmware talks to a REAL network stack (gVisor)
76
84
  cd stm32-periph-wasm/pkg
77
85
  node cli.mjs ../../eth_http/eth_http.bin 10000000 \
@@ -126,6 +134,29 @@ as a library stays dependency-free; only MCP users need
126
134
  `npm i @modelcontextprotocol/sdk zod`. See [docs/mcp.md](docs/mcp.md) for
127
135
  the tool reference and client config.
128
136
 
137
+ ## WebSocket bridge (headless Node ↔ browser UI)
138
+
139
+ A binary WebSocket protocol so the browser console can drive the emulator
140
+ running headlessly in Node — all WASM execution stays in Node, the browser
141
+ is a thin UI. Zero impact on the existing local WASM path:
142
+
143
+ ```bash
144
+ # 1. Start the bridge in Node (serves the emulator over WS on port 8234)
145
+ node site/ws-bridge.mjs eth_http/eth_http.bin --port 8234
146
+
147
+ # 2. Open the browser console with the bridge URL param
148
+ open "http://127.0.0.1:8123/?bridge=ws://127.0.0.1:8234"
149
+ ```
150
+
151
+ The `RemoteEmu` adapter (`site/remote-emu.js`) is a drop-in replacement
152
+ for the local `emu` object — same `step()`/`drainUart()`/`read32()` API,
153
+ all proxied over binary WebSocket. Device stubs (OLED/TFT/etc.) run in
154
+ Node and are not visible to browser JS. Without `?fw=`, the page boots
155
+ whatever firmware the bridge was started with; with `?fw=blinky&bridge=ws://…`,
156
+ the browser sends the firmware image over the bridge.
157
+
158
+ See AGENTS.md §20 for the full binary protocol reference.
159
+
129
160
  ## Firmwares
130
161
 
131
162
  | Firmware | What it does | Success marker |
@@ -152,9 +183,9 @@ watchdog timeouts close to spec) will diverge — see docs/progress-and-future.m
152
183
  ## Architecture
153
184
 
154
185
  ```
155
- firmware .bin ──► Unicorn WASM CPU ──► memory hooks
186
+ firmware .bin ──► Rust WASM CPU (Thumb-2 + NVIC/exceptions)
156
187
  │
157
- periph_read/write ──► Rust peripheral model (WASM)
188
+ peripheral MMIO ──► Rust peripheral model (WASM)
158
189
  RCC USART GPIO DMA ETH TIM NVIC
159
190
  │
160
191
  UART out / ETH TX frames ──► driver (cli.mjs / site/emulator.js)
@@ -162,9 +193,10 @@ firmware .bin ──► Unicorn WASM CPU ──► memory hooks
162
193
  RX frames injected (netsim, or real gVisor gateway)
163
194
  ```
164
195
 
165
- - **CPU**: Unicorn 2.1.4 compiled to WASM executes Thumb-2 code; every
166
- read/write to a hooked MMIO range is routed into the Rust model, which
167
- answers by writing the modeled register value back into guest memory.
196
+ - **CPU**: a pure-Rust Cortex-M4 Thumb-2 interpreter compiled to WASM;
197
+ peripheral accesses call straight into the Rust model, and guest IRQs
198
+ (SysTick, ETH, USART, SVC/PendSV) are delivered inline with exact
199
+ exception stacking — no native deps, no JIT, no hooks.
168
200
  - **Peripherals**: a `wasm-bindgen` crate (`stm32-periph-wasm/`); registers
169
201
  and bit fields come from the vendor SVD (`monox/stm32f407.svd`).
170
202
  - **Ethernet**: TX is captured from the DMA descriptors; RX frames are
@@ -193,8 +225,8 @@ firmware .bin ──► Unicorn WASM CPU ──► memory hooks
193
225
  │ ├── test_blinky.mjs Node blinky GPIO test (npm test)
194
226
  │ ├── test_rx_interrupt.mjs Node UART-interrupt test (npm test)
195
227
  │ ├── test_component_*.mjs Component-API tests, one firmware each (npm test)
196
- │ ├── test_doom.mjs Node DOOM boot/menu/gameplay/save test
197
- │ └── vendor/ Browser WASM build, SVD, Unicorn
228
+ │ ├── test_doom_wasm.mjs Node DOOM boot/menu/gameplay/save test
229
+ │ └── vendor/ Browser WASM build (CPU + peripherals), SVD
198
230
  ├── index.mjs, package.json npm package entry (stm32f4-emu)
199
231
  ├── mcp/ MCP server (drive the emulator from an AI agent)
200
232
  ├── .github/workflows/ CI (Linux/Windows/macOS test matrix) + Pages deploy
@@ -246,6 +278,7 @@ rebuild — delete it so the vendor assets stay tracked/committed.
246
278
 
247
279
  ## Documentation
248
280
 
281
+ - [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
282
  - [docs/architecture.md](docs/architecture.md) — how the emulator is put
250
283
  together (CPU, peripheral model, drivers, ETH flow, interrupts).
251
284
  - [docs/peripherals.md](docs/peripherals.md) — all 33 peripherals and the
@@ -262,12 +295,8 @@ rebuild — delete it so the vendor assets stay tracked/committed.
262
295
  - [docs/progress-and-future.md](docs/progress-and-future.md) — status,
263
296
  known limitations, roadmap.
264
297
 
265
- ## License
266
-
267
- GPL-3.0-only. See [LICENSE](LICENSE).
298
+ ## License & Credits
268
299
 
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.*
300
+ - **License**: GPL-3.0-only. See [LICENSE](LICENSE).
301
+ - **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.
302
+ - **DOOM**: Ported using [doomgeneric](https://github.com/ozkl/doomgeneric) by Ozkan Sezgin.
package/cli.mjs ADDED
@@ -0,0 +1,157 @@
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 { fileURLToPath } from 'node:url';
14
+ import { dirname, resolve } from 'node:path';
15
+ import * as bindings from './site/vendor/stm32_periph_wasm.js';
16
+ import { createEmulator } from './site/emulator.js';
17
+ import { parseIntelHex, parseElf } from './site/loaders.js';
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const svdXml = readFileSync(resolve(__dirname, 'site/vendor/stm32f407.svd'), 'utf8');
21
+ const wasmBytes = new Uint8Array(readFileSync(resolve(__dirname, 'site/vendor/stm32_periph_wasm_bg.wasm')));
22
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf8'));
23
+
24
+ const USAGE = `stm32f4-emu — run an STM32F4 firmware image in the emulator
25
+
26
+ Usage:
27
+ stm32f4-emu <firmware> [options]
28
+
29
+ Arguments:
30
+ <firmware> path to a firmware image (.bin, .elf, or .hex)
31
+
32
+ Options:
33
+ -n, --inst <N> instruction budget to run (default 20000000)
34
+ -f, --format <fmt> firmware format: auto|bin|hex|elf (default auto)
35
+ -v, --verbose trace guest PCs to stderr (capped per step)
36
+ -l, --lowpower halt on WFI/WFE and advance the virtual RTC until wakeup
37
+ -h, --help show this help
38
+ -V, --version show version
39
+
40
+ Examples:
41
+ stm32f4-emu build/firmware.bin
42
+ stm32f4-emu app.elf --inst 5000000 --verbose
43
+ stm32f4-emu deep_sleep_demo.bin --lowpower
44
+ `;
45
+
46
+ function fail(msg) {
47
+ process.stderr.write(msg + '\n\n' + USAGE);
48
+ process.exit(2);
49
+ }
50
+
51
+ function detectFormat(buf, format) {
52
+ if (format !== 'auto') return format;
53
+ if (buf[0] === 0x7F && buf[1] === 0x45 && buf[2] === 0x4C && buf[3] === 0x46) return 'elf';
54
+ let i = 0;
55
+ while (i < buf.length && (buf[i] === 0x20 || buf[i] === 0x09 || buf[i] === 0x0D || buf[i] === 0x0A)) i++;
56
+ if (buf[i] === 0x3A) return 'hex'; // ':'
57
+ return 'bin';
58
+ }
59
+
60
+ async function main() {
61
+ const args = process.argv.slice(2);
62
+ let firmwarePath = null;
63
+ let inst = 20000000;
64
+ let format = 'auto';
65
+ let verbose = false;
66
+ let lowpower = false;
67
+ for (let i = 0; i < args.length; i++) {
68
+ const a = args[i];
69
+ if (a === '-h' || a === '--help') { process.stdout.write(USAGE); process.exit(0); }
70
+ if (a === '-V' || a === '--version') { process.stdout.write(`stm32f4-emu ${pkg.version}\n`); process.exit(0); }
71
+ if (a === '-v' || a === '--verbose') { verbose = true; continue; }
72
+ if (a === '-l' || a === '--lowpower') { lowpower = true; continue; }
73
+ if (a === '-n' || a === '--inst') {
74
+ const v = args[++i];
75
+ inst = Number(v);
76
+ if (!Number.isFinite(inst) || inst <= 0) fail(`invalid --inst value: ${v}`);
77
+ continue;
78
+ }
79
+ if (a === '-f' || a === '--format') {
80
+ format = args[++i];
81
+ if (!['auto', 'bin', 'hex', 'elf'].includes(format)) fail(`invalid --format: ${format}`);
82
+ continue;
83
+ }
84
+ if (a.startsWith('-')) fail(`unknown option: ${a}`);
85
+ if (firmwarePath) fail('multiple firmware paths given');
86
+ firmwarePath = a;
87
+ }
88
+ if (!firmwarePath) fail('no firmware path given');
89
+
90
+ let raw;
91
+ try {
92
+ raw = new Uint8Array(readFileSync(firmwarePath));
93
+ } catch (e) {
94
+ fail(`cannot read firmware '${firmwarePath}': ${e.message}`);
95
+ }
96
+
97
+ const fmt = detectFormat(raw, format);
98
+ let firmware, extra_mem = [];
99
+ try {
100
+ if (fmt === 'elf') {
101
+ const elf = parseElf(raw);
102
+ if (!elf.flash) fail('ELF contains no FLASH segment (expected a segment at 0x08000000)');
103
+ firmware = elf.flash;
104
+ extra_mem = elf.extraMem || [];
105
+ } else if (fmt === 'hex') {
106
+ const hex = parseIntelHex(Buffer.from(raw).toString('latin1'));
107
+ if (!hex.flash) fail('Intel HEX contains no FLASH records (expected data in the 0x08000000 range)');
108
+ firmware = hex.flash;
109
+ } else {
110
+ firmware = raw;
111
+ }
112
+ } catch (e) {
113
+ fail(`failed to parse ${fmt} firmware: ${e.message}`);
114
+ }
115
+
116
+ let emu;
117
+ try {
118
+ emu = await createEmulator({
119
+ firmware, bindings, svdXml, wasmInit: wasmBytes,
120
+ extra_mem, lowpower,
121
+ });
122
+ } catch (e) {
123
+ fail(`emulator failed to load firmware: ${e.message}`);
124
+ }
125
+
126
+ const STEP = 100000;
127
+ let remaining = inst;
128
+ let lastInst = 0;
129
+ try {
130
+ while (remaining > 0) {
131
+ const take = Math.min(STEP, remaining);
132
+ if (verbose) { try { emu.traceStart(); } catch {} }
133
+ const r = emu.step(take);
134
+ if (verbose) {
135
+ try {
136
+ const pcs = emu.takeTrace() || [];
137
+ const tail = pcs.slice(-8).map((p) => `0x${(p >>> 0).toString(16)}`).join(' ');
138
+ if (tail) process.stderr.write(`[trace]${tail}\n`);
139
+ } catch {} finally { try { emu.traceStop(); } catch {} }
140
+ }
141
+ remaining -= (r.instCount - lastInst);
142
+ lastInst = r.instCount;
143
+ const u = emu.drainUart();
144
+ if (u && u.length) process.stdout.write(u.toString());
145
+ if (r.stopped) break;
146
+ }
147
+ const tail = emu.drainUart();
148
+ if (tail && tail.length) process.stdout.write(tail.toString());
149
+ } finally {
150
+ try { emu.close(); } catch {}
151
+ }
152
+ }
153
+
154
+ main().catch((e) => {
155
+ process.stderr.write(`stm32f4-emu: ${e.message}\n`);
156
+ process.exit(1);
157
+ });
package/index.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ /// <reference types="node" />
2
+
3
+ import { createEmulator, CreateEmulatorOpts, EmulatorHandle } from './site/emulator.js';
4
+
5
+ export * from './site/emulator.js';
6
+
7
+ export interface CreateSTM32F407Opts extends CreateEmulatorOpts {
8
+ /** A Uint8Array, or a key into the bundled FIRMWARES table. */
9
+ firmware: Uint8Array | string;
10
+ }
11
+
12
+ export function createSTM32F407(opts?: CreateSTM32F407Opts): Promise<EmulatorHandle>;
13
+ export function decodeFirmware(key: string): Uint8Array;
14
+ export function createNetSim(opts?: any): any;
15
+
16
+ export const FIRMWARES: Record<string, { bytes: string; [key: string]: unknown }>;
17
+ export const bindings: any;
18
+ export const unicornFactory: any;
19
+ export const svdXml: string;
20
+
21
+ // Component-attachment API (attach devices to an emulator handle).
22
+ export class LED {
23
+ constructor(emu: any, port: string, num: number, opts?: { activeLow?: boolean });
24
+ on(): void;
25
+ off(): void;
26
+ toggle(): void;
27
+ read(): boolean;
28
+ onChange(cb: (on: boolean) => void): void;
29
+ }
30
+ export class Button {
31
+ constructor(emu: any, port: string, num: number, opts?: { activeLow?: boolean });
32
+ press(): void;
33
+ release(): void;
34
+ read(): boolean;
35
+ on(evt: 'down' | 'up', cb: () => void): void;
36
+ }
37
+ export class Pwm {
38
+ constructor(emu: any, timer: string, channel?: number, opts?: { clockHz?: number });
39
+ setDuty(percent: number): void;
40
+ read(): number;
41
+ }
42
+ export class Potentiometer {
43
+ constructor(emu: any, peripheral: string, channel?: number, opts?: { min?: number; max?: number });
44
+ set(value: number): void;
45
+ }
46
+ export class I2cRegisterDevice {
47
+ constructor(emu: any, peripheral: string, opts?: any);
48
+ }
package/index.mjs CHANGED
@@ -1,28 +1,31 @@
1
1
  // stm32f4-emu — Node API entry.
2
2
  // Wraps the browser/Node-universal emulator.js with the bundled assets
3
- // (SVD, wasm bindings, Unicorn) so Node consumers get a one-call setup.
3
+ // (SVD, wasm bindings) so Node consumers get a one-call setup.
4
4
  import { readFileSync } from 'node:fs';
5
- import { createRequire } from 'node:module';
6
5
  import * as bindings from './site/vendor/stm32_periph_wasm.js';
7
6
  import { createEmulator } from './site/emulator.js';
8
7
  import { createNetSim } from './site/netsim.js';
9
8
  import { FIRMWARES } from './site/firmware.js';
10
9
  import { LED, Button, Pwm, I2cRegisterDevice, Potentiometer } from './site/components.js';
10
+ import { STM32F4, GPIOPin, USART, DMAStream } from './site/stm32f4.js';
11
11
 
12
- const require = createRequire(import.meta.url);
13
- const unicornFactory = require('./site/vendor/unicorn_arm.cjs');
12
+ // Node consumers get a one-call setup that injects the bundled assets. The
13
+ // `firmware` option is optional (defer to loadBin/loadHex/loadELF after
14
+ // create), matching the rp2040js / avr8js ergonomics.
15
+ STM32F4.create = (opts = {}) => STM32F4._create({
16
+ bindings, svdXml, wasmInit: wasmBytes, ...opts,
17
+ });
14
18
 
15
19
  const svdXml = readFileSync(new URL('./site/vendor/stm32f407.svd', import.meta.url), 'utf8');
16
20
  const wasmBytes = new Uint8Array(readFileSync(new URL('./site/vendor/stm32_periph_wasm_bg.wasm', import.meta.url)));
17
21
 
18
22
  // Decode a base64-encoded firmware from FIRMWARES.
23
+ // Uses single-pass Uint8Array.from for fewer allocations than manual loop.
19
24
  export function decodeFirmware(key) {
20
25
  const fw = FIRMWARES[key];
21
26
  if (!fw) throw new Error(`unknown firmware '${key}' (have: ${Object.keys(FIRMWARES).join(', ')})`);
22
27
  const bin = atob(fw.bytes);
23
- const out = new Uint8Array(bin.length);
24
- for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
25
- return out;
28
+ return Uint8Array.from(bin, c => c.charCodeAt(0));
26
29
  }
27
30
 
28
31
  // Convenience: create an emulator with the bundled STM32F407 assets.
@@ -33,7 +36,7 @@ export async function createSTM32F407(opts = {}) {
33
36
  const { firmware } = opts;
34
37
  const bin = typeof firmware === 'string' ? decodeFirmware(firmware) : firmware;
35
38
  if (!bin) throw new Error('createSTM32F407 requires `firmware` (Uint8Array or a FIRMWARES key)');
36
- return createEmulator({ ...opts, firmware: bin, bindings, unicorn: unicornFactory, svdXml, wasmInit: wasmBytes });
39
+ return createEmulator({ ...opts, firmware: bin, bindings, svdXml, wasmInit: wasmBytes });
37
40
  }
38
41
 
39
- export { createEmulator, createNetSim, FIRMWARES, bindings, unicornFactory, svdXml, LED, Button, Pwm, I2cRegisterDevice, Potentiometer };
42
+ export { createEmulator, createNetSim, FIRMWARES, bindings, 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,12 +1,15 @@
1
1
  {
2
2
  "name": "stm32f4-emu",
3
- "version": "0.1.0",
4
- "description": "STM32F407 emulator: Unicorn (WASM) CPU + Rust peripheral model. Runs real Cortex-M4 firmware in Node or the browser.",
3
+ "version": "1.1.1",
4
+ "description": "STM32F407 emulator: Rust CPU + peripheral model (WASM). 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
  },
12
+ "types": "index.d.ts",
10
13
  "exports": {
11
14
  ".": "./index.mjs",
12
15
  "./components": "./site/components.js",
@@ -14,22 +17,31 @@
14
17
  "./netsim": "./site/netsim.js",
15
18
  "./firmwares": "./site/firmware.js",
16
19
  "./loaders": "./site/loaders.js",
20
+ "./stm32f4": "./site/stm32f4.js",
17
21
  "./vendor": "./site/vendor/stm32_periph_wasm.js",
18
- "./vendor/unicorn": "./site/vendor/unicorn_arm.cjs",
19
- "./site": "./site/index.html"
22
+ "./remote": "./site/remote-emu.js",
23
+ "./ws-bridge": "./site/ws-bridge.mjs"
20
24
  },
21
25
  "files": [
22
26
  "index.mjs",
27
+ "index.d.ts",
28
+ "cli.mjs",
23
29
  "README.md",
30
+ "LICENSE",
24
31
  "site/emulator.js",
32
+ "site/emulator.d.ts",
33
+ "site/stm32f4.js",
25
34
  "site/components.js",
26
- "mcp/server.mjs",
27
35
  "site/netsim.js",
28
36
  "site/firmware.js",
29
37
  "site/loaders.js",
30
- "site/index.html",
38
+ "site/remote-emu.js",
39
+ "site/ws-bridge.mjs",
31
40
  "site/app.js",
32
- "site/vendor/"
41
+ "site/index.html",
42
+ "site/vendor/",
43
+ "mcp/server.mjs",
44
+ "tools/make_firmware.mjs"
33
45
  ],
34
46
  "keywords": [
35
47
  "stm32",
@@ -37,7 +49,6 @@
37
49
  "emulator",
38
50
  "arm",
39
51
  "cortex-m4",
40
- "unicorn",
41
52
  "wasm",
42
53
  "embedded",
43
54
  "firmware"
@@ -55,29 +66,66 @@
55
66
  "node": ">=20"
56
67
  },
57
68
  "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",
69
+ "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_fpu.mjs && node site/test_fpu_irq.mjs && node site/test_mpu.mjs && node site/test_usb.mjs && node site/test_qspi_cdp.mjs && node site/test_browser.mjs",
70
+ "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",
71
+ "test:bridge": "node site/test_ws_bridge.mjs",
72
+ "test:bridge:browser": "node site/test_bridge_cdp.mjs",
59
73
  "test:flow": "node site/test_flow.mjs",
60
74
  "test:blinky": "node site/test_blinky.mjs",
61
75
  "test:rx": "node site/test_rx_interrupt.mjs",
62
76
  "test:multi": "node site/test_multi_instance.mjs",
77
+ "test:can": "node site/test_candemo.mjs",
78
+ "test:caninject": "node site/test_can_inject.mjs",
79
+ "test:watchdog": "node site/test_watchdog.mjs",
80
+ "test:wwdg": "node site/test_wwdg.mjs",
81
+ "test:wwdgwin": "node site/test_wwdg_window.mjs",
82
+ "test:timcap": "node site/test_tim_capture.mjs",
83
+ "test:lowpower": "node site/test_lowpower.mjs",
84
+ "test:qspi": "node site/test_qspi.mjs",
85
+ "test:fpu": "node site/test_fpu.mjs",
86
+ "test:fpuirq": "node site/test_fpu_irq.mjs",
87
+ "test:mpu": "node site/test_mpu.mjs",
88
+ "test:qspi:browser": "node site/test_qspi_cdp.mjs",
89
+ "test:browser": "node site/test_browser.mjs",
90
+ "test:edge": "node site/test_edge_cases.mjs",
63
91
  "test:fsmc": "node site/test_fsmc.mjs && node site/test_dcmi.mjs && node site/test_fsmc_dcmi.mjs",
64
92
  "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
93
  "test:mcp": "node mcp/test_mcp.mjs",
66
94
  "mcp": "node mcp/server.mjs",
95
+ "bridge": "node site/ws-bridge.mjs",
67
96
  "firmwares": "node tools/make_firmware.mjs",
68
97
  "serve": "python3 -m http.server 8123 --directory site",
69
- "prepack": "node tools/make_firmware.mjs"
98
+ "prepack": "node tools/make_firmware.mjs",
99
+ "typecheck": "tsc --noEmit",
100
+ "lint": "eslint site mcp tools index.mjs cli.mjs",
101
+ "format": "prettier --check .",
102
+ "format:write": "prettier --write .",
103
+ "build:wasm": "wasm-pack build stm32-periph-wasm --target web --out-dir ../site/vendor && rm -f site/vendor/.gitignore && cp monox/stm32f407.svd site/vendor/ 2>/dev/null || true; command -v wasm-opt >/dev/null 2>&1 && wasm-opt -O3 site/vendor/stm32_periph_wasm_bg.wasm -o site/vendor/stm32_periph_wasm_bg.wasm || echo 'wasm-opt not found, skipping'",
104
+ "build:wasm:node": "wasm-pack build stm32-periph-wasm --target nodejs --out-dir pkg",
105
+ "build:wasm:opt": "wasm-opt -O3 site/vendor/stm32_periph_wasm_bg.wasm -o site/vendor/stm32_periph_wasm_bg.wasm",
106
+ "test:wasm": "node site/test_wasm_cpu.mjs && node site/test_flow_wasm.mjs && node site/test_audio_wasm.mjs && node site/test_wasm_multi.mjs && node site/test_lowpower_wasm.mjs && node site/probe_freertos.mjs && node site/test_doom_wasm.mjs"
107
+ },
108
+ "dependencies": {
109
+ "ws": "^8.21.3"
70
110
  },
71
111
  "peerDependencies": {
72
112
  "@modelcontextprotocol/sdk": "^1.30.0",
73
113
  "zod": "^3.25 || ^4.0"
74
114
  },
75
115
  "peerDependenciesMeta": {
76
- "@modelcontextprotocol/sdk": { "optional": true },
77
- "zod": { "optional": true }
116
+ "@modelcontextprotocol/sdk": {
117
+ "optional": true
118
+ },
119
+ "zod": {
120
+ "optional": true
121
+ }
78
122
  },
79
123
  "devDependencies": {
80
124
  "@modelcontextprotocol/sdk": "^1.30.0",
81
- "zod": "^3.25 || ^4.0"
125
+ "playwright": "^1.62.1",
126
+ "zod": "^3.25 || ^4.0",
127
+ "typescript": "^5.6.0",
128
+ "eslint": "^9.0.0",
129
+ "prettier": "^3.0.0"
82
130
  }
83
131
  }