The control app¶
Control is a frontend-only
control app with no backend: the browser talks straight to the Propeller 2
over the Web Serial API, and all the protocol logic runs as WebAssembly
compiled from the same Rust core (Protocol/ProtoEmb/runtime) used by the
firmware tooling and the SIL rig. It lives in Software/Control/.
Architecture¶
flowchart TB
subgraph main["Main thread"]
ui["React UI (Vite)<br/>uPlot live charts"]
store["Zustand store +<br/>out-of-React liveBuffer"]
port["navigator.serial<br/>port.open() (user gesture)"]
ui --- store
end
subgraph worker["Web Worker — DeviceSession"]
wasm["protoemb-wasm<br/>framing + queue + storage + client"]
end
fsa["File System Access API<br/>(data folder)"]
ui -- Comlink --> wasm
port -- "readable/writable<br/>(transferred)" --> wasm
wasm -- "events → store" --> store
store --- fsa
wasm <-->|"serial bytes"| device["Machine / SIL"]
The serial read loop and all protocol work run in a dedicated Web Worker, so the ~100 Hz sample stream never blocks rendering. The port is opened on the main thread (Web Serial requires a user gesture) and its readable/writable streams are transferred to the worker.
Source layout¶
| Path | What |
|---|---|
src/protocol/generated/ |
Generated TypeScript codec (browser-safe Uint8Array/DataView) |
src/wasm/ |
wasm-pack output of the Rust protocol client |
src/domain/ |
Pure logic (G-code build/transform, sample→CSV, proto↔display mapping) |
src/device/ |
DeviceSession.worker.ts (worker) + session.ts (main-thread client) |
src/storage/ |
DataStore over the File System Access API |
src/store/ |
Zustand store + out-of-React liveBuffer |
src/ui/ |
Screens + uPlot live charts |
src/wasm/ and src/protocol/generated/ are git-ignored build artifacts —
they're produced by build:wasm and generate:proto (see
running the app).
Reliability by design¶
The app is a monitor/controller, not a safety device (see the safety model). Around that principle it adds:
- Move validation before any motion is sent — out-of-range X/F/P and unknown G-codes are rejected on the wire (the packed codec would otherwise silently bit-wrap an over-range value).
- Crash recovery — React error boundaries (top-level + per-screen) and a fresh worker per connection, so a render throw or a panicked WASM instance can't blank the app or carry over.
- Resilient teardown / reconnect — stream death is detected in the worker; the app surfaces a Reconnect and auto-reconnects on replug.
- Durable storage — all mutating data-folder operations are serialised through
one mutex, and
index.jsonis a rebuildable cache. - Deferred PWA updates — a new version is applied only when idle and disconnected, never mid-test.
For the full hardening write-up see HARDENING.md.
Performance¶
A few choices keep the UI smooth at 100 Hz: an O(1) ring buffer for live
samples (no per-sample memmove), idle-redraw gating on the charts (no canvas
work without new data), zero-copy Uint8Array marshalling across the WASM
boundary, and route code-splitting for a lean initial bundle.
Not included¶
- Firmware flashing — the native
loadp2bootloader can't run in a browser; use the desktop tooling. - Non-Chromium browsers — Web Serial / File System Access are Chromium-only.