7.1 KiB
EdgeTX Web Simulator
Browser-based EdgeTX radio simulator using WebAssembly + WASI threads.
Prerequisites
- Node.js 20+
- Pre-built
.wasmmodules inpublic/(see Building WASM Modules)
Quick Start
npm install
npm run dev
Open http://localhost:5173 in Chrome, Edge, Safari, or any browser supporting SharedArrayBuffer and Atomics.waitAsync.
Building WASM Modules
The build requires wasi-sdk. The build script resolves it automatically:
$WASI_SDK_PATHenvironment variable (if set)/opt/wasi-sdk/(default install path, used in CI)- Auto-download via
cmake/FetchWasiSDK.cmake(fetched once, cached across builds)
From the EdgeTX repository root:
# Build all supported radios
tools/build-wasm-modules.sh
# Build specific radios only
FLAVOR="tx16s;t12;x9dp2019" tools/build-wasm-modules.sh
Output .wasm files are written to output/. Copy them to web/public/:
cp output/*.wasm web/public/
Supported Radios
The radios available in the web UI are defined in public/radios.json. This file is generated from the authoritative hardware definitions in radio/src/boards/hw_defs/. To regenerate it (e.g. after adding a new radio target):
node web/scripts/gen-radios-json.js
The script extracts inputs, switches, trims, keys, and display info from the hw_defs JSON files. Key left/right side placement matches Companion's layout.
Architecture
Browser Main Thread
┌─────────────────────────────────────────────────────┐
│ │
│ App.svelte ──── WasmRunner ──── LcdRenderer │
│ (UI, controls) (loader) (WebGL canvas) │
│ │ │ │
│ │ SharedArrayBuffer │
│ │ (analogs, LCD sync) │
│ │ │ │
│ ▼ ▼ │
│ AudioContext WASM Worker Threads (WASI) │
│ (scheduled ┌──────────────────────────┐ │
│ playback) │ worker.ts + FsProxyClient│ │
│ └────────────┬─────────────┘ │
│ SAB+Atomics (sync I/O) │
│ │ │
│ ┌────────────▼─────────────┐ │
│ │ FS Worker (fs-worker.ts) │ │
│ │ OpfsBackend (OPFS) │ │
│ └──────────────────────────┘ │
└─────────────────────────────────────────────────────┘
Lifecycle
- Radio selection — user picks a radio from the dropdown (persisted to localStorage). The FS Worker is spawned and OPFS is scanned for existing data. File uploads are available immediately.
- Run — WASM module is fetched, compiled, and instantiated. Worker threads are spawned with shared memory. The FS Worker's Atomics loop is started. The simulator begins running.
- Stop — firmware shutdown is signalled, worker threads are terminated after a grace period. The FS Worker stays alive for uploads.
- Radio switch — the old instance is torn down (FS Worker included), and a new one is initialized for the selected radio.
Key Components
- WasmRunner — loads WASM, creates shared memory, manages the FS Worker and WASI thread pool. Exposes
initFs()(spawn FS Worker),load()(compile + instantiate WASM),stopSim()(terminate threads), andstopFs()(terminate FS Worker). - FS Worker (
fs-worker.ts) — dedicated worker that owns all OPFS state. Serves synchronous filesystem requests from WASM workers viaSharedArrayBuffer+Atomics, and async UI requests (uploads, reads, wipe) viapostMessage. - OpfsBackend (
opfs-backend.ts) — in-memory directory tree backed by OPFSSyncAccessHandles. Provides a synchronous Node.js-like filesystem API. - FsProxyClient (
fs-proxy-client.ts) — worker-side stub that implementsfs.*Syncmethods by writing requests to a shared buffer and blocking onAtomics.waituntil the FS Worker responds. - LcdRenderer (
lcd-renderer.ts) — WebGL-based renderer supporting RGB565 (16-bit color), 4-bit grayscale, and 1-bit monochrome (column-major) LCD formats. - Audio — worker threads relay PCM samples via
postMessage. The main thread schedules them asAudioBufferSourcenodes for gapless 32 kHz playback.
Key Files
| File | Description |
|---|---|
src/App.svelte |
Main UI: radio selector, LCD display, controls, file management |
src/lib/wasm-runner.ts |
WASM loader, FS Worker lifecycle, thread management |
src/lib/worker.ts |
WASM worker thread entry point (WASI + thread init) |
src/lib/fs-worker.ts |
FS Worker: OPFS owner, Atomics dispatch loop, UI file ops |
src/lib/opfs-backend.ts |
OPFS-backed synchronous filesystem implementation |
src/lib/fs-proxy-client.ts |
Worker-side blocking filesystem proxy |
src/lib/fs-proxy-protocol.ts |
Shared protocol constants and serialization |
src/lib/lcd-renderer.ts |
LCD framebuffer rendering (RGB565, 4-bit grayscale, 1-bit mono) |
public/radios.json |
Radio definitions (generated — do not edit manually) |
public/_headers |
COOP/COEP headers for production deployment |
scripts/gen-radios-json.js |
Generates radios.json from radio/src/boards/hw_defs/ |
Browser Requirements
- SharedArrayBuffer (requires COOP/COEP headers, configured in
vite.config.tsandpublic/_headers) - Atomics.waitAsync — Chrome 87+, Safari 16.4+, Edge 87+, Firefox 145+
- WebAssembly threads (shared memory)
- Origin Private File System (persistent storage across sessions)
Development
npm run dev # Start dev server with HMR
npm run build # Production build to dist/
npm run preview # Preview production build
Debugging
- Filesystem tracing: Open browser console and run
fsTrace = trueto log all filesystem operations. - Trace window: The simulator UI includes a scrollable trace output showing firmware boot messages and runtime output.
Production Deployment
The dev server sets the required COOP/COEP headers automatically. For production, public/_headers provides them for platforms like Cloudflare Pages. For other hosts, configure your web server to set:
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
Cloudflare Pages
npm run build
npx wrangler pages deploy dist --project-name=edgetx-simulator