Files
ruvnet--RuView/docs/adr/ADR-019-sensing-only-ui-mode.md
T
rUv 81cc241b9e chore(repo): move v1/ → archive/v1/ + add archive/README.md (#430)
The Rust port at v2/ has been the primary codebase since the rename
in #427. The Python implementation at v1/ is no longer the active
target; the only load-bearing path is the deterministic proof bundle
at v1/data/proof/ (per ADR-011 / ADR-028 witness verification).

Move the whole Python tree into archive/v1/ and document the policy
in archive/README.md: no new features, bug fixes only when they affect
a still-load-bearing path (currently just the proof), CI continues to
verify the proof on every push and PR.

Path references updated in 26 files via path-pattern sed (only
matches v1/<known-child> patterns, never bare v1 or API URLs like
/api/v1/). Two double-prefix typos (archive/archive/v1/) caught and
hand-fixed in verify-pipeline.yml and ADR-011.

Validated:
- Python proof verify.py imports cleanly at archive/v1/data/proof/
  (numpy/scipy still required; CI installs requirements-lock.txt
  from archive/v1/ now)
- cargo test --workspace --no-default-features → 1,539 passed,
  0 failed, 8 ignored (unaffected by Python tree relocation)
- ESP32-S3 on COM7 untouched (no firmware paths changed)

After-merge: contributors should re-run any local `python v1/...`
commands as `python archive/v1/...` (CLAUDE.md and CHANGELOG already
updated).
2026-04-25 23:07:52 -04:00

123 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ADR-019: Sensing-Only UI Mode with Gaussian Splat Visualization
| Field | Value |
|-------|-------|
| **Status** | Accepted |
| **Date** | 2026-02-28 |
| **Deciders** | ruv |
| **Relates to** | ADR-013 (Feature-Level Sensing), ADR-018 (ESP32 Dev Implementation) |
## Context
The WiFi-DensePose UI was originally built to require the full FastAPI DensePose backend (`localhost:8000`) for all functionality. This backend depends on heavy Python packages (PyTorch ~2GB, torchvision, OpenCV, SQLAlchemy, Redis) making it impractical for lightweight sensing-only deployments where the user simply wants to visualize live WiFi signal data from ESP32 CSI or Windows RSSI collectors.
A Rust port exists (`v2`) using Axum with lighter runtime footprint (~10MB binary, ~5MB RAM), but it still requires libtorch C++ bindings and OpenBLAS for compilation—a non-trivial build.
Users need a way to run the UI with **only the sensing pipeline** active, without installing the full DensePose backend stack.
## Decision
Implement a **sensing-only UI mode** that:
1. **Decouples the sensing pipeline** from the DensePose API backend. The sensing WebSocket server (`ws_server.py` on port 8765) operates independently of the FastAPI backend (port 8000).
2. **Auto-detects sensing-only mode** at startup. When the DensePose backend is unreachable, the UI sets `backendDetector.sensingOnlyMode = true` and:
- Suppresses all API requests to `localhost:8000` at the `ApiService.request()` level
- Skips initialization of DensePose-dependent tabs (Dashboard, Hardware, Live Demo)
- Shows a green "Sensing mode" status toast instead of error banners
- Silences health monitoring polls
3. **Adds a new "Sensing" tab** with Three.js Gaussian splat visualization:
- Custom GLSL `ShaderMaterial` rendering point-cloud splats on a 20×20 floor grid
- Signal field splats colored by intensity (blue → green → red)
- Body disruption blob at estimated motion position
- Breathing ring modulation when breathing-band power detected
- Side panel with RSSI sparkline, feature meters, and classification badge
4. **Python WebSocket bridge** (`archive/v1/src/sensing/ws_server.py`) that:
- Auto-detects ESP32 UDP CSI stream on port 5005 (ADR-018 binary frames)
- Falls back to `WindowsWifiCollector``SimulatedCollector`
- Runs `RssiFeatureExtractor``PresenceClassifier` pipeline
- Broadcasts JSON sensing updates every 500ms on `ws://localhost:8765`
5. **Client-side fallback**: `sensing.service.js` generates simulated data when the WebSocket server is unreachable, so the visualization always works.
## Architecture
```
ESP32 (UDP :5005) ──┐
├──▶ ws_server.py (:8765) ──▶ sensing.service.js ──▶ SensingTab.js
Windows WiFi RSSI ───┘ │ │ │
Feature extraction WebSocket client gaussian-splats.js
+ Classification + Reconnect (Three.js ShaderMaterial)
+ Sim fallback
```
### Data flow
| Source | Collector | Feature Extraction | Output |
|--------|-----------|-------------------|--------|
| ESP32 CSI (ADR-018) | `Esp32UdpCollector` (UDP :5005) | Amplitude mean → pseudo-RSSI → `RssiFeatureExtractor` | `sensing_update` JSON |
| Windows WiFi | `WindowsWifiCollector` (netsh) | RSSI + signal% → `RssiFeatureExtractor` | `sensing_update` JSON |
| Simulated | `SimulatedCollector` | Synthetic RSSI patterns | `sensing_update` JSON |
### Sensing update JSON schema
```json
{
"type": "sensing_update",
"timestamp": 1234567890.123,
"source": "esp32",
"nodes": [{ "node_id": 1, "rssi_dbm": -39, "position": [2,0,1.5], "amplitude": [...], "subcarrier_count": 56 }],
"features": { "mean_rssi": -39.0, "variance": 2.34, "motion_band_power": 0.45, ... },
"classification": { "motion_level": "active", "presence": true, "confidence": 0.87 },
"signal_field": { "grid_size": [20,1,20], "values": [...] }
}
```
## Files
### Created
| File | Purpose |
|------|---------|
| `archive/v1/src/sensing/ws_server.py` | Python asyncio WebSocket server with auto-detect collectors |
| `ui/components/SensingTab.js` | Sensing tab UI with Three.js integration |
| `ui/components/gaussian-splats.js` | Custom GLSL Gaussian splat renderer |
| `ui/services/sensing.service.js` | WebSocket client with reconnect + simulation fallback |
### Modified
| File | Change |
|------|--------|
| `ui/index.html` | Added Sensing nav tab button and content section |
| `ui/app.js` | Sensing-only mode detection, conditional tab init |
| `ui/style.css` | Sensing tab layout and component styles |
| `ui/config/api.config.js` | `AUTO_DETECT: false` (sensing uses own WS) |
| `ui/services/api.service.js` | Short-circuit requests in sensing-only mode |
| `ui/services/health.service.js` | Skip polling when backend unreachable |
| `ui/components/DashboardTab.js` | Graceful failure in sensing-only mode |
## Consequences
### Positive
- UI works with zero heavy dependencies—only `pip install websockets` (+ numpy/scipy already installed)
- ESP32 CSI data flows end-to-end without PyTorch, OpenCV, or database
- Existing DensePose tabs still work when the full backend is running
- Clean console output—no `ERR_CONNECTION_REFUSED` spam in sensing-only mode
### Negative
- Two separate WebSocket endpoints: `:8765` (sensing) and `:8000/api/v1/stream/pose` (DensePose)
- Pose estimation, zone occupancy, and historical data features unavailable in sensing-only mode
- Client-side simulation fallback may mislead users if they don't notice the "Simulated" badge
### Neutral
- Rust Axum backend remains a future option for a unified lightweight server
- The sensing pipeline reuses the existing `RssiFeatureExtractor` and `PresenceClassifier` classes unchanged
## Alternatives Considered
1. **Install minimal FastAPI** (`pip install fastapi uvicorn pydantic`): Starts the server but pose endpoints return errors without PyTorch.
2. **Build Rust backend**: Single binary, but requires libtorch + OpenBLAS build toolchain.
3. **Merge sensing into FastAPI**: Would require FastAPI installed even for sensing-only use.
Option 1 was rejected because it still shows broken tabs. The chosen approach cleanly separates concerns.