wifi-densepose/docs/adr/ADR-035-live-sensing-ui-acc...

4.5 KiB
Raw Blame History

ADR-035: Live Sensing UI Accuracy & Data Source Transparency

Status

Accepted

Date

2026-03-02

Context

Issue #86 reported that the live demo shows a static/barely-animated stick figure and the sensing page displays inaccurate data, despite a working ESP32 sending real CSI frames. Investigation revealed three root causes:

  1. Docker defaults to --source simulated — even with a real ESP32 connected, the server generates synthetic sine-wave data instead of reading UDP frames.
  2. Live demo pose is analytically computedderive_pose_from_sensing() generates keypoints using sin(tick) math unrelated to actual signal content. No trained .rvf model is loaded by default.
  3. Sensing feature extraction is oversimplified — the server uses single-frame thresholds for motion detection and has no temporal analysis (breathing FFT, sliding window variance, frame history).
  4. No data source indicator — users cannot tell whether they are seeing real or simulated data.

Decision

1. Docker: Auto-detect data source

  • Default CSI_SOURCE changed from simulated to auto.
  • auto probes UDP port 5005 for an ESP32; falls back to simulation if none found.
  • Users override via CSI_SOURCE=esp32 docker-compose up.

2. Signal-responsive pose derivation

  • derive_pose_from_sensing() now reads actual sensing features:
    • motion_band_power drives limb splay and walking gait detection (> 0.55).
    • breathing_band_power drives torso expansion/contraction phased to breathing rate.
    • variance seeds per-joint noise so the skeleton moves independently.
    • dominant_freq_hz drives lateral torso lean.
    • change_points add burst jitter to extremity keypoints.
  • Tick rate reduced from 500ms to 100ms (2 fps → 10 fps).
  • pose_source field (signal_derived | model_inference) added to every WebSocket frame.

3. Temporal feature extraction

  • 100-frame circular buffer (VecDeque) added to AppStateInner.
  • Per-subcarrier temporal variance via Welford-style accumulation.
  • Breathing rate estimation via 9-candidate Goertzel filter bank (0.10.5 Hz) with 3x SNR gate.
  • Frame-to-frame L2 motion score replaces single-frame amplitude thresholds.
  • Signal quality metric: SNR-based (RSSI noise floor) blended with temporal stability.
  • Signal field driven by subcarrier variance spatial mapping instead of fixed animation.

4. Data source transparency in UI

  • Sensing tab: Banner showing "LIVE - ESP32" (green), "RECONNECTING..." (yellow), or "SIMULATED DATA" (red).
  • Live Demo tab: "Estimation Mode" badge showing "Signal-Derived" (green) or "Model Inference" (blue).
  • Setup Guide panel explaining what each ESP32 count provides (1x: presence/breathing, 3x: localization, 4x+: full pose with trained model).
  • Simulation fallback delayed from immediate to 5 failed reconnect attempts (~30s).

Consequences

Positive

  • Users with real ESP32 hardware get real data by default (auto-detect).
  • Simulated data is clearly labeled — no more confusion about data authenticity.
  • Pose skeleton visually responds to actual signal changes (motion, breathing, variance).
  • Feature extraction produces physiologically meaningful metrics (breathing rate via Goertzel, temporal motion detection).
  • Setup guide manages expectations about what each hardware configuration provides.

Negative

  • Signal-derived pose is still an approximation, not neural network inference. Per-limb tracking requires a trained .rvf model + 4+ ESP32 nodes.
  • Goertzel filter bank adds ~O(9×N) computation per frame (negligible at 100 frames).
  • Users with only 1 ESP32 may still be disappointed that arm tracking doesn't work — but the UI now explains why.

Files Changed

  • docker/Dockerfile.rustCSI_SOURCE=auto env, shell entrypoint for variable expansion
  • docker/docker-compose.ymlCSI_SOURCE=${CSI_SOURCE:-auto}, shell command string
  • wifi-densepose-sensing-server/src/main.rs — frame history buffer, Goertzel breathing estimation, temporal motion score, signal-driven pose derivation, pose_source field, 100ms tick default
  • ui/services/sensing.service.jsdataSource state, delayed simulation fallback, _simulated marker
  • ui/components/SensingTab.js — data source banner, "About This Data" card
  • ui/components/LiveDemoTab.js — estimation mode badge, setup guide panel
  • ui/style.css — banner, badge, and guide panel styles

References