docs/adr/ADR-039-esp32-edge-intelligence.md
Status: Accepted (hardware-validated on RuView ESP32-S3) Date: 2026-03-02 Deciders: @ruvnet
WiFi-DensePose captures Channel State Information (CSI) from ESP32-S3 nodes and streams raw I/Q data to a host server for processing. This architecture has limitations:
Implement a tiered edge processing pipeline on the ESP32-S3 that performs signal processing locally and sends compact results:
No on-device processing. CSI frames streamed as-is (magic 0xC5110001).
0xC5110005, reassigned from 0xC5110003 by ADR-069)All of Tier 1, plus:
0xC5110002)Core 0 (WiFi) Core 1 (DSP)
┌─────────────────┐ ┌──────────────────────────┐
│ CSI callback │──SPSC ring──▶│ Phase extract + unwrap │
│ (wifi_csi_cb) │ buffer │ Welford variance │
│ │ │ Top-K selection │
│ UDP raw stream │ │ Biquad bandpass filters │
│ (0xC5110001) │ │ Zero-crossing BPM │
└─────────────────┘ │ Presence detection │
│ Fall detection │
│ Multi-person clustering │
│ Delta compression │
│ ──▶ UDP vitals (0xC5110002)│
│ ──▶ UDP compressed (0x05) │
└──────────────────────────┘
Vitals Packet (32 bytes, magic 0xC5110002):
| Offset | Type | Field |
|---|---|---|
| 0-3 | u32 LE | Magic 0xC5110002 |
| 4 | u8 | Node ID |
| 5 | u8 | Flags (bit0=presence, bit1=fall, bit2=motion) |
| 6-7 | u16 LE | Breathing rate (BPM × 100) |
| 8-11 | u32 LE | Heart rate (BPM × 10000) |
| 12 | i8 | RSSI |
| 13 | u8 | Number of detected persons |
| 14-15 | u8[2] | Reserved |
| 16-19 | f32 LE | Motion energy |
| 20-23 | f32 LE | Presence score |
| 24-27 | u32 LE | Timestamp (ms since boot) |
| 28-31 | u32 LE | Reserved |
Compressed Frame (magic 0xC5110005, reassigned from 0xC5110003 by ADR-069):
| Offset | Type | Field |
|---|---|---|
| 0-3 | u32 LE | Magic 0xC5110005 |
| 4 | u8 | Node ID |
| 5 | u8 | WiFi channel |
| 6-7 | u16 LE | Original I/Q length |
| 8-9 | u16 LE | Compressed length |
| 10+ | bytes | RLE-encoded XOR delta |
Six NVS keys in the csi_cfg namespace:
| NVS Key | Type | Default | Description |
|---|---|---|---|
edge_tier | u8 | 2 | Processing tier (0/1/2) |
pres_thresh | u16 | 0 | Presence threshold × 1000 (0 = auto) |
fall_thresh | u16 | 2000 | Fall threshold × 1000 (rad/s²) |
vital_win | u16 | 256 | Phase history window |
vital_int | u16 | 1000 | Vitals interval (ms) |
subk_count | u8 | 8 | Top-K subcarrier count |
All configurable via provision.py --edge-tier 2 --pres-thresh 0.05 ...
POST /ota, GET /ota/status) with rollback supportfirmware/esp32-csi-node/main/edge_processing.c — DSP pipeline (~750 lines)firmware/esp32-csi-node/main/edge_processing.h — Types and APIfirmware/esp32-csi-node/main/ota_update.c/h — HTTP OTA endpointfirmware/esp32-csi-node/main/power_mgmt.c/h — Power managementrust-port/.../wifi-densepose-sensing-server/src/main.rs — Vitals parser + REST endpointscripts/provision.py — Edge config CLI arguments.github/workflows/firmware-ci.yml — CI build + size gate (updated to 950 KB for Tier 3)See ADR-040 for hot-loadable WASM modules compiled from Rust, executed via WASM3 interpreter on-device. Core modules: gesture recognition, coherence monitoring, adversarial detection.
ADR-041 defines the curated module collection (37 modules across 6 categories). Phase 1 implemented modules:
vital_trend.rs — Clinical vital sign trend analysis (bradypnea, tachypnea, apnea)intrusion.rs — State-machine intrusion detection (calibrate-monitor-arm-alert)occupancy.rs — Spatial occupancy zone detection with per-zone variance analysisMeasured on ESP32-S3 (QFN56 rev v0.2, 8 MB flash, 160 MHz, ESP-IDF v5.2).
| Milestone | Time (ms) |
|---|---|
app_main() | 412 |
| WiFi STA init | 627 |
| WiFi connected + IP | 3,732 |
| CSI collection init | 3,754 |
| Edge DSP task started | 3,773 |
| WASM runtime initialized | 3,857 |
| Total boot → ready | ~3.9 s |
| Metric | Value |
|---|---|
| Frame rate | 28.5 Hz (measured, ch 5 BW20) |
| Frame sizes | 128 / 256 bytes |
| RSSI range | -83 to -32 dBm (mean -62 dBm) |
| Per-frame interval | 30.6 ms avg |
| Region | Size |
|---|---|
| RAM (main heap) | 256 KiB |
| RAM (secondary) | 21 KiB |
| DRAM | 32 KiB |
| RTC RAM | 7 KiB |
| Total available | 316 KiB |
| PSRAM | Not populated on test board |
| WASM arena fallback | Internal heap (160 KB/slot × 4) |
| Metric | Value |
|---|---|
| Binary size | 925 KB (0xE7440 bytes) |
| Partition size | 1 MB (factory) |
| Free space | 10% (99 KB) |
| CI size gate | 950 KB (PASS) |
| WASM3 interpreter | Included (full, ~100 KB) |
| WASM binary (7 modules) | 13.8 KB (wasm32-unknown-unknown release) |
| Metric | Value |
|---|---|
| Init time | 106 ms |
| Module slots | 4 |
| Arena per slot | 160 KB |
| Frame budget | 10,000 µs (10 ms) |
| Timer interval | 1,000 ms (1 Hz) |
fall_thresh=2000 (2.0 rad/s²) triggers 6.7 false positives/s in static indoor environment. Recommend increasing to 5000-8000 for typical deployments.csi_collector.c (20 ms minimum send interval) and a 100 ms ENOMEM backoff in stream_sender.c. Binary size with fix: 947 KB. Hardware-verified stable for 200+ CSI callbacks with zero ENOMEM errors.