The Hardware Platform domain covers everything from the ESP32-S3 silicon to the server-side aggregator: collecting raw CSI, processing it on-device, running programmable WASM modules at the edge, and provisioning fleets of sensor nodes. It is the physical foundation that all higher-level domains (RuvSense, WiFi-Mat, Pose Tracking) depend on for real radio data.
This document defines the system using Domain-Driven Design (DDD): bounded contexts that own their data and rules, aggregate roots that enforce invariants, value objects that carry meaning, and domain events that connect everything. The goal is to make the firmware and hardware layer's structure match the electronics it controls -- so that anyone reading the code (or an AI agent modifying it) understands why each piece exists, not just what it does.
Bounded Contexts:
| # | Context | Responsibility | Key ADRs | Code |
|---|---|---|---|---|
| 1 | Sensor Node | WiFi CSI collection, channel hopping, TDM scheduling, UDP streaming | ADR-012, ADR-018 | firmware/esp32-csi-node/main/{csi_collector,stream_sender,nvs_config}.c |
| 2 | Edge Processing | On-device DSP pipeline (Tiers 0-2): phase unwrap, presence, vitals, fall detection | ADR-039 | firmware/esp32-csi-node/main/edge_processing.c |
| 3 | WASM Runtime | Tier 3 programmable sensing: module management, host API, budget control, RVF containers | ADR-040, ADR-041 | firmware/esp32-csi-node/main/{wasm_runtime,wasm_upload,rvf_parser}.c |
| 4 | Aggregation | Server-side CSI frame reception, timestamp alignment, multi-node feature fusion | ADR-012 | crates/wifi-densepose-hardware/src/esp32/ |
| 5 | Provisioning | NVS configuration, firmware lifecycle, fleet management, deployment presets | ADR-044 | firmware/esp32-csi-node/provision.py |
All firmware paths are relative to the repository root. Rust crate paths are relative to rust-port/wifi-densepose-rs/.
| Term | Definition |
|---|---|
| Sensor Node | An ESP32-S3 device that captures WiFi CSI frames and streams them to an aggregator via UDP |
| CSI Frame | A snapshot of Channel State Information: amplitude and phase per subcarrier, extracted from WiFi preambles |
| Subcarrier | One of 52-56 OFDM frequency bins whose complex response encodes the radio channel; the atomic unit of CSI |
| Edge Tier | Processing level on the ESP32: 0 = raw passthrough, 1 = basic DSP, 2 = vitals pipeline, 3 = WASM programmable |
| Core 0 / Core 1 | The two Xtensa LX7 cores on ESP32-S3; Core 0 runs WiFi + CSI callback, Core 1 runs the DSP pipeline |
| SPSC Ring Buffer | Single-producer single-consumer lock-free queue between Core 0 (CSI callback) and Core 1 (DSP task) |
| Vitals Packet | 32-byte UDP packet (magic 0xC5110002) containing presence, breathing BPM, heart rate BPM, fall flag |
| Compressed Frame | Delta-compressed CSI frame (magic 0xC5110003) using XOR + RLE for 30-50% bandwidth reduction |
| WASM Module | A no_std Rust program compiled to wasm32-unknown-unknown, executed on-device via WASM3 interpreter |
| Module Slot | One of 4 pre-allocated PSRAM arenas (160 KB each) that host a WASM module instance |
| Host API | 12 functions in the csi namespace that WASM modules call to read sensor data and emit events |
| RVF Container | Signed binary envelope (192-byte overhead) wrapping a WASM payload with manifest, capabilities, and Ed25519 signature |
| Budget Guard | Per-frame execution time limit (default 10 ms); modules exceeding 10 consecutive faults are auto-stopped |
| Adaptive Budget | Mincut-eigenvalue-gap-driven compute allocation: scene complexity drives how much CPU time WASM modules get |
| Aggregator | Server (laptop, RPi, or cloud) that receives UDP streams from all nodes, aligns timestamps, and fuses features |
| Feature-Level Fusion | Combining per-node extracted features (not raw I/Q) to avoid cross-node clock synchronization |
| Fused Frame | Aggregated observation from all nodes for one time window, with cross-node correlation and fused motion energy |
| NVS | Non-Volatile Storage on ESP32 flash; stores runtime configuration (WiFi creds, edge tier, TDM slot, etc.) |
| Provisioning | Writing NVS key-value pairs to a device without recompiling firmware |
| TDM Slot | Time-Division Multiplexing slot assignment for coordinated multi-node transmission |
| Channel Hopping | Switching the ESP32 radio across WiFi channels (e.g., 1, 6, 11) for multi-band CSI diversity |
| OTA Update | Over-the-air firmware update via HTTP endpoint on port 8032 |
Responsibility: Capture raw WiFi CSI frames via the ESP-IDF CSI API, serialize them into the ADR-018 binary format, and stream to the aggregator over UDP. Handle channel hopping, TDM scheduling, and rate limiting.
+--------------------------------------------------------------+
| Sensor Node Context |
+--------------------------------------------------------------+
| |
| +----------------+ +----------------+ |
| | CSI Collector | | NVS Config | |
| | (promiscuous | | (20+ keys: | |
| | mode, I/Q | | ssid, ip, | |
| | extraction) | | tier, tdm...) | |
| +-------+--------+ +-------+--------+ |
| | | |
| | CSI callback | Boot config |
| | (Core 0, 50 Hz | |
| | rate limit) | |
| v | |
| +----------------+ | |
| | Stream Sender |<-----------+ |
| | (UDP to agg, | |
| | seq numbers, | |
| | ENOMEM | |
| | backoff) |---> UDP frames (magic 0xC5110001) |
| +-------+--------+ |
| | |
| | SPSC ring buffer (to Core 1) |
| v |
| [Edge Processing Context] |
| |
+--------------------------------------------------------------+
Aggregates:
SensorNode(Aggregate Root)
Value Objects:
CsiFrameNodeIdentityNvsConfigTdmScheduleChannelHopConfig
Domain Services:
CsiCollectionService-- Registers ESP-IDF CSI callback, extracts I/Q, enforces 50 Hz rate limitStreamSendService-- Serializes frames to ADR-018 binary format, sends UDP with sequence numbersNvsConfigService-- Reads 20+ NVS keys at boot, provides typed config to all firmware components
Responsibility: On-device signal processing pipeline running on Core 1. Implements Tiers 0-2: phase extraction, Welford running statistics, top-K subcarrier selection, bandpass filtering, BPM estimation, presence detection, and fall detection.
+--------------------------------------------------------------+
| Edge Processing Context |
+--------------------------------------------------------------+
| |
| SPSC ring buffer (from Core 0) |
| | |
| v |
| +----------------+ |
| | Phase Extract | Tier 1 |
| | + Unwrap | |
| +-------+--------+ |
| | |
| v |
| +----------------+ +----------------+ |
| | Welford Stats | | Top-K Select | |
| | (per-subcarrier| | (by variance) | |
| | running var) | +-------+--------+ |
| +-------+--------+ | |
| | | |
| +----------+----------+ |
| | |
| v |
| +------------------+-----------+ Tier 2 |
| | Biquad IIR Bandpass Filters | |
| | breathing: 0.1-0.5 Hz | |
| | heart rate: 0.8-2.0 Hz | |
| +-------+----------------------+ |
| | |
| v |
| +----------------+ +----------------+ |
| | Zero-Crossing | | Presence | |
| | BPM Estimator | | Detector | |
| | | | (adaptive | |
| | | | threshold, | |
| | | | 3-sigma cal) | |
| +-------+--------+ +-------+--------+ |
| | | |
| +----------+----------+ |
| | |
| v |
| +------------------+--------+ |
| | Fall Detector | |
| | (phase acceleration | |
| | threshold) | |
| +------------------+--------+ |
| | |
| v |
| +------------------+--------+ |
| | Multi-Person Clustering | |
| | (subcarrier groups, <=4) |----> VitalsPacket (0xC5110002) |
| +---------------------------+----> CompressedFrame (0xC5110003)|
| |
+--------------------------------------------------------------+
Aggregates:
EdgeProcessingState(Aggregate Root)
Value Objects:
VitalsPacketCompressedFramePresenceStateBpmEstimateFallAlertEdgeTier
Domain Services:
PhaseExtractionService-- Converts raw I/Q to amplitude + phase, applies unwrappingWelfordStatsService-- Maintains per-subcarrier running mean and varianceTopKSelectionService-- Selects K subcarriers with highest variance for downstream processingBandpassFilterService-- Biquad IIR filters for breathing and heart rate frequency bandsPresenceDetectionService-- Adaptive threshold with 1200-frame, 3-sigma calibrationFallDetectionService-- Phase acceleration exceeding configurable threshold (default 2.0 rad/s^2)DeltaCompressionService-- XOR + RLE delta encoding for 30-50% bandwidth reduction
Responsibility: Manage the Tier 3 WASM programmable sensing layer. Load, validate, execute, and monitor WASM modules compiled from Rust. Enforce budget guards, handle RVF container verification, expose Host API, and provide HTTP management endpoints.
+--------------------------------------------------------------+
| WASM Runtime Context |
+--------------------------------------------------------------+
| |
| +--------------------+ +--------------------+ |
| | Module Manager | | RVF Verifier | |
| | (4 slots, load/ | | (Ed25519 sig, | |
| | unload/start/ | | SHA-256 hash, | |
| | stop lifecycle) | | host API compat) | |
| +--------+-----------+ +--------+-----------+ |
| | | |
| +----------+--------------+ |
| | |
| v |
| +-------------------+------------------+ |
| | WASM3 Interpreter | |
| | +-----------+ +-----------+ | |
| | | Slot 0 | | Slot 1 | ...x4 | |
| | | 160 KB | | 160 KB | | |
| | | arena | | arena | | |
| | +-----------+ +-----------+ | |
| +-------------------+------------------+ |
| | |
| v |
| +-------------------+------------------+ |
| | Host API (12 funcs) | |
| | csi_get_phase, csi_get_amplitude, | |
| | csi_get_variance, csi_get_bpm_*, | |
| | csi_emit_event, csi_log, ... | |
| +-------------------+------------------+ |
| | |
| v |
| +-------------------+------------------+ |
| | Budget Controller | |
| | B = clamp(B0 + k1*dL + k2*A | |
| | - k3*T - k4*P, | |
| | B_min, B_max) | |
| | 10 consecutive faults -> auto-stop | |
| +-------------------+------------------+ |
| | |
| +----> WASM events (magic 0xC5110004) |
| |
| +--------------------+ |
| | HTTP Upload Server | |
| | (port 8032) | |
| | POST /wasm/upload | |
| | GET /wasm/list | |
| | POST /wasm/start/N | |
| | POST /wasm/stop/N | |
| | DELETE /wasm/N | |
| +--------------------+ |
| |
+--------------------------------------------------------------+
Aggregates:
WasmModuleSlot(Aggregate Root)
Value Objects:
RvfContainerRvfManifestWasmTelemetryHostApiVersionCapabilityBitmaskBudgetAllocationModuleState
Domain Services:
RvfVerificationService-- Parses RVF header, verifies SHA-256 hash and Ed25519 signatureModuleLifecycleService-- Handles load -> start -> run -> stop -> unload transitionsBudgetControllerService-- Computes per-frame budget from mincut eigenvalue gap, thermal, and battery pressureHostApiBindingService-- Links 12 host functions to WASM3 imports in the "csi" namespaceWasmUploadService-- HTTP server on port 8032 for module management endpoints
Responsibility: Receive UDP CSI streams from multiple ESP32 nodes on the server side. Align timestamps across nodes (without cross-node phase synchronization), compute cross-node correlations, and produce fused feature frames for downstream pipeline consumption.
+--------------------------------------------------------------+
| Aggregation Context |
+--------------------------------------------------------------+
| |
| UDP socket (:5005) |
| | | | |
| v v v |
| +--------+ +--------+ +--------+ |
| | Node 0 | | Node 1 | | Node 2 | ... (up to 6) |
| | State | | State | | State | |
| | (ring | | (ring | | (ring | |
| | buf, | | buf, | | buf, | |
| | drift)| | drift)| | drift)| |
| +---+----+ +---+----+ +---+----+ |
| | | | |
| +-----+----+-----+---+ |
| | | |
| v v |
| +--------------------+--+ +-----------------------+ |
| | Timestamp Aligner | | Cross-Node Correlator | |
| | (per-node monotonic, | | (amplitude ratios, | |
| | no NTP needed) | | fused motion energy) | |
| +-----------+-----------+ +----------+------------+ |
| | | |
| +----------+----------------+ |
| | |
| v |
| +----------------------+-----+ |
| | Fused Frame | |
| | per_node_features[] | |
| | cross_node_correlation |--> pipeline_tx (mpsc channel) |
| | fused_motion_energy | |
| | fused_breathing_band | |
| +----------------------------+ |
| |
+--------------------------------------------------------------+
Aggregates:
Esp32Aggregator(Aggregate Root)
Value Objects:
FusedFrameNodeStateCrossNodeCorrelationFusedMotionEnergy
Domain Services:
UdpReceiverService-- Listens on UDP port 5005, demuxes by magic number and node IDTimestampAlignmentService-- Maps per-node monotonic timestamps to aggregator-local timeFeatureFusionService-- Computes cross-node correlation, fused motion (max across nodes), fused breathing (highest SNR)PipelineBridgeService-- Feeds fused frames into the wifi-densepose Rust pipeline via mpsc channel
Responsibility: Configure ESP32 sensor nodes by writing NVS key-value pairs without recompiling firmware. Support fleet provisioning via config files, deployment presets, read-back verification, and auto-detection of connected devices.
+--------------------------------------------------------------+
| Provisioning Context |
+--------------------------------------------------------------+
| |
| +--------------------+ +--------------------+ |
| | CLI Interface | | Config File Loader | |
| | (--ssid, --port, | | (JSON mesh config, | |
| | --edge-tier, | | common + per-node | |
| | --preset, ...) | | settings) | |
| +--------+-----------+ +--------+-----------+ |
| | | |
| +----------+--------------+ |
| | |
| v |
| +-------------------+------------------+ |
| | Preset Resolver | |
| | basic, vitals, mesh-3, | |
| | mesh-6-vitals | |
| +-------------------+------------------+ |
| | |
| v |
| +-------------------+------------------+ |
| | NVS Writer | |
| | esptool partition write | |
| | 20+ keys: ssid, password, | |
| | target_ip, edge_tier, tdm_slot, | |
| | hop_count, wasm_max, ... | |
| +-------------------+------------------+ |
| | |
| v |
| +-------------------+------------------+ |
| | Verifier (optional) | |
| | serial monitor for 5s, | |
| | check for "CSI streaming active" | |
| +--------------------------------------+ |
| |
| +--------------------+ |
| | Read-Back | |
| | (--read: dump NVS | |
| | partition, parse | |
| | key-value pairs) | |
| +--------------------+ |
| |
| +--------------------+ |
| | Auto-Detect | |
| | (scan serial ports | |
| | for ESP32-S3) | |
| +--------------------+ |
| |
+--------------------------------------------------------------+
Aggregates:
ProvisioningSession(Aggregate Root)
Value Objects:
NvsConfigDeploymentPresetMeshConfigPortIdentityVerificationResult
Domain Services:
NvsWriteService-- Writes typed NVS key-value pairs to the ESP32 flash partition via esptoolPresetResolverService-- Maps named presets (basic, vitals, mesh-3, mesh-6-vitals) to NVS key setsMeshProvisionerService-- Iterates over nodes in a config file, computing TDM slots automaticallyReadBackService-- Reads NVS partition, parses binary format, returns typed configBootVerificationService-- Opens serial monitor post-provision, checks for expected log lines
/// A physical ESP32-S3 device configured for CSI collection.
/// Owns its identity, configuration, firmware version, and current edge tier.
pub struct SensorNode {
/// Unique node identifier (0-255, assigned during provisioning)
node_id: u8,
/// WiFi MAC address of the ESP32-S3
mac: MacAddress,
/// Current WiFi channel
channel: u8,
/// Firmware version string (e.g., "1.2.0")
firmware_version: FirmwareVersion,
/// Current edge processing tier (0-3)
edge_tier: EdgeTier,
/// Full NVS configuration snapshot
config: NvsConfig,
/// TDM slot assignment (None if standalone)
tdm_slot: Option<TdmSchedule>,
/// Channel hopping configuration
hop_config: Option<ChannelHopConfig>,
/// Current operational status
status: NodeStatus,
/// Monotonic boot timestamp (ms since power-on)
uptime_ms: u64,
}
impl SensorNode {
/// Invariant: node_id must be unique within a mesh deployment
/// Invariant: edge_tier 3 requires WASM runtime to be initialized
/// Invariant: tdm_slot.slot < tdm_slot.total_nodes
pub fn new(node_id: u8, mac: MacAddress, config: NvsConfig) -> Self { /* ... */ }
pub fn transition_tier(&mut self, new_tier: EdgeTier) -> Result<(), TierError> {
// Cannot go to Tier 3 if WASM runtime is not available
// Cannot downgrade while WASM modules are running
/* ... */
}
}/// Maintains the full on-device DSP pipeline state for one sensor node.
/// Runs exclusively on Core 1.
pub struct EdgeProcessingState {
/// Current processing tier
tier: EdgeTier,
/// Per-subcarrier running statistics (Welford)
subcarrier_stats: [WelfordAccumulator; 56],
/// Top-K selected subcarrier indices
top_k_indices: Vec<u8>,
/// Biquad IIR filter states
breathing_filter: BiquadState,
heartrate_filter: BiquadState,
/// Current presence detection state
presence: PresenceState,
/// Latest BPM estimates
breathing_bpm: Option<BpmEstimate>,
heartrate_bpm: Option<BpmEstimate>,
/// Fall detection state
fall_detector: FallDetectorState,
/// Multi-person clustering state (up to 4 persons)
person_clusters: Vec<PersonCluster>,
/// Calibration state (1200-frame adaptive threshold)
calibration: CalibrationState,
}
impl EdgeProcessingState {
/// Invariant: Only processes frames on Core 1 (never Core 0)
/// Invariant: Tier 0 performs no processing (passthrough only)
/// Invariant: Tier 2 includes all of Tier 1 processing
/// Invariant: person_clusters.len() <= 4
pub fn process_frame(&mut self, frame: &RawCsiFrame) -> ProcessingResult { /* ... */ }
}/// One of 4 pre-allocated WASM execution slots on the ESP32-S3.
/// Each slot owns its PSRAM arena, WASM3 runtime instance, and telemetry.
pub struct WasmModuleSlot {
/// Slot index (0-3)
slot_id: u8,
/// Pre-allocated PSRAM arena (160 KB, fixed at boot)
arena: FixedArena,
/// Loaded module metadata (None if slot is empty)
module: Option<LoadedModule>,
/// Current slot state
state: ModuleState,
/// Per-module telemetry counters
telemetry: WasmTelemetry,
/// Budget allocation for this slot (microseconds per frame)
budget_us: u32,
}
/// Metadata for a loaded WASM module
pub struct LoadedModule {
/// Module name from RVF manifest (up to 32 chars)
name: String,
/// SHA-256 hash of the WASM payload
build_hash: [u8; 32],
/// Declared capability bitmask
capabilities: CapabilityBitmask,
/// Author string from manifest
author: String,
/// WASM3 function pointers for lifecycle
fn_on_init: WasmFunction,
fn_on_frame: WasmFunction,
fn_on_timer: WasmFunction,
}
impl WasmModuleSlot {
/// Invariant: Arena is pre-allocated at boot and never freed (prevents fragmentation)
/// Invariant: Module auto-stopped after 10 consecutive budget faults
/// Invariant: RVF signature must be verified before loading (when wasm_verify=1)
/// Invariant: Module binary + WASM3 heap must fit within 160 KB arena
pub fn load(&mut self, rvf: &RvfContainer) -> Result<(), WasmLoadError> { /* ... */ }
pub fn on_frame(&mut self, n_sc: i32) -> Result<Vec<WasmEvent>, WasmExecError> {
// Measure execution time
// Record telemetry
// Check budget guard
/* ... */
}
}/// Server-side aggregator that receives CSI streams from multiple ESP32 nodes,
/// aligns timestamps, and produces fused feature frames.
pub struct Esp32Aggregator {
/// UDP socket listening for node streams (port 5005)
socket: UdpSocket,
/// Per-node state: ring buffer, last timestamp, drift estimate
nodes: HashMap<u8, NodeState>,
/// Ring buffer of fused feature frames
fused_buffer: VecDeque<FusedFrame>,
/// Channel to downstream pipeline
pipeline_tx: mpsc::Sender<CsiData>,
/// Configuration
config: AggregatorConfig,
}
impl Esp32Aggregator {
/// Invariant: Fuses features, never raw phases (clock drift makes cross-node
/// phase alignment impossible with 20-50 ppm crystal oscillators)
/// Invariant: Handles missing nodes gracefully (partial fused frames are valid)
/// Invariant: Sequence number gaps < 100ms are interpolated, not dropped
pub fn receive_and_fuse(&mut self) -> Result<FusedFrame, AggError> { /* ... */ }
}/// A provisioning session that configures one or more ESP32 nodes.
/// Tracks which nodes have been provisioned and their verification status.
pub struct ProvisioningSession {
/// Session identifier
session_id: SessionId,
/// Common configuration shared across all nodes
common_config: CommonConfig,
/// Per-node provisioning state
node_results: Vec<NodeProvisionResult>,
/// Preset used (if any)
preset: Option<DeploymentPreset>,
/// Mesh configuration (if provisioning multiple nodes)
mesh_config: Option<MeshConfig>,
}
impl ProvisioningSession {
/// Invariant: WiFi credentials must be non-empty
/// Invariant: target_ip must be a valid IPv4 address
/// Invariant: TDM slot indices must be unique and contiguous within a mesh
/// Invariant: hop_count must match the length of the channel list
pub fn provision_node(&mut self, port: &PortIdentity) -> Result<(), ProvisionError> {
/* ... */
}
}/// A single CSI observation from one ESP32 node.
/// Immutable snapshot of the radio channel at one instant.
pub struct CsiFrame {
/// Monotonic timestamp (ms since node boot)
timestamp_ms: u32,
/// Source node identifier
node_id: u8,
/// RSSI in dBm (typically -90 to -20)
rssi: i8,
/// WiFi channel number (1-13)
channel: u8,
/// Per-subcarrier amplitude (|CSI|, 52-56 values)
amplitude: Vec<f32>,
/// Per-subcarrier phase (arg(CSI), 52-56 values, radians)
phase: Vec<f32>,
/// Sequence number for loss detection
seq_num: u32,
}/// 32-byte Tier 2 output packet sent at configurable intervals.
/// Contains all vital sign estimates from on-device processing.
pub struct VitalsPacket {
/// Presence state
presence: PresenceState,
/// Motion score (0-255, higher = more motion)
motion_score: u8,
/// Breathing rate estimate (BPM, None if not confident)
breathing_bpm: Option<f32>,
/// Heart rate estimate (BPM, None if not confident)
heart_rate_bpm: Option<f32>,
/// Fall detected flag
fall_flag: bool,
/// Number of detected persons (0-8)
n_persons: u8,
/// Motion energy scalar
motion_energy: f32,
/// Presence confidence score
presence_score: f32,
/// RSSI at time of measurement
rssi: i8,
/// Timestamp (ms since boot)
timestamp_ms: u32,
}/// Processing tier on the ESP32-S3. Each tier includes all functionality
/// of lower tiers.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum EdgeTier {
/// Tier 0: Raw CSI passthrough (magic 0xC5110001). No on-device processing.
Disabled = 0,
/// Tier 1: Phase unwrap, Welford stats, top-K selection, delta compression.
/// Adds ~30 KB binary overhead.
BasicDsp = 1,
/// Tier 2: All of Tier 1 + biquad bandpass, BPM estimation, presence,
/// fall detection, multi-person clustering. Adds ~3 KB over Tier 1.
FullPipeline = 2,
/// Tier 3: All of Tier 2 + WASM3 runtime for programmable sensing modules.
/// Adds ~100 KB binary (WASM3 interpreter).
WasmProgrammable = 3,
}/// Complete NVS configuration for one ESP32 sensor node.
/// Covers all 20+ firmware-readable keys.
pub struct NvsConfig {
// -- Network --
pub ssid: String,
pub password: String,
pub target_ip: Ipv4Addr,
pub target_port: u16, // default: 5005
pub node_id: u8, // default: 0
// -- TDM --
pub tdm_slot: u8, // default: 0
pub tdm_total: u8, // default: 1 (no TDM)
// -- Channel Hopping --
pub hop_count: u8, // default: 1 (no hop)
pub chan_list: Vec<u8>, // default: [1, 6, 11]
pub dwell_ms: u32, // default: 100
// -- Edge Processing --
pub edge_tier: EdgeTier, // default: Tier 2
pub pres_thresh: u16, // default: 0 (auto-calibrate)
pub fall_thresh: u16, // default: 2000 (2.0 rad/s^2)
pub vital_win: u16, // default: 256
pub vital_int: u16, // default: 1000 ms
pub subk_count: u8, // default: 8
// -- Power --
pub power_duty: u8, // default: 100 (always on)
// -- WASM --
pub wasm_max: u8, // default: 4
pub wasm_verify: bool, // default: true (secure-by-default)
pub wasm_pubkey: Option<[u8; 32]>, // Ed25519 public key
// -- MAC Filter --
pub filter_mac: Option<MacAddress>,
}/// RVF (RuVector Format) container for signed WASM deployment.
/// Total overhead: 192 bytes (32 header + 96 manifest + 64 signature).
pub struct RvfContainer {
/// Format version (currently 1)
pub format_version: u16,
/// Feature flags (bit 0: has_signature, bit 1: has_test_vectors)
pub flags: u16,
/// Module manifest
pub manifest: RvfManifest,
/// Raw WASM payload (starts with "\0asm" magic)
pub wasm_payload: Vec<u8>,
/// Ed25519 signature over header + manifest + payload (64 bytes)
pub signature: Option<[u8; 64]>,
/// Optional test vectors for self-verification
pub test_vectors: Option<Vec<u8>>,
}
/// 96-byte packed manifest describing the WASM module.
pub struct RvfManifest {
pub module_name: String, // up to 32 chars
pub required_host_api: u16, // version (1 = current)
pub capabilities: CapabilityBitmask,
pub max_frame_us: u32, // requested per-frame budget
pub max_events_per_sec: u16, // rate limit
pub memory_limit_kb: u16, // max WASM heap
pub event_schema_version: u16,
pub build_hash: [u8; 32], // SHA-256 of WASM payload
pub min_subcarriers: u16,
pub max_subcarriers: u16,
pub author: String, // up to 10 chars
}/// Per-module execution telemetry, exposed via /wasm/list endpoint.
pub struct WasmTelemetry {
/// Total on_frame() calls since module start
pub frame_count: u32,
/// Total csi_emit_event() calls
pub event_count: u32,
/// WASM3 runtime errors
pub error_count: u32,
/// Cumulative execution time (microseconds)
pub total_us: u32,
/// Worst-case single-frame execution time (microseconds)
pub max_us: u32,
/// Number of times frame budget was exceeded
pub budget_faults: u32,
}/// Aggregated observation from all nodes for one time window.
/// Product of feature-level fusion (not signal-level).
pub struct FusedFrame {
/// Aggregator-local monotonic timestamp
timestamp: Instant,
/// Per-node features (None if node dropped frames)
node_features: Vec<Option<CsiFrame>>,
/// Cross-node correlation matrix (N x N)
cross_node_correlation: Array2<f64>,
/// Fused motion energy (max across all nodes)
fused_motion_energy: f64,
/// Fused breathing band (coherent sum from highest-SNR node)
fused_breathing_band: f64,
}/// Named provisioning presets for common deployment scenarios.
pub enum DeploymentPreset {
/// Single node, Tier 0, no TDM, no hopping
Basic,
/// Single node, Tier 2, vital_int=1000, subk_count=32
Vitals,
/// 3-node TDM, Tier 1, hop_count=3, channels=[1,6,11]
Mesh3,
/// 6-node TDM, Tier 2, hop_count=3, channels=[1,6,11], vital_int=500
Mesh6Vitals,
}/// Tri-state presence classification from the edge DSP pipeline.
pub enum PresenceState {
/// No motion detected; room appears empty
Empty,
/// Static human presence (breathing motion only)
Present,
/// Active motion detected
Moving,
}/// Lifecycle state of a WASM module slot.
pub enum ModuleState {
/// Slot is empty (arena allocated but no module loaded)
Empty,
/// Module loaded into arena but not yet started
Loaded,
/// Module running: on_frame() called per CSI frame, on_timer() at interval
Running,
/// Module explicitly stopped by user
Stopped,
/// Module auto-stopped due to error (10 consecutive budget faults or runtime error)
Error,
}/// Emitted when an ESP32 node completes boot and begins CSI collection.
pub struct NodeBooted {
pub node_id: u8,
pub mac: MacAddress,
pub firmware_version: FirmwareVersion,
pub edge_tier: EdgeTier,
pub uptime_ms: u64,
pub timestamp: DateTime<Utc>,
}
/// Emitted each time a CSI frame is received by the aggregator.
pub struct CsiFrameReceived {
pub node_id: u8,
pub seq_num: u32,
pub subcarrier_count: u8,
pub rssi: i8,
pub channel: u8,
pub timestamp: DateTime<Utc>,
}/// Emitted when presence detection state transitions.
pub struct PresenceChanged {
pub node_id: u8,
pub previous: PresenceState,
pub current: PresenceState,
pub motion_energy: f32,
pub timestamp: DateTime<Utc>,
}
/// Emitted at each vitals interval (default 1 Hz) with latest estimates.
pub struct VitalsUpdated {
pub node_id: u8,
pub breathing_bpm: Option<f32>,
pub heart_rate_bpm: Option<f32>,
pub n_persons: u8,
pub timestamp: DateTime<Utc>,
}
/// Emitted when phase acceleration exceeds the fall detection threshold.
pub struct FallDetected {
pub node_id: u8,
pub motion_energy: f32,
pub phase_acceleration: f32,
pub threshold: f32,
pub timestamp: DateTime<Utc>,
}/// Emitted when a WASM module is loaded into a slot and passes verification.
pub struct WasmModuleLoaded {
pub slot_id: u8,
pub module_name: String,
pub build_hash: [u8; 32],
pub capabilities: CapabilityBitmask,
pub author: String,
pub timestamp: DateTime<Utc>,
}
/// Emitted when a WASM module is auto-stopped or encounters a runtime error.
pub struct WasmModuleFaulted {
pub slot_id: u8,
pub module_name: String,
pub fault_type: WasmFaultType,
pub fault_count: u32,
pub telemetry: WasmTelemetry,
pub timestamp: DateTime<Utc>,
}
pub enum WasmFaultType {
/// Exceeded per-frame budget 10 consecutive times
BudgetExhausted,
/// WASM3 runtime trap (stack overflow, OOB memory, etc.)
RuntimeTrap,
/// Module called an unavailable host API function
MissingImport,
/// RVF signature verification failed
SignatureInvalid,
}
/// Emitted when a WASM module calls csi_emit_event().
pub struct WasmEventEmitted {
pub slot_id: u8,
pub module_name: String,
pub event_type: u8,
pub value: f32,
pub timestamp: DateTime<Utc>,
}/// Emitted when a node's NVS configuration has been successfully written.
pub struct NodeProvisioningComplete {
pub node_id: u8,
pub port: String,
pub config_keys: Vec<String>,
pub preset: Option<DeploymentPreset>,
pub verified: bool,
pub timestamp: DateTime<Utc>,
}
/// Emitted when mesh provisioning completes for all nodes in a config file.
pub struct MeshProvisioningComplete {
pub session_id: SessionId,
pub node_count: usize,
pub failed_nodes: Vec<u8>,
pub timestamp: DateTime<Utc>,
}| # | Invariant | Rationale | Enforcement |
|---|---|---|---|
| 1 | Core 0 handles WiFi + CSI callback only; Core 1 handles all DSP | Prevents WiFi stack corruption from compute-heavy DSP. CSI callback runs in ISR context on Core 0. | FreeRTOS task pinning: xTaskCreatePinnedToCore(..., 1) for DSP task |
| 2 | SPSC ring buffer between cores prevents memory contention | Lock-free single-producer single-consumer avoids mutexes between ISR and task contexts | csi_spsc_ring implementation with atomic read/write indices |
| 3 | CSI callback rate-limited to 50 Hz | Prevents lwIP pbuf exhaustion at high CSI rates (100-500 Hz in promiscuous mode). Issue #127 root cause. | 20 ms minimum interval check in csi_collector.c |
| 4 | sendto() uses 100 ms ENOMEM backoff |
UDP sends can fail when lwIP pbuf pool is temporarily exhausted; immediate retry amplifies the problem | stream_sender.c checks errno == ENOMEM and delays |
| 5 | Binary must fit within 1 MB OTA partition | ESP32-S3 partition table allocates 1 MB for the factory app. Exceeding this prevents OTA updates. | CI size gate at 950 KB in firmware-ci.yml |
| # | Invariant | Rationale | Enforcement |
|---|---|---|---|
| 6 | WASM modules get max 10 ms per frame | Prevents a runaway module from blocking the Tier 2 DSP pipeline and missing CSI frames | esp_timer_get_time() measurement + budget fault counter |
| 7 | Auto-stop after 10 consecutive budget faults | Graceful degradation: faulted module is stopped, Tier 2 pipeline continues unaffected | Fault counter in WasmModuleSlot, state transition to Error |
| 8 | RVF signature verification enabled by default | WASM upload is remote code execution; signatures ensure authenticity | wasm_verify=1 default in Kconfig and NVS fallback |
| 9 | WASM arenas are pre-allocated at boot (640 KB PSRAM) | Dynamic malloc/free cycles fragment PSRAM over days of continuous operation | Fixed 160 KB arenas per slot, zeroed on unload but never freed |
| 10 | Maximum 4 concurrent WASM module slots | Bounds PSRAM usage and prevents compute exhaustion | WASM_MAX_SLOTS constant, validated at load time |
| # | Invariant | Rationale | Enforcement |
|---|---|---|---|
| 11 | Feature-level fusion only (never raw phase alignment) | ESP32 crystal drift of 20-50 ppm makes cross-node phase coherence impossible | Aggregator extracts per-node features independently, then correlates |
| 12 | Missing nodes produce partial fused frames, not errors | Nodes may drop offline; the system must degrade gracefully | Option<CsiFrame> per node in FusedFrame.node_features |
| 13 | Sequence number gaps < 100 ms are interpolated | Brief UDP losses should not create discontinuities in downstream processing | Gap detection + linear interpolation in NodeState ring buffer |
| # | Invariant | Rationale | Enforcement |
|---|---|---|---|
| 14 | WiFi credentials must never appear in tracked files | Prevents credential leakage via git history | .gitignore for sdkconfig, provision.py writes to NVS only |
| 15 | TDM slot indices must be unique within a mesh | Duplicate slots cause transmission collisions | Validation in MeshProvisionerService, config file schema check |
| 16 | hop_count must equal chan_list.len() |
Mismatch causes firmware to read uninitialized channel values | CLI validation + NVS write-time assertion |
Manages the ESP-IDF CSI API lifecycle on Core 0.
pub trait CsiCollectionService {
/// Register CSI callback, configure promiscuous mode, set channel.
/// Rate-limits callback invocations to 50 Hz.
fn start_collection(&mut self, config: &NvsConfig) -> Result<(), CsiError>;
/// Stop CSI collection and deregister callback.
fn stop_collection(&mut self) -> Result<(), CsiError>;
/// Get current collection statistics (frames/sec, drops, errors).
fn stats(&self) -> CollectionStats;
}Orchestrates the Tier 1-2 DSP chain on Core 1.
pub trait EdgeProcessingPipeline {
/// Process a single CSI frame through the configured tier pipeline.
/// Returns vitals packet (if Tier 2 interval elapsed) and/or compressed frame.
fn process_frame(
&mut self,
raw: &RawCsiFrame,
) -> Result<ProcessingOutput, ProcessingError>;
/// Reconfigure the pipeline tier at runtime (e.g., via NVS update).
fn set_tier(&mut self, tier: EdgeTier) -> Result<(), TierError>;
/// Get calibration status (0.0-1.0, 1.0 = fully calibrated after 1200 frames).
fn calibration_progress(&self) -> f32;
}
pub struct ProcessingOutput {
pub vitals: Option<VitalsPacket>,
pub compressed: Option<CompressedFrame>,
pub events: Vec<EdgeEvent>,
}Manages the lifecycle of WASM modules across 4 slots.
pub trait WasmModuleManager {
/// Load an RVF container into the next available slot.
/// Verifies signature (if wasm_verify=1), checks host API compatibility,
/// validates binary fits within arena.
fn load_module(&mut self, rvf: &RvfContainer) -> Result<u8, WasmLoadError>;
/// Start a loaded module (calls on_init()).
fn start_module(&mut self, slot_id: u8) -> Result<(), WasmExecError>;
/// Stop a running module.
fn stop_module(&mut self, slot_id: u8) -> Result<(), WasmExecError>;
/// Unload a module from its slot (zeroes the arena).
fn unload_module(&mut self, slot_id: u8) -> Result<(), WasmLoadError>;
/// Get telemetry for all slots.
fn list_modules(&self) -> Vec<(u8, Option<&LoadedModule>, &ModuleState, &WasmTelemetry)>;
/// Execute on_frame() for all running modules within the budget.
fn dispatch_frame(&mut self, n_sc: i32) -> Vec<WasmEvent>;
}Server-side fusion of per-node features into a coherent multi-node observation.
pub trait FeatureFusionService {
/// Fuse features from N nodes for one time window.
/// - Motion energy: max across nodes
/// - Breathing band: highest-SNR node as primary
/// - Location: cross-node amplitude ratios
fn fuse(
&self,
node_features: &[Option<CsiFrame>],
) -> Result<FusedFrame, FusionError>;
/// Compute cross-node correlation matrix.
fn cross_correlate(
&self,
features: &[Option<CsiFrame>],
) -> Array2<f64>;
}Orchestrates the full provisioning workflow for individual nodes and meshes.
pub trait ProvisioningService {
/// Provision a single node with the given configuration.
fn provision_node(
&mut self,
port: &PortIdentity,
config: &NvsConfig,
) -> Result<NodeProvisionResult, ProvisionError>;
/// Provision all nodes defined in a mesh config file.
fn provision_mesh(
&mut self,
mesh: &MeshConfig,
) -> Result<Vec<NodeProvisionResult>, ProvisionError>;
/// Read back the current NVS configuration from a connected device.
fn read_config(
&self,
port: &PortIdentity,
) -> Result<NvsConfig, ProvisionError>;
/// Verify a provisioned node booted successfully.
fn verify_boot(
&self,
port: &PortIdentity,
timeout_secs: u32,
) -> Result<VerificationResult, ProvisionError>;
/// Auto-detect connected ESP32-S3 devices.
fn detect_ports(&self) -> Vec<PortIdentity>;
}+------------------------------------------------------------------+
| Hardware Platform Domain |
+------------------------------------------------------------------+
| |
| +------------------+ |
| | Provisioning | |
| | Context |--(writes NVS)---+ |
| | (provision.py) | | |
| +------------------+ | |
| v |
| +------------------+ SPSC +------------------+ |
| | Sensor Node |---------->| Edge Processing | |
| | Context | ring buf | Context | |
| | (Core 0) | | (Core 1) | |
| +--------+---------+ +--------+----------+ |
| | | |
| | UDP raw (0x01) | feeds CSI data |
| | v |
| | +------------------+ |
| | | WASM Runtime | |
| | | Context | |
| | | (Tier 3, Core 1)| |
| | +--------+---------+ |
| | | |
| | UDP raw UDP vitals (0x02) | UDP events (0x04) |
| | (0x01) UDP compressed | |
| | (0x03) | |
| +----------+------------------+ |
| | |
| v |
| +------------------+ |
| | Aggregation | |
| | Context | |
| | (Server-side) | |
| +--------+---------+ |
| | |
| | mpsc channel |
| v |
+------------------------------------------------------------------+
| DOWNSTREAM (Customer/Supplier) |
| +-----------------+ +-----------------+ +-----------------+ |
| | wifi-densepose | | wifi-densepose | | wifi-densepose | |
| | -signal | | -nn | | -mat | |
| | (RuvSense) | | (Inference) | | (Disaster) | |
| +-----------------+ +-----------------+ +-----------------+ |
+------------------------------------------------------------------+
Relationship Types:
| Upstream | Downstream | Relationship | Description |
|---|---|---|---|
| Provisioning | Sensor Node | Customer/Supplier | Provisioning writes NVS config that the node reads at boot |
| Sensor Node | Edge Processing | Partnership | Tightly coupled via SPSC ring buffer on the same chip |
| Edge Processing | WASM Runtime | Customer/Supplier | Edge pipeline feeds CSI data to WASM modules via Host API |
| Sensor Node | Aggregation | Published Language | ADR-018 binary wire format (magic bytes, fixed offsets) |
| Edge Processing | Aggregation | Published Language | Vitals (0xC5110002) and compressed (0xC5110003) wire formats |
| WASM Runtime | Aggregation | Published Language | WASM events (0xC5110004) wire format |
| Aggregation | Downstream crates | Customer/Supplier | Aggregator produces FusedFrame consumed by signal/nn/mat |
The aggregator translates between the hardware-specific ESP32 binary wire format and the wifi-densepose Rust pipeline types.
/// Adapts raw ESP32 UDP packets to the wifi-densepose-signal CsiData type.
pub struct Esp32ToPipelineAdapter {
/// Maps ADR-018 magic bytes to frame type
frame_parser: FrameParser,
/// Converts ESP32 I/Q byte pairs to f32 amplitude/phase
iq_converter: IqConverter,
}
impl Esp32ToPipelineAdapter {
/// Parse a raw UDP datagram from an ESP32 node into a pipeline-ready frame.
/// Handles magic byte demuxing:
/// 0xC5110001 -> raw CSI frame
/// 0xC5110002 -> vitals packet
/// 0xC5110003 -> compressed frame (decompress first)
/// 0xC5110004 -> WASM event packet
pub fn parse_datagram(
&self,
data: &[u8],
src_addr: SocketAddr,
) -> Result<ParsedFrame, ParseError> {
/* ... */
}
}
pub enum ParsedFrame {
RawCsi(CsiFrame),
Vitals(VitalsPacket),
CompressedCsi(CsiFrame), // decompressed
WasmEvent(WasmEventEmitted),
}The Host API acts as an anti-corruption layer between the WASM module world (no_std, wasm32 ABI) and the firmware's C data structures.
/// Translates between WASM3 linear memory and firmware C structs.
/// Each Host API function validates indices, clamps values, and converts types.
pub struct WasmHostApiAdapter {
/// Pointer to current CSI frame data (set before each on_frame dispatch)
current_frame: *const EdgeProcessingState,
/// Event buffer for this dispatch cycle
event_buffer: Vec<WasmEvent>,
}
impl WasmHostApiAdapter {
/// csi_get_phase(sc_idx: i32) -> f32
/// Validates sc_idx is within [0, n_subcarriers), returns 0.0 if out of bounds.
pub fn get_phase(&self, sc_idx: i32) -> f32 { /* ... */ }
/// csi_emit_event(event_type: i32, value: f32) -> void
/// Validates event_type is within the module's declared event ID range.
/// Applies dead-band filter: suppresses if |value - last_emitted| < threshold.
pub fn emit_event(&mut self, event_type: i32, value: f32) { /* ... */ }
}The provisioning tool translates between human-readable CLI arguments / JSON config files and the ESP-IDF NVS binary format.
/// Adapts CLI/JSON configuration to ESP32 NVS binary partition format.
/// Handles type conversions, validation, and encoding.
pub struct ProvisioningAdapter {
/// Maps CLI flag names to NVS key names and types
key_registry: HashMap<String, NvsKeySpec>,
}
pub struct NvsKeySpec {
pub nvs_key: String, // e.g., "edge_tier"
pub nvs_type: NvsType, // u8, u16, u32, string, blob
pub default: Option<String>,
pub validator: Box<dyn Fn(&str) -> bool>,
}
impl ProvisioningAdapter {
/// Convert a typed NvsConfig struct to a list of NVS binary writes.
pub fn to_nvs_entries(&self, config: &NvsConfig) -> Vec<NvsEntry> { /* ... */ }
/// Parse an NVS binary partition dump into a typed NvsConfig.
pub fn from_nvs_partition(&self, data: &[u8]) -> Result<NvsConfig, ParseError> { /* ... */ }
}All ESP32 UDP packets share a 4-byte magic prefix for demuxing at the aggregator.
| Magic | Name | Source | Size | Rate | Description |
|---|---|---|---|---|---|
0xC5110001 |
Raw CSI | Tier 0+ | ~128-404 B | 20-28.5 Hz | Full I/Q per subcarrier |
0xC5110002 |
Vitals | Tier 2+ | 32 B | 1 Hz (configurable) | Presence, BPM, fall flag |
0xC5110003 |
Compressed | Tier 1+ | variable | 20-28.5 Hz | XOR+RLE delta-compressed CSI |
0xC5110004 |
WASM Events | Tier 3 | variable | event-driven | Module event_type + value tuples |
| Metric | Value | Source |
|---|---|---|
| CSI frame rate | 28.5 Hz (measured) | ADR-039 hardware benchmark |
| Boot to ready | 3.9 s | WiFi connect dominates |
| Binary size | 925 KB (10% free in 1 MB) | Includes full WASM3 runtime |
| WASM init time | 106 ms | 4 slots, 160 KB arenas |
| WASM binary size (7 modules) | 13.8 KB | wasm32-unknown-unknown release |
| Internal RAM available | 316 KiB | No PSRAM on test board |
| Crystal drift | 20-50 ppm | 72-180 ms divergence per hour |
| BOM (3-node starter kit) | $54 | ADR-012 bill of materials |
- ADR-012: ESP32 CSI Sensor Mesh -- Hardware selection, mesh architecture, BOM
- ADR-018: Dev Implementation -- Binary frame format, ADR-018 wire protocol
- ADR-039: ESP32-S3 Edge Intelligence -- Tiered processing, DSP pipeline, hardware benchmarks
- ADR-040: WASM Programmable Sensing -- WASM3 runtime, Host API, RVF container, adaptive budget
- ADR-041: WASM Module Collection -- 60-module catalog, event ID registry, budget tiers
- ADR-044: Provisioning Tool Enhancements -- NVS coverage, presets, mesh config, read-back
- RuvSense Domain Model -- Upstream signal processing domain
- WiFi-Mat Domain Model -- Downstream disaster response domain