twin.ts
@file Runs the compiled woz_uwb digital twin inside this Worker.
Overview
@file Runs the compiled woz_uwb digital twin inside this Worker.
web-twin/twin.js embeds its WASM as a decoded byte string and instantiates
it at runtime with WebAssembly.instantiate(bytes, imports) — the one path
workerd refuses ("Wasm code generation disallowed by embedder", proven
directly against wrangler dev; see docs/twin-worker-phase0.md). twin.js
itself is never edited: Emscripten's Module["instantiateWasm"] hook lets
this file hand it a build-time-precompiled WebAssembly.Module instead
(imported as a static .wasm module, the one WASM path workerd does
allow), so twin.js's own embedded-bytes loader is never reached.
twin.wasm is extracted once by scripts/twin-wasm-extract.ts and drift-
checked against web-twin/twin.js by test/twin-wasm-drift.test.ts on every
run — a rebuilt twin.js with no matching re-extraction fails that gate
rather than silently running stale firmware.
The .wasm import below is deliberately dynamic, not a static top-level
import: Node's own module loader treats a static import x from "*.wasm"
as a native WASM-ES-module and tries to resolve twin.wasm's own imports
(wasi_snapshot_preview1) as JS packages, which crashes immediately under
plain node --test (this repo's own test runner) — a real regression
caught by running the full bot suite, not something wrangler's bundler
does. A dynamic import() is resolved lazily, only in the branch that
actually calls it, so Node never touches the WASM path at all.
depends on citations.ts · used by twin.ts
API
Fasync function loadTwinWasmModule(): Promise<WebAssembly.Module>
Under Node (tests, scripts/twin-wasm-extract.ts's own consumers, this
repo's node --test runner): read the same bytes from disk and compile
them the ordinary way — Node allows runtime WASM codegen; only workerd
refuses it (docs/twin-worker-phase0.md).
Under workerd: import("./twin.wasm") must stay a literal,
statically-analyzable specifier so wrangler's bundler compiles it into a
WebAssembly.Module ahead of time — the one WASM path workerd allows.
bootTwinFexport async function bootTwin(): Promise<TwinHandle>
Boot one twin instance. Each call is a fresh WASM instance (a firmware reboot).
handleApproach · calls cmOrNull, loadTwinWasmModuleFfunction applyNoise(cm: number, noise: NoiseLevel, rand: () => number): number
A deterministic bench-style noise model, not a firmware constant: SIM, matching the calibration web-twin/index.html's own noise checkbox uses (index.html:833-848 — "bench-like spikes", jitter ~+-6cm, ~6% chance of a ~1500-1600mm spike, cited there against app_main.cpp:237-238's observed bench swing). "heavy" scales both knobs; there is no firmware source for that scaling, it is this command's own choice of a rougher bench.
runApproachScenarioFfunction makeRng(seed: number): () => number
xorshift32 — deterministic so a scenario's ASCII diagram/PNG can be reproduced from the same options without storing the whole round list.
runApproachScenarioUndocumented (6)
cmOrNull, instantiateWasm, ScenarioTooLong, ScenarioTooLong.constructor, runApproachScenario, explainDecision