mirror of
https://github.com/ruvnet/RuView.git
synced 2026-04-28 14:09:33 +00:00
The Rust port lived two directories deep (rust-port/wifi-densepose-rs/) without any sibling under rust-port/ that warranted the extra level. Move the whole workspace up to v2/ to match v1/ (Python) at the same depth and shorten every cd / build command across the repo. git mv preserves history for all tracked files. 60 files updated for path references (CI workflows, ADRs, docs, scripts, READMEs, internal .claude-flow state). Two manual fixes for relative-cd paths in CLAUDE.md and ADR-043 that became wrong after the depth change (cd ../.. → cd ..). Validated: - cargo check --workspace --no-default-features → clean (after target/ nuke; the gitignored target/ was carried by the OS rename and had hard-coded old paths in build scripts) - cargo test --workspace --no-default-features → 1,539 passed, 0 failed, 8 ignored (same totals as pre-rename) - ESP32-S3 on COM7 → still streaming live CSI (cb #40300, RSSI -64 dBm) After-merge follow-up: contributors should `rm -rf v2/target` once and let cargo regenerate from the new path.
582 lines
26 KiB
Markdown
582 lines
26 KiB
Markdown
# ADR-040: WASM Programmable Sensing (Tier 3)
|
||
|
||
**Status**: Accepted
|
||
**Date**: 2026-03-02
|
||
**Deciders**: @ruvnet
|
||
|
||
## Context
|
||
|
||
ADR-039 implemented Tiers 0-2 of the ESP32-S3 edge intelligence pipeline:
|
||
- **Tier 0**: Raw CSI passthrough (magic `0xC5110001`)
|
||
- **Tier 1**: Basic DSP — phase unwrap, Welford stats, top-K, delta compression
|
||
- **Tier 2**: Full pipeline — vitals, presence, fall detection, multi-person
|
||
|
||
The firmware uses ~820 KB of flash, leaving ~80 KB headroom in the 1 MB OTA partition. The ESP32-S3 has 8 MB PSRAM available for runtime data. New sensing algorithms (gesture recognition, signal coherence monitoring, adversarial detection) currently require a full firmware reflash — impractical for deployed sensor networks.
|
||
|
||
The project already has 35+ RuVector WASM crates and 28 pre-built `.wasm` binaries, but none are integrated into the ESP32 firmware.
|
||
|
||
## Decision
|
||
|
||
Add a **Tier 3 WASM programmable sensing layer** that executes hot-loadable algorithms compiled from Rust to `wasm32-unknown-unknown`, interpreted on-device via the WASM3 runtime.
|
||
|
||
### Architecture
|
||
|
||
```
|
||
Core 1 (DSP Task)
|
||
┌──────────────────────────────────────────────────┐
|
||
│ Tier 2 Pipeline (existing) │
|
||
│ Phase extract → Welford → Top-K → Biquad → │
|
||
│ BPM → Presence → Fall → Multi-person │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────┐ │
|
||
│ │ Tier 3 WASM Runtime (new) │ │
|
||
│ │ WASM3 Interpreter (MIT, ~100 KB flash) │ │
|
||
│ │ ┌────────────┐ ┌────────────┐ │ │
|
||
│ │ │ Module 0 │ │ Module 1 │ ...×4 │ │
|
||
│ │ │ gesture.wm │ │ coherence │ │ │
|
||
│ │ └─────┬──────┘ └─────┬──────┘ │ │
|
||
│ │ │ │ │ │
|
||
│ │ Host API ("csi" namespace) │ │
|
||
│ │ csi_get_phase, csi_get_amplitude, ... │ │
|
||
│ └──────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ UDP output (0xC5110004) │
|
||
└──────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Components
|
||
|
||
| Component | File | Description |
|
||
|-----------|------|-------------|
|
||
| WASM3 component | `components/wasm3/CMakeLists.txt` | ESP-IDF managed component, fetches WASM3 from GitHub |
|
||
| Runtime host | `main/wasm_runtime.c/h` | WASM3 environment, module slots, host API bindings |
|
||
| HTTP upload | `main/wasm_upload.c/h` | REST endpoints for module management on port 8032 |
|
||
| Rust WASM crate | `wifi-densepose-wasm-edge/` | `no_std` sensing algorithms compiled to WASM |
|
||
|
||
### Host API (namespace "csi")
|
||
|
||
| Import | Signature | Description |
|
||
|--------|-----------|-------------|
|
||
| `csi_get_phase` | `(i32) -> f32` | Current phase for subcarrier index |
|
||
| `csi_get_amplitude` | `(i32) -> f32` | Current amplitude |
|
||
| `csi_get_variance` | `(i32) -> f32` | Welford running variance |
|
||
| `csi_get_bpm_breathing` | `() -> f32` | Breathing BPM from Tier 2 |
|
||
| `csi_get_bpm_heartrate` | `() -> f32` | Heart rate BPM from Tier 2 |
|
||
| `csi_get_presence` | `() -> i32` | Presence flag (0/1) |
|
||
| `csi_get_motion_energy` | `() -> f32` | Motion energy scalar |
|
||
| `csi_get_n_persons` | `() -> i32` | Detected person count |
|
||
| `csi_get_timestamp` | `() -> i32` | Milliseconds since boot |
|
||
| `csi_emit_event` | `(i32, f32) -> void` | Emit custom event to host |
|
||
| `csi_log` | `(i32, i32) -> void` | Debug log from WASM memory |
|
||
| `csi_get_phase_history` | `(i32, i32) -> i32` | Copy phase history ring buffer |
|
||
|
||
### Module Lifecycle
|
||
|
||
| Export | Called | Description |
|
||
|--------|--------|-------------|
|
||
| `on_init()` | Once, when module starts | Initialize module state |
|
||
| `on_frame(n_sc: i32)` | Per CSI frame (~20 Hz) | Process current frame |
|
||
| `on_timer()` | At configurable interval | Periodic tasks |
|
||
|
||
### Wire Protocol (magic `0xC5110004`)
|
||
|
||
| Offset | Type | Field |
|
||
|--------|------|-------|
|
||
| 0-3 | u32 LE | Magic `0xC5110004` |
|
||
| 4 | u8 | Node ID |
|
||
| 5 | u8 | Module ID (slot index) |
|
||
| 6-7 | u16 LE | Event count |
|
||
| 8+ | Event[] | Array of (u8 type, f32 value) tuples |
|
||
|
||
### HTTP Endpoints (port 8032)
|
||
|
||
| Method | Path | Description |
|
||
|--------|------|-------------|
|
||
| `POST` | `/wasm/upload` | Upload .wasm binary (max 128 KB) |
|
||
| `GET` | `/wasm/list` | List loaded modules with status |
|
||
| `POST` | `/wasm/start/:id` | Start a module |
|
||
| `POST` | `/wasm/stop/:id` | Stop a module |
|
||
| `DELETE` | `/wasm/:id` | Unload a module |
|
||
|
||
### WASM Crate Modules
|
||
|
||
| Module | Source | Events | Description |
|
||
|--------|--------|--------|-------------|
|
||
| `gesture.rs` | `ruvsense/gesture.rs` | 1 (Core) | DTW template matching for gesture recognition |
|
||
| `coherence.rs` | `ruvector/viewpoint/coherence.rs` | 2 (Core) | Phase phasor coherence monitoring |
|
||
| `adversarial.rs` | `ruvsense/adversarial.rs` | 3 (Core) | Signal anomaly/adversarial detection |
|
||
| `vital_trend.rs` | ADR-041 Phase 1 | 100-111 (Medical) | Clinical vital sign trend analysis (bradypnea, tachypnea, bradycardia, tachycardia, apnea) |
|
||
| `occupancy.rs` | ADR-041 Phase 1 | 300-302 (Building) | Spatial occupancy zone detection with per-zone variance analysis |
|
||
| `intrusion.rs` | ADR-041 Phase 1 | 200-203 (Security) | State-machine intrusion detector (calibrate-monitor-arm-alert) |
|
||
|
||
### Memory Budget
|
||
|
||
| Component | SRAM | PSRAM | Flash |
|
||
|-----------|------|-------|-------|
|
||
| WASM3 interpreter | ~10 KB | — | ~100 KB |
|
||
| WASM module storage (×4) | — | 512 KB | — |
|
||
| WASM execution stack | 8 KB | — | — |
|
||
| Host API bindings | 2 KB | — | ~15 KB |
|
||
| HTTP upload handler | 1 KB | — | ~8 KB |
|
||
| RVF parser + verifier | 1 KB | — | ~6 KB |
|
||
| **Total Tier 3** | **~22 KB** | **512 KB** | **~129 KB** |
|
||
| **Running total (Tier 0-3)** | **~34 KB** | **512 KB** | **~925 KB** |
|
||
|
||
**Measured binary size**: 925 KB (0xE7440 bytes), 10% free in 1 MB OTA partition.
|
||
|
||
### NVS Configuration
|
||
|
||
| Key | Type | Default | Description |
|
||
|-----|------|---------|-------------|
|
||
| `wasm_max` | u8 | 4 | Maximum concurrent WASM modules |
|
||
| `wasm_verify` | u8 | 1 | Require signature verification (secure-by-default) |
|
||
| `wasm_pubkey` | blob(32) | — | Signing public key for WASM verification |
|
||
|
||
## Consequences
|
||
|
||
### Positive
|
||
- Deploy new sensing algorithms to 1000+ nodes without reflashing firmware
|
||
- 20-year extensibility horizon — new algorithms via .wasm uploads
|
||
- Algorithms developed/tested in Rust, compiled to portable WASM
|
||
- PSRAM utilization (previously unused 8 MB) for module storage
|
||
- Hot-swap algorithms for A/B testing in production deployments
|
||
- Same `no_std` Rust code runs on ESP32 (WASM3) and in browser (wasm-pack)
|
||
|
||
### Negative
|
||
- WASM3 interpreter overhead: ~10× slower than native C for compute-heavy code
|
||
- Adds ~123 KB flash footprint (firmware approaches 950 KB of 1 MB limit)
|
||
- Additional attack surface via WASM module upload endpoint
|
||
- Debugging WASM modules on ESP32 is harder than native C
|
||
|
||
### Risks
|
||
|
||
| Risk | Mitigation |
|
||
|------|------------|
|
||
| WASM3 memory management may fragment PSRAM over time | Fixed 160 KB arenas pre-allocated at boot per slot — no runtime malloc/free cycles |
|
||
| Complex WASM modules (>64 KB) may cause stack overflow in interpreter | `WASM_STACK_SIZE` = 8 KB, `d_m3MaxFunctionStackHeight` = 128; modules validated at load time |
|
||
| HTTP upload endpoint requires network security | Ed25519 signature verification enabled by default (`wasm_verify=1`); disable only via NVS for lab/dev |
|
||
| Runaway WASM module blocks DSP pipeline | Per-frame budget guard (10 ms default); module auto-stopped after 10 consecutive faults |
|
||
| Denial-of-service via rapid upload/unload cycles | Max 4 concurrent slots; upload handler validates size before PSRAM copy |
|
||
|
||
## Implementation
|
||
|
||
- `firmware/esp32-csi-node/components/wasm3/CMakeLists.txt` — WASM3 ESP-IDF component
|
||
- `firmware/esp32-csi-node/main/wasm_runtime.c/h` — Runtime host with 12 API bindings + manifest
|
||
- `firmware/esp32-csi-node/main/wasm_upload.c/h` — HTTP REST endpoints (RVF-aware)
|
||
- `firmware/esp32-csi-node/main/rvf_parser.c/h` — RVF container parser and verifier
|
||
- `v2/.../wifi-densepose-wasm-edge/` — Rust WASM crate (gesture, coherence, adversarial, rvf, occupancy, vital_trend, intrusion)
|
||
- `v2/.../wifi-densepose-sensing-server/src/main.rs` — `0xC5110004` parser
|
||
- `docs/adr/ADR-039-esp32-edge-intelligence.md` — Updated with Tier 3 reference
|
||
|
||
---
|
||
|
||
## Appendix A: Production Hardening
|
||
|
||
The initial Tier 3 implementation addresses five production-readiness concerns:
|
||
|
||
### A.1 Fixed PSRAM Arenas
|
||
|
||
Dynamic `heap_caps_malloc` / `free` cycles on PSRAM fragment memory over days of
|
||
continuous operation. Instead, each module slot pre-allocates a **160 KB fixed arena**
|
||
at boot (`WASM_ARENA_SIZE`). The WASM binary and WASM3 runtime heap both live inside
|
||
this arena. Unloading a module zeroes the arena but never frees it — the slot is
|
||
reused on the next `wasm_runtime_load()`.
|
||
|
||
```
|
||
Boot: [arena0: 160 KB][arena1: 160 KB][arena2: 160 KB][arena3: 160 KB]
|
||
Total: 640 KB PSRAM
|
||
Load: [module0 binary | wasm3 heap | ...padding... ]
|
||
Unload:[zeroed .......................................] ← slot reusable
|
||
```
|
||
|
||
This eliminates fragmentation at the cost of reserving 640 KB PSRAM at boot
|
||
(8% of 8 MB). The remaining 7.36 MB is available for future use.
|
||
|
||
### A.2 Per-Frame Budget Guard
|
||
|
||
Each `on_frame()` call is measured with `esp_timer_get_time()`. If execution
|
||
exceeds `WASM_FRAME_BUDGET_US` (default 10 ms = 10,000 us), a budget fault is
|
||
recorded. After **10 consecutive faults**, the module is auto-stopped with
|
||
`WASM_MODULE_ERROR` state. This prevents a runaway WASM module from blocking the
|
||
Tier 2 DSP pipeline.
|
||
|
||
```c
|
||
int64_t t_start = esp_timer_get_time();
|
||
m3_CallV(slot->fn_on_frame, n_sc);
|
||
uint32_t elapsed_us = (uint32_t)(esp_timer_get_time() - t_start);
|
||
|
||
slot->total_us += elapsed_us;
|
||
if (elapsed_us > slot->max_us) slot->max_us = elapsed_us;
|
||
|
||
if (elapsed_us > WASM_FRAME_BUDGET_US) {
|
||
slot->budget_faults++;
|
||
if (slot->budget_faults >= 10) {
|
||
slot->state = WASM_MODULE_ERROR; // auto-stop
|
||
}
|
||
}
|
||
```
|
||
|
||
The budget is configurable via `WASM_FRAME_BUDGET_US` (Kconfig or NVS override).
|
||
|
||
### A.3 Per-Module Telemetry
|
||
|
||
The `/wasm/list` endpoint and `wasm_module_info_t` struct expose per-module
|
||
telemetry:
|
||
|
||
| Field | Type | Description |
|
||
|-------|------|-------------|
|
||
| `frame_count` | u32 | Total on_frame calls since start |
|
||
| `event_count` | u32 | Total csi_emit_event calls |
|
||
| `error_count` | u32 | WASM3 runtime errors |
|
||
| `total_us` | u32 | Cumulative execution time (microseconds) |
|
||
| `max_us` | u32 | Worst-case single frame execution time |
|
||
| `budget_faults` | u32 | Times frame budget was exceeded |
|
||
|
||
Mean execution time = `total_us / frame_count`. This enables remote monitoring
|
||
of module health and performance regression detection.
|
||
|
||
### A.4 Secure-by-Default
|
||
|
||
`wasm_verify` defaults to **1** in both Kconfig and the NVS fallback path.
|
||
Uploaded `.wasm` binaries must include a valid Ed25519 signature (same key as
|
||
OTA firmware). Disable only for lab/dev use via:
|
||
|
||
```bash
|
||
python provision.py --port COM7 --wasm-verify # NVS: wasm_verify=1 (default)
|
||
# To disable in dev: write wasm_verify=0 to NVS directly
|
||
```
|
||
|
||
---
|
||
|
||
## Appendix B: Adaptive Budget Architecture (Mincut-Driven)
|
||
|
||
### B.1 Design Principle
|
||
|
||
One control loop turns **sensing into a bounded compute budget**, spends that
|
||
budget on **sparse or spiking inference**, and exports **only deltas**. The
|
||
budget is driven by the **mincut eigenvalue gap** (Δλ = λ₂ − λ₁ of the CSI
|
||
graph Laplacian), which reflects scene complexity: a quiet room has Δλ ≈ 0,
|
||
a busy room has large Δλ.
|
||
|
||
### B.2 Control Loop
|
||
|
||
```
|
||
┌─────────────────────────────────┐
|
||
CSI frames ───→ │ Tier 2 DSP (existing) │
|
||
│ Welford stats, top-K, presence │
|
||
└──────────┬────────────────────────┘
|
||
│
|
||
┌──────────────▼──────────────────────┐
|
||
│ Budget Controller │
|
||
│ │
|
||
│ Inputs: │
|
||
│ Δλ = mincut eigenvalue gap │
|
||
│ A = anomaly_score (adversarial) │
|
||
│ T = thermal_pressure (0.0-1.0) │
|
||
│ P = battery_pressure (0.0-1.0) │
|
||
│ │
|
||
│ Output: │
|
||
│ B = frame compute budget (μs) │
|
||
│ │
|
||
│ B = clamp(B₀ + k₁·max(0,Δλ) │
|
||
│ + k₂·A │
|
||
│ − k₃·T │
|
||
│ − k₄·P, │
|
||
│ B_min, B_max) │
|
||
└──────────────┬──────────────────────┘
|
||
│
|
||
┌──────────────▼──────────────────────┐
|
||
│ WASM Module Dispatch │
|
||
│ Budget B split across active modules│
|
||
│ Each module gets B/N μs per frame │
|
||
└──────────────┬──────────────────────┘
|
||
│
|
||
┌──────────────▼──────────────────────┐
|
||
│ Delta Export │
|
||
│ Only emit events when Δ > threshold │
|
||
│ Quiet room → near-zero UDP traffic │
|
||
└─────────────────────────────────────┘
|
||
```
|
||
|
||
### B.3 Budget Formula
|
||
|
||
```
|
||
B = clamp(B₀ + k₁·max(0, Δλ) + k₂·A − k₃·T − k₄·P, B_min, B_max)
|
||
```
|
||
|
||
| Symbol | Default | Description |
|
||
|--------|---------|-------------|
|
||
| B₀ | 5,000 μs | Base budget (5 ms) |
|
||
| k₁ | 2,000 | Δλ sensitivity (more scene change → more budget) |
|
||
| k₂ | 3,000 | Anomaly boost (detected anomaly → more compute) |
|
||
| k₃ | 4,000 | Thermal penalty (chip hot → less compute) |
|
||
| k₄ | 3,000 | Battery penalty (low SoC → less compute) |
|
||
| B_min | 1,000 μs | Floor: always run at least 1 ms |
|
||
| B_max | 15,000 μs | Ceiling: never exceed 15 ms |
|
||
|
||
### B.4 Where Δλ Comes From
|
||
|
||
The mincut graph is the **top-K subcarrier correlation graph** already
|
||
maintained by Tier 1/2 DSP. Subcarriers are nodes; edge weights are
|
||
pairwise Pearson correlation magnitudes over the Welford window. The
|
||
algebraic connectivity (Fiedler value λ₂) of this graph's Laplacian
|
||
approximates the mincut value. On ESP32-S3 with K=8 subcarriers, this
|
||
is an 8×8 eigenvalue problem — solvable with power iteration in <100 μs.
|
||
|
||
### B.5 Spiking and Sparse Optimizations
|
||
|
||
When the budget is tight (Δλ ≈ 0, quiet room), WASM modules should:
|
||
|
||
1. **Skip on_frame entirely** if Δλ < ε (no scene change → no computation)
|
||
2. **Sparse inference**: Only process the top-K subcarriers that changed
|
||
(already tracked by Tier 1 delta compression)
|
||
3. **Spiking semantics**: Modules emit events only when state transitions
|
||
occur, not on every frame. The host tracks a per-module "last emitted"
|
||
state and suppresses duplicate events.
|
||
|
||
### B.6 Thermal and Power Hooks
|
||
|
||
ESP32-S3 provides:
|
||
- `temp_sensor_read()` — on-chip temperature (°C)
|
||
- ADC reading of battery voltage (if wired)
|
||
|
||
Thermal pressure: `T = clamp((temp_celsius - 60) / 20, 0, 1)` — ramps
|
||
from 0 at 60°C to 1.0 at 80°C (thermal throttle zone).
|
||
|
||
Battery pressure: `P = clamp((3.3 - battery_volts) / 0.6, 0, 1)` — ramps
|
||
from 0 at 3.3V to 1.0 at 2.7V (brownout zone).
|
||
|
||
### B.7 Transport Strategy
|
||
|
||
WASM output packets (`0xC5110004`) adopt **delta-only export**:
|
||
|
||
- Events are only emitted when the value changes by more than a
|
||
configurable dead-band (default: 5% of previous value)
|
||
- Quiet room = zero WASM UDP packets (only Tier 2 vitals at 1 Hz)
|
||
- Busy room = bursty WASM events, naturally rate-limited by budget B
|
||
|
||
Future work: QUIC-lite transport with 0-RTT connection resumption and
|
||
congestion-aware pacing, replacing raw UDP for WASM event streams.
|
||
|
||
---
|
||
|
||
## Appendix C: Hardware Benchmark (RuView ESP32-S3)
|
||
|
||
Measured on ESP32-S3 (QFN56 rev v0.2, 8 MB flash, 160 MHz, ESP-IDF v5.2,
|
||
board without PSRAM). WiFi connected to AP at RSSI -25 dBm, channel 5 BW20.
|
||
|
||
### WASM Runtime Performance
|
||
|
||
| Metric | Value |
|
||
|--------|-------|
|
||
| WASM runtime init | **106 ms** |
|
||
| Total boot to ready | **3.9 s** (including WiFi connect) |
|
||
| Module slots | 4 × 160 KB (heap fallback, no PSRAM) |
|
||
| WASM binary size (7 modules) | **13.8 KB** (wasm32-unknown-unknown release) |
|
||
| Frame budget | 10,000 µs (10 ms) |
|
||
| Timer interval | 1,000 ms (1 Hz) |
|
||
|
||
### CSI Throughput
|
||
|
||
| Metric | Value |
|
||
|--------|-------|
|
||
| Frame rate | **28.5 Hz** (exceeds 20 Hz estimate) |
|
||
| Frame sizes | 128 / 256 bytes |
|
||
| Per-frame interval | 30.6 ms avg |
|
||
| RSSI range | -83 to -32 dBm (mean -62 dBm) |
|
||
|
||
### Rust Test Results
|
||
|
||
| Crate | Tests | Status |
|
||
|-------|-------|--------|
|
||
| wifi-densepose-wasm-edge (std) | 14 | All pass, 0 warnings |
|
||
| Full workspace | 1,411 | All pass, 0 failed |
|
||
|
||
### Known Issues
|
||
|
||
1. **Fall threshold too sensitive** — default 2.0 rad/s² produces 6.7 false positives/s in static environment. Recommend 5.0-8.0 for deployment.
|
||
2. **No PSRAM on test board** — WASM arenas fall back to internal heap (316 KiB total). Production boards with 8 MB PSRAM will use dedicated PSRAM arenas.
|
||
3. **WiFi-Ethernet isolation** — some consumer routers block bridging between WiFi and wired clients. Verify network path during deployment.
|
||
|
||
### B.8 Implementation Plan
|
||
|
||
| Step | Scope | Effort |
|
||
|------|-------|--------|
|
||
| 1 | Add `edge_compute_fiedler()` in `edge_processing.c` — power iteration on 8×8 Laplacian | ~50 lines C |
|
||
| 2 | Add budget controller struct and update formula in `wasm_runtime.c` | ~30 lines C |
|
||
| 3 | Wire thermal/battery sensors into budget inputs | ~20 lines C |
|
||
| 4 | Add delta-export dead-band filter in `wasm_runtime_on_frame()` | ~15 lines C |
|
||
| 5 | NVS keys for k₁-k₄, B_min, B_max, dead-band threshold | ~10 lines C |
|
||
|
||
Total: ~125 lines of C, no new files. All constants configurable via NVS.
|
||
|
||
### B.9 Failure Modes
|
||
|
||
| Failure | Behavior |
|
||
|---------|----------|
|
||
| Δλ estimate wrong (correlation noise) | Budget oscillates — clamped by B_min/B_max |
|
||
| Thermal sensor absent | T defaults to 0 (no throttle) |
|
||
| Battery ADC not wired | P defaults to 0 (always-on mode) |
|
||
| All WASM modules budget-faulted | DSP pipeline runs Tier 2 only — graceful degradation |
|
||
|
||
---
|
||
|
||
## Appendix C: RVF Container Format
|
||
|
||
### C.1 Problem
|
||
|
||
Raw `.wasm` uploads over HTTP are remote code execution. Signatures solve
|
||
authenticity, but without a manifest the host has no way to enforce budgets,
|
||
check API compatibility, or identify what it's running. RVF wraps the WASM
|
||
payload with governance metadata in a single artifact.
|
||
|
||
### C.2 Binary Layout
|
||
|
||
```
|
||
Offset Size Type Field
|
||
────────────────────────────────────────────
|
||
0 4 [u8;4] Magic "RVF\x01" (0x01465652 LE)
|
||
4 2 u16 LE format_version (1)
|
||
6 2 u16 LE flags (bit 0: has_signature, bit 1: has_test_vectors)
|
||
8 4 u32 LE manifest_len (always 96)
|
||
12 4 u32 LE wasm_len
|
||
16 4 u32 LE signature_len (0 or 64)
|
||
20 4 u32 LE test_vectors_len (0 if none)
|
||
24 4 u32 LE total_len (header + manifest + wasm + sig + tvec)
|
||
28 4 u32 LE reserved (0)
|
||
────────────────────────────────────────────
|
||
32 96 struct Manifest (see below)
|
||
128 N bytes WASM payload ("\0asm" magic)
|
||
128+N 0|64 bytes Ed25519 signature (signs bytes 0..128+N-1)
|
||
128+N+S M bytes Test vectors (optional)
|
||
```
|
||
|
||
Total overhead: 32 (header) + 96 (manifest) + 64 (signature) = **192 bytes**.
|
||
|
||
### C.3 Manifest (96 bytes, packed)
|
||
|
||
| Offset | Size | Type | Field |
|
||
|--------|------|------|-------|
|
||
| 0 | 32 | char[] | `module_name` — null-terminated ASCII |
|
||
| 32 | 2 | u16 | `required_host_api` — version (1 = current) |
|
||
| 34 | 4 | u32 | `capabilities` — RVF_CAP_* bitmask |
|
||
| 38 | 4 | u32 | `max_frame_us` — requested per-frame budget (0 = use default) |
|
||
| 42 | 2 | u16 | `max_events_per_sec` — rate limit (0 = unlimited) |
|
||
| 44 | 2 | u16 | `memory_limit_kb` — max WASM heap (0 = use default) |
|
||
| 46 | 2 | u16 | `event_schema_version` — for receiver compatibility |
|
||
| 48 | 32 | [u8;32] | `build_hash` — SHA-256 of WASM payload |
|
||
| 80 | 2 | u16 | `min_subcarriers` — minimum required (0 = any) |
|
||
| 82 | 2 | u16 | `max_subcarriers` — maximum expected (0 = any) |
|
||
| 84 | 10 | char[] | `author` — null-padded ASCII |
|
||
| 94 | 2 | [u8;2] | reserved (0) |
|
||
|
||
### C.4 Capability Bitmask
|
||
|
||
| Bit | Flag | Host API functions |
|
||
|-----|------|--------------------|
|
||
| 0 | `READ_PHASE` | `csi_get_phase` |
|
||
| 1 | `READ_AMPLITUDE` | `csi_get_amplitude` |
|
||
| 2 | `READ_VARIANCE` | `csi_get_variance` |
|
||
| 3 | `READ_VITALS` | `csi_get_bpm_*`, `csi_get_presence`, `csi_get_n_persons` |
|
||
| 4 | `READ_HISTORY` | `csi_get_phase_history` |
|
||
| 5 | `EMIT_EVENTS` | `csi_emit_event` |
|
||
| 6 | `LOG` | `csi_log` |
|
||
|
||
Modules declare which host APIs they need. Future firmware versions may
|
||
refuse to link imports that aren't declared in capabilities — defense in
|
||
depth against supply-chain attacks.
|
||
|
||
### C.5 On-Device Flow
|
||
|
||
```
|
||
HTTP POST /wasm/upload
|
||
│
|
||
▼
|
||
┌────────────────────────┐
|
||
│ Check first 4 bytes │
|
||
│ "RVF\x01" → RVF path │
|
||
│ "\0asm" → raw path │
|
||
└───────┬────────────────┘
|
||
│
|
||
┌────▼────┐ ┌───────────┐
|
||
│ RVF │ │ Raw WASM │
|
||
│ parse │ │ (dev only,│
|
||
│ header │ │ verify=0) │
|
||
└────┬────┘ └─────┬─────┘
|
||
│ │
|
||
┌────▼────┐ │
|
||
│ Verify │ │
|
||
│ SHA-256 │ │
|
||
│ hash │ │
|
||
└────┬────┘ │
|
||
│ │
|
||
┌────▼────┐ │
|
||
│ Verify │ │
|
||
│ Ed25519 │ │
|
||
│ sig │ │
|
||
└────┬────┘ │
|
||
│ │
|
||
┌────▼────┐ │
|
||
│ Check │ │
|
||
│ host API│ │
|
||
│ version │ │
|
||
└────┬────┘ │
|
||
│ │
|
||
├────────────────┘
|
||
▼
|
||
┌───────────────────┐
|
||
│ wasm_runtime_load │
|
||
│ set_manifest │
|
||
│ start module │
|
||
└───────────────────┘
|
||
```
|
||
|
||
### C.6 Rollback Support
|
||
|
||
Each slot stores the SHA-256 build hash from the manifest. The `/wasm/list`
|
||
endpoint returns this hash. Fleet management systems can:
|
||
|
||
1. Push an RVF to a node
|
||
2. Verify the installed hash matches via GET `/wasm/list`
|
||
3. Roll back by pushing the previous RVF (same slot reused after unload)
|
||
|
||
Two-slot strategy: maintain slot 0 as "last known good" and slot 1 as
|
||
"candidate". Promote by stopping slot 0 and starting slot 1.
|
||
|
||
### C.7 Rust Builder
|
||
|
||
The `wifi-densepose-wasm-edge` crate provides `rvf::builder::build_rvf()`
|
||
(behind the `std` feature) to package a `.wasm` binary into an `.rvf`:
|
||
|
||
```rust
|
||
use wifi_densepose_wasm_edge::rvf::builder::{build_rvf, RvfConfig};
|
||
|
||
let wasm = std::fs::read("target/wasm32-unknown-unknown/release/module.wasm")?;
|
||
let rvf = build_rvf(&wasm, &RvfConfig {
|
||
module_name: "gesture".into(),
|
||
author: "rUv".into(),
|
||
capabilities: CAP_READ_PHASE | CAP_EMIT_EVENTS,
|
||
max_frame_us: 5000,
|
||
..Default::default()
|
||
});
|
||
std::fs::write("gesture.rvf", &rvf)?;
|
||
// Then sign externally with Ed25519 and patch_signature()
|
||
```
|
||
|
||
### C.8 Implementation Files
|
||
|
||
| File | Description |
|
||
|------|-------------|
|
||
| `firmware/.../main/rvf_parser.h` | RVF types, capability flags, parse/verify API |
|
||
| `firmware/.../main/rvf_parser.c` | Header/manifest parser, SHA-256 hash check |
|
||
| `wifi-densepose-wasm-edge/src/rvf.rs` | Format constants, builder (std), tests |
|
||
|
||
### C.9 Failure Modes
|
||
|
||
| Failure | Behavior |
|
||
|---------|----------|
|
||
| RVF too large for PSRAM buffer | Rejected at receive with 400 |
|
||
| Build hash mismatch | Rejected at parse with `ESP_ERR_INVALID_CRC` |
|
||
| Signature absent when `wasm_verify=1` | Rejected with 403 |
|
||
| Host API version too new | Rejected with `ESP_ERR_NOT_SUPPORTED` |
|
||
| Raw WASM when `wasm_verify=1` | Rejected with 403 |
|