End-to-end deployment fixes that took the two ESP32-S3 sensor boards (room01, room02) from "boots but DSP frozen, OTA always rolls back" to "motion/presence/breathing all live, two consecutive OTA round-trips succeed". Full forensic write-up in docs/adr/ADR-098. Firmware (firmware/esp32-csi-node/main/): * csi_collector.c — remove esp_wifi_set_promiscuous(true): this call silenced the CSI RX callback entirely on this silicon revision (yield=0pps). Without it, callbacks resume at ~5-10 pps. * edge_processing.c — root cause: incoming CSI frames carry 192 subcarriers but EDGE_MAX_SUBCARRIERS=128, so the size check early-returned every frame and Step 8 (motion) never ran. Truncate to 128 + warn once instead of returning. * edge_processing.c — replace per-bin unwrapped-phase variance with temporal variance of per-frame broadband mean amplitude. Empirical separation on deployed hardware: empty 0.07-0.10, walking 3.5-14 (~44x). Scaled by /3.0 and clamped to [0,1]. * edge_processing.c — biquad fs 20.0 -> 10.0, matching the actual callback rate (was halving the breathing passband). * ota_update.c — OTA_WITH_SEQUENTIAL_WRITES -> OTA_SIZE_UNKNOWN to erase the full target partition (stale tail of the previous larger image was crashing the new image on boot, looking like rollback). * ota_update.c — httpd_config_t.stack_size = 8192 (default 4 KB overflowed in OTA verify path). * main.c — log esp_reset_reason() and running_partition->label once at app_main start, so OTA outcomes are visible without guesswork. * sdkconfig.defaults — local deployment defaults: tier=2, display disabled (no expander on these boards), 8192 timer stack. Sensing server (v2/crates/wifi-densepose-sensing-server/): * src/main.rs — parse_rv_feature_state() for the 0xC5110006 feature_state packet that RuView FW emits by default; this format was previously unhandled. Wire ahead of parse_esp32_vitals. * src/main.rs — BaselineTracker with hysteretic motion gating on top of FW-reported scores, so UI sees clean boolean presence transitions. * src/main.rs — refuse --source simulate; remove auto-fallback to synthetic data. Production builds never run on fake signals. * src/main.rs/csi.rs — parse_csi_lean() for legacy FW 5.47 CSV packets; defence-in-depth for mistakenly flashed legacy sensors. Desktop UI (v2/crates/wifi-densepose-desktop/): * src/commands/discovery.rs — third discovery path: HTTP /status sweep across the local /24 in parallel with mDNS/UDP. mDNS+UDP-beacon are not advertised by current RuView FW. Replace sequential for-task-in-tasks select-with-deadline (which blocked on slow unrelated IPs) with futures::join_all + overall timeout. * src/commands/server.rs — pass --bind-addr (was --bind); pass RUST_LOG env instead of unsupported --log-level; auto-load bundled wifi-densepose-v1.rvf next to the binary; reasonable defaults (esp32 source, 0.0.0.0 bind). * ui/* — keep last good node list when a poll returns 0 (discovery is jittery on busy LANs); 8 s timeout (was 3 s); remove "simulate" from DataSource enum and Sensing dropdown; default Sensing source esp32. Mobile UI (ui/mobile/): * constants/websocket.ts — WS_PATH '/ws/sensing' + WS_PORT 8765 to match the RuView sensing-server's WS endpoint (was the legacy FastAPI /api/v1/stream/pose). * services/ws.service.ts — derive WS host from serverUrl but use WS_PORT; remove simulation fallback paths entirely (no generateSimulatedData, no startSimulation on reconnect failure). * stores/settingsStore.ts — serverUrl defaults to http://100.123.189.10:8080 (deployed Mac's Tailscale IP), so the phone connects from any network without LAN dependency. * stores/matStore.ts — default dataSource='real', simulationAcknowledged=true; no synthetic triage data. * screens/MATScreen, VitalsScreen — hide simulation overlay/badge. Docker: * docker/docker-compose.yml — sensing-server host port 5005 -> 5006 to match the RuView FW's compiled CSI_TARGET_PORT default. Documentation: * docs/adr/ADR-098-esp32s3-csi-deployment-fixes.md — full forensic ADR covering each decision, the empirical numbers that drove it, the false hypotheses we ruled out along the way, and open items. Verified on hardware (both nodes): * motion empty < 0.05 (room01 0.018, room02 0.070) * motion walking > 0.3 within 1-3 s, saturates at 1.0 * motion decay < 0.1 within 5 s after leaving * breathing 21-22 BPM detected after ~30 s stationary * two consecutive OTA round-trips succeed without USB intervention * discovery finds both sensors via HTTP sweep in <2 s Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| src | ||
| tests | ||
| Cargo.toml | ||
| README.md | ||
README.md
wifi-densepose-sensing-server
Lightweight Axum server for real-time WiFi sensing with RuVector signal processing.
Overview
wifi-densepose-sensing-server is the operational backend for WiFi-DensePose. It receives raw CSI
frames from ESP32 hardware over UDP, runs them through the RuVector-powered signal processing
pipeline, and broadcasts processed sensing updates to browser clients via WebSocket. A built-in
static file server hosts the sensing UI on the same port.
The crate ships both a library (wifi_densepose_sensing_server) exposing the training and inference
modules, and a binary (sensing-server) that starts the full server stack.
Integrates wifi-densepose-wifiscan for multi-BSSID WiFi scanning per ADR-022 Phase 3.
Features
- UDP CSI ingestion -- Receives ESP32 CSI frames on port 5005 and parses them into the internal
CsiFramerepresentation. - Vital sign detection -- Pure-Rust FFT-based breathing rate (0.1--0.5 Hz) and heart rate (0.67--2.0 Hz) estimation from CSI amplitude time series (ADR-021).
- RVF container -- Standalone binary container format for packaging model weights, metadata, and
configuration into a single
.rvffile with 64-byte aligned segments. - RVF pipeline -- Progressive model loading with streaming segment decoding.
- Graph Transformer -- Cross-attention bottleneck between antenna-space CSI features and the
COCO 17-keypoint body graph, followed by GCN message passing (ADR-023 Phase 2). Pure
std, no ML dependencies. - SONA adaptation -- LoRA + EWC++ online adaptation for environment drift without catastrophic forgetting (ADR-023 Phase 5).
- Contrastive CSI embeddings -- Self-supervised SimCLR-style pretraining with InfoNCE loss, projection head, fingerprint indexing, and cross-modal pose alignment (ADR-024).
- Sparse inference -- Activation profiling, sparse matrix-vector multiply, INT8/FP16 quantization, and a full sparse inference engine for edge deployment (ADR-023 Phase 6).
- Dataset pipeline -- Training dataset loading and batching.
- Multi-BSSID scanning -- Windows
netshintegration for BSSID discovery viawifi-densepose-wifiscan(ADR-022). - WebSocket broadcast -- Real-time sensing updates pushed to all connected clients at
ws://localhost:8765/ws/sensing. - Static file serving -- Hosts the sensing UI on port 8080 with CORS headers.
Modules
| Module | Description |
|---|---|
vital_signs |
Breathing and heart rate extraction via FFT spectral analysis |
rvf_container |
RVF binary format builder and reader |
rvf_pipeline |
Progressive model loading from RVF containers |
graph_transformer |
Graph Transformer + GCN for CSI-to-pose estimation |
trainer |
Training loop orchestration |
dataset |
Training data loading and batching |
sona |
LoRA adapters and EWC++ continual learning |
sparse_inference |
Neuron profiling, sparse matmul, INT8/FP16 quantization |
embedding |
Contrastive CSI embedding model and fingerprint index |
Quick Start
# Build the server
cargo build -p wifi-densepose-sensing-server
# Run with default settings (HTTP :8080, UDP :5005, WS :8765)
cargo run -p wifi-densepose-sensing-server
# Run with custom ports
cargo run -p wifi-densepose-sensing-server -- \
--http-port 9000 \
--udp-port 5005 \
--static-dir ./ui
Using as a library
use wifi_densepose_sensing_server::vital_signs::VitalSignDetector;
// Create a detector with 20 Hz sample rate
let mut detector = VitalSignDetector::new(20.0);
// Feed CSI amplitude samples
for amplitude in csi_amplitudes.iter() {
detector.push_sample(*amplitude);
}
// Extract vital signs
if let Some(vitals) = detector.detect() {
println!("Breathing: {:.1} BPM", vitals.breathing_rate_bpm);
println!("Heart rate: {:.0} BPM", vitals.heart_rate_bpm);
}
Architecture
ESP32 ──UDP:5005──> [ CSI Receiver ]
|
[ Signal Pipeline ]
(vital_signs, graph_transformer, sona)
|
[ WebSocket Broadcast ]
|
Browser <──WS:8765── [ Axum Server :8080 ] ──> Static UI files
Related Crates
| Crate | Role |
|---|---|
wifi-densepose-wifiscan |
Multi-BSSID WiFi scanning (ADR-022) |
wifi-densepose-core |
Shared types and traits |
wifi-densepose-signal |
CSI signal processing algorithms |
wifi-densepose-hardware |
ESP32 hardware interfaces |
wifi-densepose-wasm |
Browser WASM bindings for the sensing UI |
wifi-densepose-train |
Full training pipeline with ruvector |
wifi-densepose-mat |
Disaster detection module |
License
MIT OR Apache-2.0