mirror of
https://github.com/ruvnet/RuView.git
synced 2026-04-28 14:09:33 +00:00
98 lines
5.9 KiB
Markdown
98 lines
5.9 KiB
Markdown
# 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 computed** — `derive_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.1–0.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.
|
||
|
||
### 5. Dark mode consistency
|
||
- Live Demo tab converted from light theme to dark mode matching the rest of the UI.
|
||
- All sidebar panels, badges, buttons, dropdowns use dark backgrounds with muted text.
|
||
|
||
### 6. Render mode implementations
|
||
All four render modes in the pose visualization dropdown now produce distinct visual output:
|
||
|
||
| Mode | Rendering |
|
||
|------|-----------|
|
||
| **Skeleton** | Green lines connecting joints + red keypoint dots |
|
||
| **Keypoints** | Large colored dots with glow and labels, no connecting lines |
|
||
| **Heatmap** | Gaussian radial blobs per keypoint (hue per person), faint skeleton overlay at 25% opacity |
|
||
| **Dense** | Body region segmentation with colored filled polygons — head (red), torso (blue), left arm (green), right arm (orange), left leg (purple), right leg (yellow) |
|
||
|
||
Previously heatmap and dense were stubs that fell back to skeleton mode.
|
||
|
||
### 7. pose_source passthrough fix
|
||
The `pose_source` field from the WebSocket message was being dropped in `convertZoneDataToRestFormat()` in `pose.service.js`. Now passed through so the Estimation Mode badge displays correctly.
|
||
|
||
## Files Changed
|
||
- `docker/Dockerfile.rust` — `CSI_SOURCE=auto` env, shell entrypoint for variable expansion
|
||
- `docker/docker-compose.yml` — `CSI_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.js` — `dataSource` state, delayed simulation fallback, `_simulated` marker
|
||
- `ui/services/pose.service.js` — `pose_source` passthrough in data conversion
|
||
- `ui/components/SensingTab.js` — data source banner, "About This Data" card
|
||
- `ui/components/LiveDemoTab.js` — estimation mode badge, setup guide panel, dark mode theme
|
||
- `ui/utils/pose-renderer.js` — heatmap (Gaussian blobs) and dense (body region segmentation) render modes
|
||
- `ui/style.css` — banner, badge, guide panel, and about-text styles
|
||
- `README.md` — live pose detection screenshot
|
||
- `assets/screen.png` — screenshot asset
|
||
|
||
## References
|
||
- Issue: https://github.com/ruvnet/wifi-densepose/issues/86
|
||
- ADR-029: RuvSense multistatic sensing mode (proposed — full pipeline integration)
|
||
- ADR-014: SOTA signal processing
|