Compare commits

...

9 Commits

Author SHA1 Message Date
arsen f6adcb2014 ui(raw): surface WiFi-CSI breathing + heart rate pills
ADR-021 already publishes `vital_signs` inside SensingUpdate but the
raw calibration console had no readout — the operator had to curl
/api/v1/vital-signs to see breathing/HR. Add two pills (🫁 + 💓)
next to the mmWave one and update them on every WS tick.

Confidence < 20 % dims the pill so noise-floor estimates don't read
as real values. Missing/zero rates fall back to "— BPM".

Mirrored ui/raw.html → static/raw.html so both deployment paths
serve the same console.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 12:17:36 +07:00
arsen e53a2e1f5c feat(adr-121): mmWave radar pill in raw.html top bar
Adds a hidden-by-default 📡 mmWave pill next to the global badge + CV
stat. Polls /api/v1/mmwave/latest at 5 Hz (~200 ms) — well above the
HLK-LD2402's 6 Hz native cadence so no information is lost. Pill shows:

  📡 mmWave 152 cm · 60 ms

Distance + age (ms since last reading). Fades to 50% opacity when age
>1.5 s, hides entirely when the server reports `available: false`
(port absent or stale >2 s).

Synced both copies — ui/raw.html (deploy mirror) + static/raw.html
(canonical source referenced by ADR-104 / ADR-107).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:54:54 +07:00
arsen b74ffd958a chore(ui): serve raw.html from ui/ so the calibration console is reachable
Previously raw.html lived only at v2/crates/wifi-densepose-sensing-server/static/raw.html.
When the server is started with --ui-path /Users/arsen/Desktop/RuView/ui
(the SPA path) the calibration console returns 404 on /ui/raw.html.

Copy the file into ui/ alongside index.html so a single --ui-path
covers both the SPA and the engineer-facing raw view. The static/
copy in the crate stays as the canonical source (referenced by ADRs
104/107); ui/raw.html is a deploy mirror.

Live at http://localhost:8080/ui/raw.html.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:50:47 +07:00
arsen a36af57d19 fix(docs): repair internal links broken by README/CLAUDE doc-slim
After ADR-117/118 docs sweep (commit 4075b608) extracted Use Cases,
How It Works, Edge Modules, Self-Learning sections from README into
docs/use-cases.md + docs/architecture.md, but two classes of links
were left dangling:

1. README anchor links pointing at section IDs that no longer exist
   in README:
     #edge-intelligence-adr-041  → moved to docs/use-cases.md
     #esp32-s3-hardware-pipeline → architecture detail in docs/
     #vital-sign-detection       → moved out
     #sensing-server             → moved out
     #-quick-start               → renamed during slim

   Replaced with deep links into docs/use-cases.md or docs/dev-handbook.md
   / docs/architecture.md where appropriate.

2. Extracted docs (docs/use-cases.md etc.) had path links written from
   the perspective of repo root (docs/edge-modules/, v2/crates/...) —
   broken once the file moved into docs/. Bulk-rewrote via Python
   regex pass:
     docs/edge-modules/X → edge-modules/X
     docs/adr/X          → adr/X
     v2/...              → ../v2/...
     archive/...         → ../archive/...
     scripts/...         → ../scripts/...
     plugins/...         → ../plugins/...
     firmware/...        → ../firmware/...

3. docs/use-cases.md self-reference #ai-backbone-ruvector → that
   section was never moved; replaced with prose + link to
   architecture.md.

Final scan: ZERO dangling anchors in the doc tree. One valid
anchor `#edge-module-list` in use-cases.md points to a local
`<details id="edge-module-list">` block.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:34:02 +07:00
arsen cb6e24ed57 feat(adr-121): HLK-LD2402 mmWave radar live readout in UI
Adds a dedicated blocking serial-reader thread that opens the
HLK-LD2402 over a CP2102 USB-UART bridge (default 115200 8N1),
parses ASCII `distance:<cm>\r\n` lines @ ~6 Hz, stores the latest
reading in a static OnceLock<Mutex<…>>, and exposes it via:

  GET /api/v1/mmwave/latest →
    { "available": true, "distance_cm": 152, "age_ms": 90 }
    { "available": false }            (port absent, stale > 2 s)

UI (Sensing tab) polls the endpoint every visible WS tick and
shows a new blue card "mmWave Radar (24 GHz)" with distance +
age bar. Card hides when unavailable.

CLI:
  --mmwave-port /dev/cu.usbserial-1140
  --mmwave-baud 115200            (default)

Both optional — server runs as before if the module is absent.
Open failure: single WARN log, reader thread exits, server keeps
serving WiFi sensing.

Verified live: distance 149-153 cm at ~6 Hz, REST returns fresh
readings with age_ms 55-127.

Out of scope (logged in ADR-121): Engineering Mode binary frames,
vitals cross-check vs ADR-021, W-MLP feature fusion, auto-reconnect.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:27:28 +07:00
arsen 831602b584 docs(sensors): correct hardware mapping — nodes 1/2 are camera boards
Operator clarified: nodes 1 and 2 (.101 / .100) are ESP32-S3 + OV-camera
boards (sensor_06, sensor_07 in the photo set), NOT YD-ESP32-23. Nodes
3-6 (.102 / .104 / .105 / .106) are the YD-ESP32-23 boards with u.FL
external-antenna connectors (sensor_08, sensor_09).

Impact: Pack E.2 (WiFlow camera-supervised retrain) is closer than
previously assumed — the camera hardware is already deployed at nodes
1 and 2. Path becomes:
  1. Extend FW with parallel camera_capture.c → stream MJPEG over UDP/HTTP
  2. Run MediaPipe Pose on server (deps already installed in
     ~/.venv/ruview-train from earlier session)
  3. Time-align with existing scripts/align-ground-truth.js
  4. Retrain via scripts/train-wiflow-supervised.js --scale lite

The 4 PCB-strip antennas in sensor_02 map 1:1 to nodes 3-6 — drop-in
upgrade once each board is power-cycled to swap the antenna feed.

README now lists the per-node board type, IP, camera/u.FL status, and
which photos show each. No code changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:13:51 +07:00
arsen 2538fa2fab docs: import hardware photos + sensor inventory
9 photos of the additional sensor/antenna hardware staged for ADR-120+
experimentation (captured 2026-05-18):

  sensor_01  5× u.FL pigtail antennas (bare)
  sensor_02  4× flat PCB-strip 2.4 GHz antennas w/ 3M backing + u.FL
  sensor_03  HLK-LD2402 24 GHz mmWave radar (close-up, chip S1KM0008)
  sensor_04  CP2102 USB-to-UART bridge (AMS1117-3.3 LDO)
  sensor_05  HLK-LD2402 + USB-UART wired together (working setup)
  sensor_06  ESP32-S3 dev board with microSD slot (back)
  sensor_07  ESP32-S3-WROOM with OV-camera + ribbon FFC mounted
  sensor_08  YD-ESP32-23 2022-V1.3 (back) — spare matching nodes 1-6
  sensor_09  YD-ESP32-23 (front) — ESP32-S3-N16R8 + FTDI

assets/sensors/README.md catalogues each photo + suggests where each
piece fits in the roadmap:
  * u.FL antennas → attach to n1/n5 (near-AP, sep_ratio ~0.05 per ADR-118)
  * HLK-LD2402 → vitals ground-truth reference for WiFi pipeline
  * Camera-ESP32-S3 → on-device camera capture for WiFlow Pack E.2 retrain
  * YD-ESP32-23 spare → flashable as node 7 when needed

Photos referenced only from this README, not used by any code path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:09:45 +07:00
arsen 4075b6082d docs: enforce ≤200-line cap on README/CLAUDE/CHECKLIST and 3 ADRs
User-stated rule: README.md and CLAUDE.md must not exceed 200 lines;
all detail goes into docs/ with a link. ADRs also targeted at ≤200.

Before:
  README.md   542 lines
  CLAUDE.md   407 lines
  CHECKLIST   235 lines
  ADR-116     224
  ADR-117     245
  ADR-120     209

After:
  README.md   198 ✓
  CLAUDE.md   149 ✓
  CHECKLIST   199 ✓
  ADR-116     191 ✓
  ADR-117     199 ✓
  ADR-120     200 ✓
  ADR-115/118/119  already under (161 / 193 / 161)

New supporting docs (extracted content):
  docs/use-cases.md     — full deployment-tier catalogue + 60 ADR-041 edge modules
                          + ADR-024 self-learning section, all moved from README
  docs/architecture.md  — pipeline diagram + module breakdown from README
  docs/dev-handbook.md  — crate map, RuvSense modules, build/firmware/release
                          /publish, witness verification — all moved from CLAUDE.md
  docs/claude-swarm.md  — V3 CLI commands, agent types, memory commands —
                          moved from CLAUDE.md

Trims (compress prose without losing facts):
  ADR-116 — D7 honesty section + Verified Acceptance + Open Items
  ADR-117 — Context narrative folded to bullets + Out of Scope condensed
  ADR-120 — Out of Scope condensed
  CHECKLIST — adaptive classifier entries compacted + Deferred grouped

CLAUDE.md now adds the ≤200-line rule explicitly to Behavioral Rules
+ Project Architecture + Pre-Merge Checklist so future sessions can't
forget it. README.md was a 67% reduction; CLAUDE.md 63%.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:04:15 +07:00
arsen e2c68191a2 docs: CHECKLIST sweep + .gitignore session artifacts + UI CSS catchup
- CHECKLIST.md: refresh head sha (12e1cf9d), date (2026-05-18),
  count (47 → 50 Done), explicit Done entries for ADR-118/119/120
  with the full session accuracy trajectory (40.4% → 90.40%).
- .gitignore: stop tracking deployment-specific training artifacts:
  v2/data/recordings/ (175 MB each), v2/data/adaptive_model.json
  (regenerated on each retrain), v2/data/baseline.json (regenerated
  on /api/v1/baseline/calibrate).
- ui/style.css: ship the .sensing-class-label color rules for
  present_moving (yellow), waving (purple), transition (orange) —
  written during ADR-117 conversation but missed by that commit.
- git rm --cached v2/data/adaptive_model.json (stays on disk; untracked).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 10:32:39 +07:00
32 changed files with 1969 additions and 1133 deletions

9
.gitignore vendored
View File

@ -258,3 +258,12 @@ v2/crates/rvcsi-node/*.node
v2/crates/rvcsi-node/binding.js
v2/crates/rvcsi-node/binding.d.ts
v2/crates/rvcsi-node/npm/
# ADR-117/118/119/120: deployment-specific training artifacts.
# - recordings/ are large (175 MB each) and room/operator-specific
# - adaptive_model.json is regenerated by `POST /api/v1/adaptive/train`
# - baseline.json is regenerated by `POST /api/v1/baseline/calibrate`
v2/data/recordings/
v2/data/adaptive_model.json
v2/data/baseline.json
.claude/launch.json

View File

@ -5,15 +5,25 @@ at the end of every session. Pair with
[`docs/references/espectre-gap-analysis.md`](docs/references/espectre-gap-analysis.md)
for the technical detail behind each line.
Last sweep: **2026-05-17**, branch `feat/ota-rssi-mobile`, head `0ec1e4b0`.
Status: 47 Done / 0 Open in-scope. Deferred items (out of session scope,
each with explicit reason) listed at the bottom.
Last sweep: **2026-05-18**, branch `feat/ota-rssi-mobile`, head `12e1cf9d`.
Status: **50 Done / 0 Open in-scope**. Deferred listed at the bottom.
This count includes the ADR-100..114 carry-in from the prior agent + this
session's ADR-115 (FW set-target REST), ADR-116 (WiFlow-v1 Rust loader),
ADR-116 cosmetic (UI dropdown), and ADR-117 (process hygiene + audit
follow-ups). ADR-111 is intentionally absent (folded into ADR-109 during
the AP-MAC tracking work).
ADR-100..114 carry-in from the prior agent; ADR-115..120 are this
session. ADR-111 intentionally absent (folded into ADR-109).
Adaptive classifier accuracy trajectory this session — full detail in
ADR-118/119/120:
| Stage | Acc |
|---|---|
| 2-node, 15-feat LogReg (baseline) | 40.4% |
| 6-node, 15-feat LogReg | 44.4% |
| 6-node, 22-feat LogReg (ADR-118) | 49.58% |
| 6-node, 22-feat MLP (ADR-119) | 53.53% |
| 6-node, 22-feat W-MLP (ADR-120) | **90.40%** (training-set) |
W-MLP 90.40% is training-set accuracy; held-out test + cleaner
per-class re-records are the recommended next step.
---
@ -91,6 +101,19 @@ the AP-MAC tracking work).
runtime classifier; sensing tab container restored; multi-node
test guards external :5005; docs/typo/range sweep.
### Adaptive Classifier (data pipeline + model)
- [x] **ADR-118** Feature decorrelation + multi-node extractor (22 feats
= 4 global + 6 nodes × 3 with z-score). Accuracy 44.4% → 49.58%
(`e86f6506`).
- [x] **ADR-119** Frame-level MLP (22→32→6 ReLU+softmax), manual
backprop, ~3k weights. Accuracy 49.58% → 53.53% (`94330708`).
- [x] **ADR-120** Windowed temporal W-MLP (440→64→6, 20×22 stack) —
captures walking / sit-stand / gesture cadence. Accuracy 53.53%
→ 90.40% training; held-out TBD. Hybrid priority (rule-based owns
4 base, W-MLP owns waving/transition) + two-layer label smoothing
+ `/api/v1/adaptive/debug` (`da4c123d`..`12e1cf9d`, 7 commits).
### Tests / fixtures
- [x] **ADR-114** `tests/fixtures/replay_idle.jsonl` +
@ -112,7 +135,7 @@ the AP-MAC tracking work).
### Documentation
- [x] **ADR-100..117** all written (ADR-111 intentionally absent), each ≤ 200 lines
- [x] **ADR-100..120** all written (ADR-111 intentionally absent), each ≤ 200 lines
- [x] `docs/references/espectre-techniques.md` — Pace technique catalogue
- [x] `docs/references/espectre-gap-analysis.md` — section-by-section gap
- [x] Documentation actualization sweep — every Open Items section
@ -151,26 +174,15 @@ ADR-113, see Done above)
### Deferred — out of session scope
Marked here so future sessions don't re-litigate; each line carries
an explicit reason. Bring them back only if scope changes.
Each line carries an explicit reason; revisit if scope changes.
- **HA via MQTT** — new integration. Excluded by current session brief
(no new integrations on current hardware).
- **ESPHome native component** — same reason as HA/MQTT.
- **Web Serial calibration game** — explicitly excluded.
- **Boot-time NBVI freeze in FW** — explicitly excluded.
- **Per-channel NVS cache for gain-lock** — explicitly excluded; only
matters if channel hopping is reactivated, which is also excluded.
- **DensePose model train + load** — explicitly excluded.
- **AETHER contrastive pretrain on live data** — explicitly excluded.
- **MERIDIAN domain generalization** — explicitly excluded.
- **Channel hopping (ADR-029)** — explicitly excluded.
- **Multi-antenna support (`n_antennas` > 1)** — explicitly excluded.
- **README.md trim (542 lines)** — explicitly excluded.
- **CLAUDE.md trim (407 lines)** — explicitly excluded.
- **Tailscale-target in NVS** — Mac stable on TP-Link this session,
low ROI. Not blocking. (ADR-100 follow-up; bring back if Mac
network swap becomes routine.)
* New integrations (excluded by session brief): HA/MQTT, ESPHome,
Web Serial game, DensePose train, AETHER pretrain, MERIDIAN.
* FW changes excluded: boot-time NBVI freeze, per-channel NVS cache
for gain-lock, channel hopping (ADR-029), multi-antenna support
(`n_antennas > 1`).
* **Tailscale-target in NVS** — Mac stable on TP-Link this session;
low ROI. ADR-100 follow-up if Mac network swap becomes routine.
---
@ -178,7 +190,7 @@ an explicit reason. Bring them back only if scope changes.
| Doc | Purpose |
|---|---|
| [`docs/adr/`](docs/adr) | All ADRs 001-117 (111 absent); 100-117 are this session |
| [`docs/adr/`](docs/adr) | All ADRs 001-120 (111 absent); 100-120 are this session |
| [`docs/references/espectre-techniques.md`](docs/references/espectre-techniques.md) | Pace technique catalogue + RuView adoption |
| [`docs/references/espectre-gap-analysis.md`](docs/references/espectre-gap-analysis.md) | Section-by-section gap with priority table |
| [`docs/references/ota-pipeline.md`](docs/references/ota-pipeline.md) | OTA recipe — port 8032, three FW prereqs |

376
CLAUDE.md
View File

@ -1,223 +1,47 @@
# Claude Code Configuration — WiFi-DensePose + Claude Flow V3
# Claude Code Configuration — WiFi-DensePose
## Project: wifi-densepose
WiFi-based human pose estimation using Channel State Information (CSI).
Dual codebase: Python v1 (`v1/`) and Rust port (`v2/`).
### Key Rust Crates
| Crate | Description |
|-------|-------------|
| `wifi-densepose-core` | Core types, traits, error types, CSI frame primitives |
| `wifi-densepose-signal` | SOTA signal processing + RuvSense multistatic sensing (14 modules) |
| `wifi-densepose-nn` | Neural network inference (ONNX, PyTorch, Candle backends) |
| `wifi-densepose-train` | Training pipeline with ruvector integration + ruview_metrics |
| `wifi-densepose-mat` | Mass Casualty Assessment Tool — disaster survivor detection |
| `wifi-densepose-hardware` | ESP32 aggregator, TDM protocol, channel hopping firmware |
| `wifi-densepose-ruvector` | RuVector v2.0.4 integration + cross-viewpoint fusion (5 modules) |
| `wifi-densepose-api` | REST API (Axum) |
| `wifi-densepose-db` | Database layer (Postgres, SQLite, Redis) |
| `wifi-densepose-config` | Configuration management |
| `wifi-densepose-wasm` | WebAssembly bindings for browser deployment |
| `wifi-densepose-cli` | CLI tool (`wifi-densepose` binary) |
| `wifi-densepose-sensing-server` | Lightweight Axum server for WiFi sensing UI |
| `wifi-densepose-wifiscan` | Multi-BSSID WiFi scanning (ADR-022) |
| `wifi-densepose-vitals` | ESP32 CSI-grade vital sign extraction (ADR-021) |
| `nvsim` | Deterministic NV-diamond magnetometer pipeline simulator (ADR-089) — standalone leaf, WASM-ready |
| `vendor/rvcsi` (submodule) | **rvCSI** — edge RF sensing runtime (ADR-095/096): 9 crates (`rvcsi-core`/`-dsp`/`-events`/`-adapter-file`/`-adapter-nexmon`/`-ruvector`/`-runtime`/`-node`/`-cli`). Lives in its own repo ([github.com/ruvnet/rvcsi](https://github.com/ruvnet/rvcsi)), vendored here under `vendor/rvcsi`, published to crates.io as `rvcsi-* 0.3.x` and to npm as `@ruv/rvcsi`. Not a `v2/` workspace member — depend on the published crates (or the submodule's `crates/rvcsi-*` paths). Normalized `CsiFrame`/`CsiWindow`/`CsiEvent` schema, validate-before-FFI, reusable DSP, typed confidence-scored events, the napi-c Nexmon shim (real nexmon_csi `.pcap` from a Raspberry Pi 5 / 4 / 3B+ — BCM43455c0), the napi-rs SDK, the `rvcsi` CLI, a Claude Code plugin. |
WiFi-based human pose estimation from Channel State Information (CSI).
Dual codebase: Python v1 (`archive/v1/`) and Rust port (`v2/`).
### RuvSense Modules (`signal/src/ruvsense/`)
| Module | Purpose |
|--------|---------|
| `multiband.rs` | Multi-band CSI frame fusion, cross-channel coherence |
| `phase_align.rs` | Iterative LO phase offset estimation, circular mean |
| `multistatic.rs` | Attention-weighted fusion, geometric diversity |
| `coherence.rs` | Z-score coherence scoring, DriftProfile |
| `coherence_gate.rs` | Accept/PredictOnly/Reject/Recalibrate gate decisions |
| `pose_tracker.rs` | 17-keypoint Kalman tracker with AETHER re-ID embeddings |
| `field_model.rs` | SVD room eigenstructure, perturbation extraction |
| `tomography.rs` | RF tomography, ISTA L1 solver, voxel grid |
| `longitudinal.rs` | Welford stats, biomechanics drift detection |
| `intention.rs` | Pre-movement lead signals (200-500ms) |
| `cross_room.rs` | Environment fingerprinting, transition graph |
| `gesture.rs` | DTW template matching gesture classifier |
| `adversarial.rs` | Physically impossible signal detection, multi-link consistency |
See **[`CHECKLIST.md`](CHECKLIST.md)** for current implementation status
(50 Done / 0 Open in-scope; ADR-100..120 are this operational session).
### Cross-Viewpoint Fusion (`ruvector/src/viewpoint/`)
| Module | Purpose |
|--------|---------|
| `attention.rs` | CrossViewpointAttention, GeometricBias, softmax with G_bias |
| `geometry.rs` | GeometricDiversityIndex, Cramer-Rao bounds, Fisher Information |
| `coherence.rs` | Phase phasor coherence, hysteresis gate |
| `fusion.rs` | MultistaticArray aggregate root, domain events |
### Detailed handbooks (extracted to keep this file ≤200 lines)
### RuVector v2.0.4 Integration (ADR-016 complete, ADR-017 proposed)
All 5 ruvector crates integrated in workspace:
- `ruvector-mincut``metrics.rs` (DynamicPersonMatcher) + `subcarrier_selection.rs`
- `ruvector-attn-mincut``model.rs` (apply_antenna_attention) + `spectrogram.rs`
- `ruvector-temporal-tensor``dataset.rs` (CompressedCsiBuffer) + `breathing.rs`
- `ruvector-solver``subcarrier.rs` (sparse interpolation 114→56) + `triangulation.rs`
- `ruvector-attention``model.rs` (apply_spatial_attention) + `bvp.rs`
### Architecture Decisions
43 ADRs in `docs/adr/` (ADR-001 through ADR-043). Key ones:
- ADR-014: SOTA signal processing (Accepted)
- ADR-015: MM-Fi + Wi-Pose training datasets (Accepted)
- ADR-016: RuVector training pipeline integration (Accepted — complete)
- ADR-017: RuVector signal + MAT integration (Proposed — next target)
- ADR-024: Contrastive CSI embedding / AETHER (Accepted)
- ADR-027: Cross-environment domain generalization / MERIDIAN (Accepted)
- ADR-028: ESP32 capability audit + witness verification (Accepted)
- ADR-029: RuvSense multistatic sensing mode (Proposed)
- ADR-030: RuvSense persistent field model (Proposed)
- ADR-031: RuView sensing-first RF mode (Proposed)
- ADR-032: Multistatic mesh security hardening (Proposed)
### Supported Hardware
| Device | Port | Chip | Role | Cost |
|--------|------|------|------|------|
| ESP32-S3 (8MB flash) | COM7 | Xtensa dual-core | WiFi CSI sensing node | ~$9 |
| ESP32-S3 SuperMini (4MB) | — | Xtensa dual-core | WiFi CSI (compact) | ~$6 |
| ESP32-C6 + Seeed MR60BHA2 | COM4 | RISC-V + 60 GHz FMCW | mmWave HR/BR/presence | ~$15 |
| HLK-LD2410 | — | 24 GHz FMCW | Presence + distance | ~$3 |
**Not supported:** ESP32 (original), ESP32-C3 — single-core, can't run CSI DSP pipeline.
### Build & Test Commands (this repo)
```bash
# Rust — full workspace tests (1,031+ tests, ~2 min)
cd v2
cargo test --workspace --no-default-features
# Rust — single crate check (no GPU needed)
cargo check -p wifi-densepose-train --no-default-features
# Python — deterministic proof verification (SHA-256)
python archive/v1/data/proof/verify.py
# Python — test suite
cd archive/v1 && python -m pytest tests/ -x -q
```
### ESP32 Firmware Build (Windows — Python subprocess required)
```bash
# Build 8MB firmware (real WiFi CSI mode, no mocks)
# See CLAUDE.local.md for the full Python subprocess command
# Key: must strip MSYSTEM env vars for ESP-IDF v5.4 on Git Bash
# Build 4MB firmware
cp sdkconfig.defaults.4mb sdkconfig.defaults
# then same build process
# Flash to COM7
# [python, idf_py, '-p', 'COM7', 'flash']
# Provision WiFi
python firmware/esp32-csi-node/provision.py --port COM7 \
--ssid "YourWiFi" --password "secret" --target-ip 192.168.1.20
# Monitor serial
python -m serial.tools.miniterm COM7 115200
```
### Firmware Release Process
1. Build 8MB from `sdkconfig.defaults.template` (no mock)
2. Build 4MB from `sdkconfig.defaults.4mb` (no mock)
3. Save 6 binaries: `esp32-csi-node.bin`, `bootloader.bin`, `partition-table.bin`, `ota_data_initial.bin`, `esp32-csi-node-4mb.bin`, `partition-table-4mb.bin`
4. Tag: `git tag v0.X.Y-esp32 && git push origin v0.X.Y-esp32`
5. Release: `gh release create v0.X.Y-esp32 <binaries> --title "..." --notes-file ...`
6. Verify on real hardware (COM7) before publishing
7. **CRITICAL:** Always test with real WiFi CSI, not mock mode — mock missed the Kconfig threshold bug
### Crate Publishing Order
Crates must be published in dependency order:
1. `wifi-densepose-core` (no internal deps)
2. `wifi-densepose-vitals` (no internal deps)
3. `wifi-densepose-wifiscan` (no internal deps)
4. `wifi-densepose-hardware` (no internal deps)
5. `wifi-densepose-config` (no internal deps)
6. `wifi-densepose-db` (no internal deps)
7. `wifi-densepose-signal` (depends on core)
8. `wifi-densepose-nn` (no internal deps, workspace only)
9. `wifi-densepose-ruvector` (no internal deps, workspace only)
10. `wifi-densepose-train` (depends on signal, nn)
11. `wifi-densepose-mat` (depends on core, signal, nn)
12. `wifi-densepose-api` (no internal deps)
13. `wifi-densepose-wasm` (depends on mat)
14. `wifi-densepose-sensing-server` (depends on wifiscan)
15. `wifi-densepose-cli` (depends on mat)
### Validation & Witness Verification (ADR-028)
**After any significant code change, run the full validation:**
```bash
# 1. Rust tests — must be 1,031+ passed, 0 failed
cd v2
cargo test --workspace --no-default-features
# 2. Python proof — must print VERDICT: PASS
cd ..
python archive/v1/data/proof/verify.py
# 3. Generate witness bundle (includes both above + firmware hashes)
bash scripts/generate-witness-bundle.sh
# 4. Self-verify the bundle — must be 7/7 PASS
cd dist/witness-bundle-ADR028-*/
bash VERIFY.sh
```
**If the Python proof hash changes** (e.g., numpy/scipy version update):
```bash
# Regenerate the expected hash, then verify it passes
python archive/v1/data/proof/verify.py --generate-hash
python archive/v1/data/proof/verify.py
```
**Witness bundle contents** (`dist/witness-bundle-ADR028-<sha>.tar.gz`):
- `WITNESS-LOG-028.md` — 33-row attestation matrix with evidence per capability
- `ADR-028-esp32-capability-audit.md` — Full audit findings
- `proof/verify.py` + `expected_features.sha256` — Deterministic pipeline proof
- `test-results/rust-workspace-tests.log` — Full cargo test output
- `firmware-manifest/source-hashes.txt` — SHA-256 of all 7 ESP32 firmware files
- `crate-manifest/versions.txt` — All 15 crates with versions
- `VERIFY.sh` — One-command self-verification for recipients
**Key proof artifacts:**
- `archive/v1/data/proof/verify.py` — Trust Kill Switch: feeds reference signal through production pipeline, hashes output
- `archive/v1/data/proof/expected_features.sha256` — Published expected hash
- `archive/v1/data/proof/sample_csi_data.json` — 1,000 synthetic CSI frames (seed=42)
- `docs/WITNESS-LOG-028.md` — 11-step reproducible verification procedure
- `docs/adr/ADR-028-esp32-capability-audit.md` — Complete audit record
### Branch
Default branch: `main`
Active feature branch: `ruvsense-full-implementation` (PR #77)
| File | Covers |
|---|---|
| [`docs/dev-handbook.md`](docs/dev-handbook.md) | Rust crate map (15 crates), RuvSense modules (14), Cross-Viewpoint fusion (5), Architecture Decisions list, supported hardware, build & test commands, ESP32 firmware build + provision, release process, crate publishing order, witness verification (ADR-028) |
| [`docs/claude-swarm.md`](docs/claude-swarm.md) | V3 CLI commands, available agents (60+ types), memory commands reference, Claude Code vs CLI tools |
| [`docs/architecture.md`](docs/architecture.md) | End-to-end pipeline diagram from CSI capture to pose / vital signs / room fingerprint |
| [`docs/use-cases.md`](docs/use-cases.md) | Full deployment-tier catalogue (Everyday / Specialized / Robotics / Extreme) + all 60 ADR-041 edge modules + self-learning system (ADR-024) |
| [`docs/adr/`](docs/adr/) | All 120 ADRs (ADR-111 intentionally absent); session-specific records ADR-100..120 |
---
## Behavioral Rules (Always Enforced)
- Do what has been asked; nothing more, nothing less
- NEVER create files unless they're absolutely necessary for achieving your goal
- NEVER create files unless absolutely necessary for the goal
- ALWAYS prefer editing an existing file to creating a new one
- NEVER proactively create documentation files (*.md) or README files unless explicitly requested
- NEVER save working files, text/mds, or tests to the root folder
- Never continuously check status after spawning a swarm — wait for results
- NEVER save working files, tests or markdown to the root folder
- ALWAYS read a file before editing it
- NEVER commit secrets, credentials, or .env files
- Never continuously check status after spawning a swarm — wait for results
- README.md and CLAUDE.md must each stay ≤ 200 lines; move detail to `docs/` and link
## File Organization
- NEVER save to root folder — use the directories below
- `docs/adr/` — Architecture Decision Records (43 ADRs)
- `docs/adr/` — Architecture Decision Records (currently 120, ADR-111 absent)
- `docs/ddd/` — Domain-Driven Design models
- `v2/crates/` — Rust workspace crates (15 crates)
- `v2/crates/wifi-densepose-signal/src/ruvsense/` — RuvSense multistatic modules (14 files)
- `v2/crates/wifi-densepose-ruvector/src/viewpoint/` — Cross-viewpoint fusion (5 files)
- `v2/crates/` — Rust workspace crates (15+ crates)
- `v2/crates/wifi-densepose-signal/src/ruvsense/` — RuvSense multistatic modules
- `v2/crates/wifi-densepose-ruvector/src/viewpoint/` — Cross-viewpoint fusion
- `v2/crates/wifi-densepose-hardware/src/esp32/` — ESP32 TDM protocol
- `firmware/esp32-csi-node/main/` — ESP32 C firmware (channel hopping, NVS config, TDM)
- `firmware/esp32-csi-node/main/` — ESP32 C firmware (CSI capture, NVS config, OTA, channel hopping)
- `archive/v1/src/` — Python source (core, hardware, services, api)
- `archive/v1/data/proof/` — Deterministic CSI proof bundles
- `.claude-flow/` — Claude Flow coordination state (committed for team sharing)
@ -226,7 +50,7 @@ Active feature branch: `ruvsense-full-implementation` (PR #77)
## Project Architecture
- Follow Domain-Driven Design with bounded contexts
- Keep files under 500 lines
- Keep files under 500 lines; ADRs ≤ 200 lines; README.md and CLAUDE.md ≤ 200 lines
- Use typed interfaces for all public APIs
- Prefer TDD London School (mock-first) for new code
- Use event sourcing for state changes
@ -244,39 +68,38 @@ Active feature branch: `ruvsense-full-implementation` (PR #77)
Before merging any PR, verify each item applies and is addressed:
1. **Rust tests pass**`cargo test --workspace --no-default-features` (1,031+ passed, 0 failed)
1. **Rust tests pass**`cargo test --workspace --no-default-features`
2. **Python proof passes**`python archive/v1/data/proof/verify.py` (VERDICT: PASS)
3. **README.md** — Update platform tables, crate descriptions, hardware tables, feature summaries if scope changed
4. **CLAUDE.md** — Update crate table, ADR list, module tables, version if scope changed
5. **CHANGELOG.md** — Add entry under `[Unreleased]` with what was added/fixed/changed
6. **User guide** (`docs/user-guide.md`) — Update if new data sources, CLI flags, or setup steps were added
7. **ADR index** — Update ADR count in README docs table if a new ADR was created
3. **README.md** — Update if scope changed; verify ≤ 200 lines
4. **CLAUDE.md** — Update if scope changed; verify ≤ 200 lines; move detail into `docs/`
5. **CHANGELOG.md** — Add entry under `[Unreleased]`
6. **User guide** (`docs/user-guide.md`) — Update if new data sources, CLI flags, or setup steps
7. **ADR index** — Update ADR count + range in CHECKLIST and reference tables when a new ADR is created
8. **Witness bundle** — Regenerate if tests or proof hash changed: `bash scripts/generate-witness-bundle.sh`
9. **Docker Hub image**Only rebuild if Dockerfile, dependencies, or runtime behavior changed
9. **Docker Hub image**Rebuild only if Dockerfile / dependencies / runtime behavior changed
10. **Crate publishing** — Only needed if a crate is published to crates.io and its public API changed
11. **`.gitignore`** — Add any new build artifacts or binaries
12. **Security audit** — Run security review for new modules touching hardware/network boundaries
11. **`.gitignore`** — Add any new build artifacts or large deployment-specific data files
12. **Security audit** — Run a security review for new modules touching hardware/network boundaries
## Build & Test
```bash
# Build
npm run build
# Rust — full workspace tests
cargo test --workspace --no-default-features
# Test
npm test
# Lint
npm run lint
# Python — deterministic proof
python archive/v1/data/proof/verify.py
```
- ALWAYS run tests after making code changes
- ALWAYS run tests after code changes
- ALWAYS verify build succeeds before committing
Full per-crate commands and firmware flash recipe: **[`docs/dev-handbook.md`](docs/dev-handbook.md)**.
## Security Rules
- NEVER hardcode API keys, secrets, or credentials in source files
- NEVER commit .env files or any file containing secrets
- NEVER commit `.env` files or any file containing secrets
- Always validate user input at system boundaries
- Always sanitize file paths to prevent directory traversal
- Run `npx @claude-flow/cli@latest security scan` after security-related changes
@ -294,114 +117,33 @@ npm run lint
- MUST initialize the swarm using CLI tools when starting complex tasks
- MUST spawn concurrent agents using Claude Code's Task tool
- Never use CLI tools alone for execution — Task tool agents do the actual work
- Never use CLI tools alone for execution — Task-tool agents do the actual work
- MUST call CLI tools AND Task tool in ONE message for complex work
### 3-Tier Model Routing (ADR-026)
| Tier | Handler | Latency | Cost | Use Cases |
|------|---------|---------|------|-----------|
| **1** | Agent Booster (WASM) | <1ms | $0 | Simple transforms (varconst, add types) Skip LLM |
| **2** | Haiku | ~500ms | $0.0002 | Simple tasks, low complexity (<30%) |
| **3** | Sonnet/Opus | 2-5s | $0.003-0.015 | Complex reasoning, architecture, security (>30%) |
- Always check for `[AGENT_BOOSTER_AVAILABLE]` or `[TASK_MODEL_RECOMMENDATION]` before spawning agents
- Use Edit tool directly when `[AGENT_BOOSTER_AVAILABLE]`
## Swarm Configuration & Anti-Drift
- ALWAYS use hierarchical topology for coding swarms
- Keep maxAgents at 6-8 for tight coordination
- Use specialized strategy for clear role boundaries
- Use `raft` consensus for hive-mind (leader maintains authoritative state)
- Run frequent checkpoints via `post-task` hooks
- Keep shared memory namespace for all agents
```bash
npx @claude-flow/cli@latest swarm init --topology hierarchical --max-agents 8 --strategy specialized
```
Full CLI command reference, agent type catalogue, memory operations and
3-tier model routing: **[`docs/claude-swarm.md`](docs/claude-swarm.md)**.
## Swarm Execution Rules
- ALWAYS use `run_in_background: true` for all agent Task calls
- ALWAYS put ALL agent Task calls in ONE message for parallel execution
- After spawning, STOP — do NOT add more tool calls or check status
- Never poll TaskOutput or check swarm status — trust agents to return
- When agent results arrive, review ALL results before proceeding
1. **Spawn in background** — use `run_in_background: true` for all agent Task calls
2. **Spawn all at once** — put ALL agent Task calls in ONE message for parallel execution
3. **Tell the user** — after spawning, list what each agent is doing
4. **Stop and wait** — after spawning, STOP; do NOT add more tool calls or check status
5. **No polling** — never poll TaskOutput or swarm status; trust agents to return
6. **Synthesize** — when agent results arrive, review ALL before proceeding
7. **No confirmation** — don't ask "should I check?"; just wait for results
## V3 CLI Commands
---
### Core Commands
## Branch
| Command | Subcommands | Description |
|---------|-------------|-------------|
| `init` | 4 | Project initialization |
| `agent` | 8 | Agent lifecycle management |
| `swarm` | 6 | Multi-agent swarm coordination |
| `memory` | 11 | AgentDB memory with HNSW search |
| `task` | 6 | Task creation and lifecycle |
| `session` | 7 | Session state management |
| `hooks` | 17 | Self-learning hooks + 12 workers |
| `hive-mind` | 6 | Byzantine fault-tolerant consensus |
### Quick CLI Examples
```bash
npx @claude-flow/cli@latest init --wizard
npx @claude-flow/cli@latest agent spawn -t coder --name my-coder
npx @claude-flow/cli@latest swarm init --v3-mode
npx @claude-flow/cli@latest memory search --query "authentication patterns"
npx @claude-flow/cli@latest doctor --fix
```
## Available Agents (60+ Types)
### Core Development
`coder`, `reviewer`, `tester`, `planner`, `researcher`
### Specialized
`security-architect`, `security-auditor`, `memory-specialist`, `performance-engineer`
### Swarm Coordination
`hierarchical-coordinator`, `mesh-coordinator`, `adaptive-coordinator`
### GitHub & Repository
`pr-manager`, `code-review-swarm`, `issue-tracker`, `release-manager`
### SPARC Methodology
`sparc-coord`, `sparc-coder`, `specification`, `pseudocode`, `architecture`
## Memory Commands Reference
```bash
# Store (REQUIRED: --key, --value; OPTIONAL: --namespace, --ttl, --tags)
npx @claude-flow/cli@latest memory store --key "pattern-auth" --value "JWT with refresh" --namespace patterns
# Search (REQUIRED: --query; OPTIONAL: --namespace, --limit, --threshold)
npx @claude-flow/cli@latest memory search --query "authentication patterns"
# List (OPTIONAL: --namespace, --limit)
npx @claude-flow/cli@latest memory list --namespace patterns --limit 10
# Retrieve (REQUIRED: --key; OPTIONAL: --namespace)
npx @claude-flow/cli@latest memory retrieve --key "pattern-auth" --namespace patterns
```
## Quick Setup
```bash
claude mcp add claude-flow -- npx -y @claude-flow/cli@latest
npx @claude-flow/cli@latest daemon start
npx @claude-flow/cli@latest doctor --fix
```
## Claude Code vs CLI Tools
- Claude Code's Task tool handles ALL execution: agents, file ops, code generation, git
- CLI tools handle coordination via Bash: swarm init, memory, hooks, routing
- NEVER use CLI tools as a substitute for Task tool agents
Default branch: `main`.
Current operator branch (this session series): `feat/ota-rssi-mobile`
PR [#596](https://github.com/ruvnet/RuView/pull/596) on the upstream fork.
## Support
- Documentation: https://github.com/ruvnet/claude-flow
- Issues: https://github.com/ruvnet/claude-flow/issues
- GitHub Issues: <https://github.com/ruvnet/RuView/issues>
- ADR index: [`docs/adr/`](docs/adr/)
- Implementation status: [`CHECKLIST.md`](CHECKLIST.md)
- Detailed dev handbook: [`docs/dev-handbook.md`](docs/dev-handbook.md)

384
README.md
View File

@ -36,14 +36,14 @@ RuView also supports pose estimation (17 COCO keypoints via the WiFlow architect
### Built for low-power edge applications
[Edge modules](#edge-intelligence-adr-041) are small programs that run directly on the ESP32 sensor — no internet needed, no cloud fees, instant response.
[Edge modules](docs/use-cases.md) are small programs that run directly on the ESP32 sensor — no internet needed, no cloud fees, instant response.
[![Rust 1.85+](https://img.shields.io/badge/rust-1.85+-orange.svg)](https://www.rust-lang.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Tests: 1463](https://img.shields.io/badge/tests-1463%20passed-brightgreen.svg)](https://github.com/ruvnet/RuView)
[![Docker: multi-arch](https://img.shields.io/badge/docker-amd64%20%2B%20arm64-blue.svg)](https://hub.docker.com/r/ruvnet/wifi-densepose)
[![Vital Signs](https://img.shields.io/badge/vital%20signs-breathing%20%2B%20heartbeat-red.svg)](#vital-sign-detection)
[![ESP32 Ready](https://img.shields.io/badge/ESP32--S3-CSI%20streaming-purple.svg)](#esp32-s3-hardware-pipeline)
[![Vital Signs](https://img.shields.io/badge/vital%20signs-breathing%20%2B%20heartbeat-red.svg)](docs/use-cases.md)
[![ESP32 Ready](https://img.shields.io/badge/ESP32--S3-CSI%20streaming-purple.svg)](docs/use-cases.md)
[![crates.io](https://img.shields.io/crates/v/wifi-densepose-ruvector.svg)](https://crates.io/crates/wifi-densepose-ruvector)
@ -97,7 +97,6 @@ node scripts/mincut-person-counter.js --port 5006 # Correct person counting
>
---
<a href="https://ruvnet.github.io/RuView/">
<img src="assets/v2-screen.png" alt="WiFi DensePose — Live pose detection with setup guide" width="800">
</a>
@ -110,378 +109,35 @@ node scripts/mincut-person-counter.js --port 5006 # Correct person counting
&nbsp;|&nbsp;
<a href="https://ruvnet.github.io/RuView/pointcloud/"><strong>▶ Live 3D Point Cloud</strong></a>
> The [server](#-quick-start) is optional for visualization and aggregation — the ESP32 [runs independently](#esp32-s3-hardware-pipeline) for presence detection, vital signs, and fall alerts.
> The [server](docs/dev-handbook.md) is optional for visualization and aggregation — the ESP32 [runs independently](docs/architecture.md) for presence detection, vital signs, and fall alerts.
>
> **Live ESP32 pipeline**: Connect an ESP32-S3 node → run the [sensing server](#sensing-server) → open the [pose fusion demo](https://ruvnet.github.io/RuView/pose-fusion.html) for real-time dual-modal pose estimation (webcam + WiFi CSI). See [ADR-059](docs/adr/ADR-059-live-esp32-csi-pipeline.md).
> **Live ESP32 pipeline**: Connect an ESP32-S3 node → run the [sensing server](docs/dev-handbook.md) → open the [pose fusion demo](https://ruvnet.github.io/RuView/pose-fusion.html) for real-time dual-modal pose estimation (webcam + WiFi CSI). See [ADR-059](docs/adr/ADR-059-live-esp32-csi-pipeline.md).
## 🔬 How It Works
WiFi routers flood every room with radio waves. When a person moves — or even breathes — those waves scatter differently. WiFi DensePose reads that scattering pattern and reconstructs what happened:
WiFi routers flood every room with radio waves. RuView's ESP32 mesh
captures CSI from those waves, fuses it across channels and nodes, and
feeds a coherence-gated signal pipeline into an attention-graph neural
network that outputs pose keypoints, vital signs, and room fingerprints.
```
WiFi Router → radio waves pass through room → hit human body → scatter
ESP32 mesh (4-6 nodes) captures CSI on channels 1/6/11 via TDM protocol
Multi-Band Fusion: 3 channels × 56 subcarriers = 168 virtual subcarriers per link
Multistatic Fusion: N×(N-1) links → attention-weighted cross-viewpoint embedding
Coherence Gate: accept/reject measurements → stable for days without tuning
Signal Processing: Hampel, SpotFi, Fresnel, BVP, spectrogram → clean features
AI Backbone (RuVector): attention, graph algorithms, compression, field model
Signal-Line Protocol (CRV): 6-stage gestalt → sensory → topology → coherence → search → model
Neural Network: processed signals → 17 body keypoints + vital signs + room model
Output: real-time pose, breathing, heart rate, room fingerprint, drift alerts
```
No training cameras required — the [Self-Learning system (ADR-024)](docs/adr/ADR-024-contrastive-csi-embedding-model.md) bootstraps from raw WiFi data alone. [MERIDIAN (ADR-027)](docs/adr/ADR-027-cross-environment-domain-generalization.md) ensures the model works in any room, not just the one it trained in.
→ **Full pipeline diagram + module-by-module breakdown:**
[`docs/architecture.md`](docs/architecture.md)
---
## 🏢 Use Cases & Applications
WiFi sensing works anywhere WiFi exists. No new hardware in most cases — just software on existing access points or a $8 ESP32 add-on. Because there are no cameras, deployments avoid privacy regulations (GDPR video, HIPAA imaging) by design.
RuView serves four deployment tiers: **Everyday** (healthcare, retail,
office), **Specialized** (events, fitness, education), **Robotics &
Industrial** (cobots, AMRs, manufacturing) and **Extreme** (search &
rescue, defense, underground). Each one comes with a concrete hardware
BOM, expected accuracy, and pointer to the matching ADR-041 edge module.
**Scaling:** Each AP distinguishes ~3-5 people (56 subcarriers). Multi-AP multiplies linearly — a 4-AP retail mesh covers ~15-20 occupants. No hard software limit; the practical ceiling is signal physics.
Also covers the [Self-Learning WiFi AI (ADR-024)](docs/adr/ADR-024-contrastive-csi-embedding-model.md)
(128-dim fingerprint, 55 KB on ESP32) and the full 60-module ADR-041
Edge Intelligence catalogue.
| | Why WiFi sensing wins | Traditional alternative |
|---|----------------------|----------------------|
| 🔒 | **No video, no GDPR/HIPAA imaging rules** | Cameras require consent, signage, data retention policies |
| 🧱 | **Works through walls, shelving, debris** | Cameras need line-of-sight per room |
| 🌙 | **Works in total darkness** | Cameras need IR or visible light |
| 💰 | **$0-$8 per zone** (existing WiFi or ESP32) | Camera systems: $200-$2,000 per zone |
| 🔌 | **WiFi already deployed everywhere** | PIR/radar sensors require new wiring per room |
<details>
<summary><strong>🏥 Everyday</strong> — Healthcare, retail, office, hospitality (commodity WiFi)</summary>
| Use Case | What It Does | Hardware | Key Metric | Edge Module |
|----------|-------------|----------|------------|-------------|
| **Elderly care / assisted living** | Fall detection, nighttime activity monitoring, breathing rate during sleep — no wearable compliance needed | 1 ESP32-S3 per room ($8) | Fall alert <2s | [Sleep Apnea](docs/edge-modules/medical.md), [Gait Analysis](docs/edge-modules/medical.md) |
| **Hospital patient monitoring** | Continuous breathing + heart rate for non-critical beds without wired sensors; nurse alert on anomaly | 1-2 APs per ward | Breathing: 6-30 BPM | [Respiratory Distress](docs/edge-modules/medical.md), [Cardiac Arrhythmia](docs/edge-modules/medical.md) |
| **Emergency room triage** | Automated occupancy count + wait-time estimation; detect patient distress (abnormal breathing) in waiting areas | Existing hospital WiFi | Occupancy accuracy >95% | [Queue Length](docs/edge-modules/retail.md), [Panic Motion](docs/edge-modules/security.md) |
| **Retail occupancy & flow** | Real-time foot traffic, dwell time by zone, queue length — no cameras, no opt-in, GDPR-friendly | Existing store WiFi + 1 ESP32 | Dwell resolution ~1m | [Customer Flow](docs/edge-modules/retail.md), [Dwell Heatmap](docs/edge-modules/retail.md) |
| **Office space utilization** | Which desks/rooms are actually occupied, meeting room no-shows, HVAC optimization based on real presence | Existing enterprise WiFi | Presence latency <1s | [Meeting Room](docs/edge-modules/building.md), [HVAC Presence](docs/edge-modules/building.md) |
| **Hotel & hospitality** | Room occupancy without door sensors, minibar/bathroom usage patterns, energy savings on empty rooms | Existing hotel WiFi | 15-30% HVAC savings | [Energy Audit](docs/edge-modules/building.md), [Lighting Zones](docs/edge-modules/building.md) |
| **Restaurants & food service** | Table turnover tracking, kitchen staff presence, restroom occupancy displays — no cameras in dining areas | Existing WiFi | Queue wait ±30s | [Table Turnover](docs/edge-modules/retail.md), [Queue Length](docs/edge-modules/retail.md) |
| **Parking garages** | Pedestrian presence in stairwells and elevators where cameras have blind spots; security alert if someone lingers | Existing WiFi | Through-concrete walls | [Loitering](docs/edge-modules/security.md), [Elevator Count](docs/edge-modules/building.md) |
</details>
<details>
<summary><strong>🏟️ Specialized</strong> — Events, fitness, education, civic (CSI-capable hardware)</summary>
| Use Case | What It Does | Hardware | Key Metric | Edge Module |
|----------|-------------|----------|------------|-------------|
| **Smart home automation** | Room-level presence triggers (lights, HVAC, music) that work through walls — no dead zones, no motion-sensor timeouts | 2-3 ESP32-S3 nodes ($24) | Through-wall range ~5m | [HVAC Presence](docs/edge-modules/building.md), [Lighting Zones](docs/edge-modules/building.md) |
| **Fitness & sports** | Rep counting, posture correction, breathing cadence during exercise — no wearable, no camera in locker rooms | 3+ ESP32-S3 mesh | Pose: 17 keypoints | [Breathing Sync](docs/edge-modules/exotic.md), [Gait Analysis](docs/edge-modules/medical.md) |
| **Childcare & schools** | Naptime breathing monitoring, playground headcount, restricted-area alerts — privacy-safe for minors | 2-4 ESP32-S3 per zone | Breathing: ±1 BPM | [Sleep Apnea](docs/edge-modules/medical.md), [Perimeter Breach](docs/edge-modules/security.md) |
| **Event venues & concerts** | Crowd density mapping, crush-risk detection via breathing compression, emergency evacuation flow tracking | Multi-AP mesh (4-8 APs) | Density per m² | [Customer Flow](docs/edge-modules/retail.md), [Panic Motion](docs/edge-modules/security.md) |
| **Stadiums & arenas** | Section-level occupancy for dynamic pricing, concession staffing, emergency egress flow modeling | Enterprise AP grid | 15-20 per AP mesh | [Dwell Heatmap](docs/edge-modules/retail.md), [Queue Length](docs/edge-modules/retail.md) |
| **Houses of worship** | Attendance counting without facial recognition — privacy-sensitive congregations, multi-room campus tracking | Existing WiFi | Zone-level accuracy | [Elevator Count](docs/edge-modules/building.md), [Energy Audit](docs/edge-modules/building.md) |
| **Warehouse & logistics** | Worker safety zones, forklift proximity alerts, occupancy in hazardous areas — works through shelving and pallets | Industrial AP mesh | Alert latency <500ms | [Forklift Proximity](docs/edge-modules/industrial.md), [Confined Space](docs/edge-modules/industrial.md) |
| **Civic infrastructure** | Public restroom occupancy (no cameras possible), subway platform crowding, shelter headcount during emergencies | Municipal WiFi + ESP32 | Real-time headcount | [Customer Flow](docs/edge-modules/retail.md), [Loitering](docs/edge-modules/security.md) |
| **Museums & galleries** | Visitor flow heatmaps, exhibit dwell time, crowd bottleneck alerts — no cameras near artwork (flash/theft risk) | Existing WiFi | Zone dwell ±5s | [Dwell Heatmap](docs/edge-modules/retail.md), [Shelf Engagement](docs/edge-modules/retail.md) |
</details>
<details>
<summary><strong>🤖 Robotics & Industrial</strong> — Autonomous systems, manufacturing, android spatial awareness</summary>
WiFi sensing gives robots and autonomous systems a spatial awareness layer that works where LIDAR and cameras fail — through dust, smoke, fog, and around corners. The CSI signal field acts as a "sixth sense" for detecting humans in the environment without requiring line-of-sight.
| Use Case | What It Does | Hardware | Key Metric | Edge Module |
|----------|-------------|----------|------------|-------------|
| **Cobot safety zones** | Detect human presence near collaborative robots — auto-slow or stop before contact, even behind obstructions | 2-3 ESP32-S3 per cell | Presence latency <100ms | [Forklift Proximity](docs/edge-modules/industrial.md), [Perimeter Breach](docs/edge-modules/security.md) |
| **Warehouse AMR navigation** | Autonomous mobile robots sense humans around blind corners, through shelving racks — no LIDAR occlusion | ESP32 mesh along aisles | Through-shelf detection | [Forklift Proximity](docs/edge-modules/industrial.md), [Loitering](docs/edge-modules/security.md) |
| **Android / humanoid spatial awareness** | Ambient human pose sensing for social robots — detect gestures, approach direction, and personal space without cameras always on | Onboard ESP32-S3 module | 17-keypoint pose | [Gesture Language](docs/edge-modules/exotic.md), [Emotion Detection](docs/edge-modules/exotic.md) |
| **Manufacturing line monitoring** | Worker presence at each station, ergonomic posture alerts, headcount for shift compliance — works through equipment | Industrial AP per zone | Pose + breathing | [Confined Space](docs/edge-modules/industrial.md), [Gait Analysis](docs/edge-modules/medical.md) |
| **Construction site safety** | Exclusion zone enforcement around heavy machinery, fall detection from scaffolding, personnel headcount | Ruggedized ESP32 mesh | Alert <2s, through-dust | [Panic Motion](docs/edge-modules/security.md), [Structural Vibration](docs/edge-modules/industrial.md) |
| **Agricultural robotics** | Detect farm workers near autonomous harvesters in dusty/foggy field conditions where cameras are unreliable | Weatherproof ESP32 nodes | Range ~10m open field | [Forklift Proximity](docs/edge-modules/industrial.md), [Rain Detection](docs/edge-modules/exotic.md) |
| **Drone landing zones** | Verify landing area is clear of humans — WiFi sensing works in rain, dust, and low light where downward cameras fail | Ground ESP32 nodes | Presence: >95% accuracy | [Perimeter Breach](docs/edge-modules/security.md), [Tailgating](docs/edge-modules/security.md) |
| **Clean room monitoring** | Personnel tracking without cameras (particle contamination risk from camera fans) — gown compliance via pose | Existing cleanroom WiFi | No particulate emission | [Clean Room](docs/edge-modules/industrial.md), [Livestock Monitor](docs/edge-modules/industrial.md) |
</details>
<details>
<summary><strong>🔥 Extreme</strong> — Through-wall, disaster, defense, underground</summary>
These scenarios exploit WiFi's ability to penetrate solid materials — concrete, rubble, earth — where no optical or infrared sensor can reach. The WiFi-Mat disaster module (ADR-001) is specifically designed for this tier.
| Use Case | What It Does | Hardware | Key Metric | Edge Module |
|----------|-------------|----------|------------|-------------|
| **Search & rescue (WiFi-Mat)** | Detect survivors through rubble/debris via breathing signature, START triage color classification, 3D localization | Portable ESP32 mesh + laptop | Through 30cm concrete | [Respiratory Distress](docs/edge-modules/medical.md), [Seizure Detection](docs/edge-modules/medical.md) |
| **Firefighting** | Locate occupants through smoke and walls before entry; breathing detection confirms life signs remotely | Portable mesh on truck | Works in zero visibility | [Sleep Apnea](docs/edge-modules/medical.md), [Panic Motion](docs/edge-modules/security.md) |
| **Prison & secure facilities** | Cell occupancy verification, distress detection (abnormal vitals), perimeter sensing — no camera blind spots | Dedicated AP infrastructure | 24/7 vital signs | [Cardiac Arrhythmia](docs/edge-modules/medical.md), [Loitering](docs/edge-modules/security.md) |
| **Military / tactical** | Through-wall personnel detection, room clearing confirmation, hostage vital signs at standoff distance | Directional WiFi + custom FW | Range: 5m through wall | [Perimeter Breach](docs/edge-modules/security.md), [Weapon Detection](docs/edge-modules/security.md) |
| **Border & perimeter security** | Detect human presence in tunnels, behind fences, in vehicles — passive sensing, no active illumination to reveal position | Concealed ESP32 mesh | Passive / covert | [Perimeter Breach](docs/edge-modules/security.md), [Tailgating](docs/edge-modules/security.md) |
| **Mining & underground** | Worker presence in tunnels where GPS/cameras fail, breathing detection after collapse, headcount at safety points | Ruggedized ESP32 mesh | Through rock/earth | [Confined Space](docs/edge-modules/industrial.md), [Respiratory Distress](docs/edge-modules/medical.md) |
| **Maritime & naval** | Below-deck personnel tracking through steel bulkheads (limited range, requires tuning), man-overboard detection | Ship WiFi + ESP32 | Through 1-2 bulkheads | [Structural Vibration](docs/edge-modules/industrial.md), [Panic Motion](docs/edge-modules/security.md) |
| **Wildlife research** | Non-invasive animal activity monitoring in enclosures or dens — no light pollution, no visual disturbance | Weatherproof ESP32 nodes | Zero light emission | [Livestock Monitor](docs/edge-modules/industrial.md), [Dream Stage](docs/edge-modules/exotic.md) |
</details>
<details>
<summary><strong>🧩 Edge Intelligence (<a href="docs/adr/ADR-041-wasm-module-collection.md">ADR-041</a>)</strong> — 60 WASM modules across 13 categories, all implemented (609 tests)</summary>
Small programs that run directly on the ESP32 sensor — no internet needed, no cloud fees, instant response. Each module is a tiny WASM file (5-30 KB) that you upload to the device over-the-air. It reads WiFi signal data and makes decisions locally in under 10 ms. [ADR-041](docs/adr/ADR-041-wasm-module-collection.md) defines 60 modules across 13 categories — all 60 are implemented with 609 tests passing.
| | Category | Examples |
|---|----------|---------|
| 🏥 | [**Medical & Health**](docs/edge-modules/medical.md) | Sleep apnea detection, cardiac arrhythmia, gait analysis, seizure detection |
| 🔐 | [**Security & Safety**](docs/edge-modules/security.md) | Intrusion detection, perimeter breach, loitering, panic motion |
| 🏢 | [**Smart Building**](docs/edge-modules/building.md) | Zone occupancy, HVAC control, elevator counting, meeting room tracking |
| 🛒 | [**Retail & Hospitality**](docs/edge-modules/retail.md) | Queue length, dwell heatmaps, customer flow, table turnover |
| 🏭 | [**Industrial**](docs/edge-modules/industrial.md) | Forklift proximity, confined space monitoring, structural vibration |
| 🔮 | [**Exotic & Research**](docs/edge-modules/exotic.md) | Sleep staging, emotion detection, sign language, breathing sync |
| 📡 | [**Signal Intelligence**](docs/edge-modules/signal-intelligence.md) | Cleans and sharpens raw WiFi signals — focuses on important regions, filters noise, fills in missing data, and tracks which person is which |
| 🧠 | [**Adaptive Learning**](docs/edge-modules/adaptive-learning.md) | The sensor learns new gestures and patterns on its own over time — no cloud needed, remembers what it learned even after updates |
| 🗺️ | [**Spatial Reasoning**](docs/edge-modules/spatial-temporal.md) | Figures out where people are in a room, which zones matter most, and tracks movement across areas using graph-based spatial logic |
| ⏱️ | [**Temporal Analysis**](docs/edge-modules/spatial-temporal.md) | Learns daily routines, detects when patterns break (someone didn't get up), and verifies safety rules are being followed over time |
| 🛡️ | [**AI Security**](docs/edge-modules/ai-security.md) | Detects signal replay attacks, WiFi jamming, injection attempts, and flags abnormal behavior that could indicate tampering |
| ⚛️ | [**Quantum-Inspired**](docs/edge-modules/autonomous.md) | Uses quantum-inspired math to map room-wide signal coherence and search for optimal sensor configurations |
| 🤖 | [**Autonomous & Exotic**](docs/edge-modules/autonomous.md) | Self-managing sensor mesh — auto-heals dropped nodes, plans its own actions, and explores experimental signal representations |
All implemented modules are `no_std` Rust, share a [common utility library](v2/crates/wifi-densepose-wasm-edge/src/vendor_common.rs), and talk to the host through a 12-function API. Full documentation: [**Edge Modules Guide**](docs/edge-modules/README.md). See the [complete implemented module list](#edge-module-list) below.
</details>
<details id="edge-module-list">
<summary><strong>🧩 Edge Intelligence — <a href="docs/edge-modules/README.md">All 65 Modules Implemented</a></strong> (ADR-041 complete)</summary>
All 60 modules are implemented, tested (609 tests passing), and ready to deploy. They compile to `wasm32-unknown-unknown`, run on ESP32-S3 via WASM3, and share a [common utility library](v2/crates/wifi-densepose-wasm-edge/src/vendor_common.rs). Source: [`crates/wifi-densepose-wasm-edge/src/`](v2/crates/wifi-densepose-wasm-edge/src/)
**Core modules** (ADR-040 flagship + early implementations):
| Module | File | What It Does |
|--------|------|-------------|
| Gesture Classifier | [`gesture.rs`](v2/crates/wifi-densepose-wasm-edge/src/gesture.rs) | DTW template matching for hand gestures |
| Coherence Filter | [`coherence.rs`](v2/crates/wifi-densepose-wasm-edge/src/coherence.rs) | Phase coherence gating for signal quality |
| Adversarial Detector | [`adversarial.rs`](v2/crates/wifi-densepose-wasm-edge/src/adversarial.rs) | Detects physically impossible signal patterns |
| Intrusion Detector | [`intrusion.rs`](v2/crates/wifi-densepose-wasm-edge/src/intrusion.rs) | Human vs non-human motion classification |
| Occupancy Counter | [`occupancy.rs`](v2/crates/wifi-densepose-wasm-edge/src/occupancy.rs) | Zone-level person counting |
| Vital Trend | [`vital_trend.rs`](v2/crates/wifi-densepose-wasm-edge/src/vital_trend.rs) | Long-term breathing and heart rate trending |
| RVF Parser | [`rvf.rs`](v2/crates/wifi-densepose-wasm-edge/src/rvf.rs) | RVF container format parsing |
**Vendor-integrated modules** (24 modules, ADR-041 Category 7):
**📡 Signal Intelligence** — Real-time CSI analysis and feature extraction
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| Flash Attention | [`sig_flash_attention.rs`](v2/crates/wifi-densepose-wasm-edge/src/sig_flash_attention.rs) | Tiled attention over 8 subcarrier groups — finds spatial focus regions and entropy | S (<5ms) |
| Coherence Gate | [`sig_coherence_gate.rs`](v2/crates/wifi-densepose-wasm-edge/src/sig_coherence_gate.rs) | Z-score phasor gating with hysteresis: Accept / PredictOnly / Reject / Recalibrate | L (<2ms) |
| Temporal Compress | [`sig_temporal_compress.rs`](v2/crates/wifi-densepose-wasm-edge/src/sig_temporal_compress.rs) | 3-tier adaptive quantization (8-bit hot / 5-bit warm / 3-bit cold) | L (<2ms) |
| Sparse Recovery | [`sig_sparse_recovery.rs`](v2/crates/wifi-densepose-wasm-edge/src/sig_sparse_recovery.rs) | ISTA L1 reconstruction for dropped subcarriers | H (<10ms) |
| Person Match | [`sig_mincut_person_match.rs`](v2/crates/wifi-densepose-wasm-edge/src/sig_mincut_person_match.rs) | Hungarian-lite bipartite assignment for multi-person tracking | S (<5ms) |
| Optimal Transport | [`sig_optimal_transport.rs`](v2/crates/wifi-densepose-wasm-edge/src/sig_optimal_transport.rs) | Sliced Wasserstein-1 distance with 4 projections | L (<2ms) |
**🧠 Adaptive Learning** — On-device learning without cloud connectivity
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| DTW Gesture Learn | [`lrn_dtw_gesture_learn.rs`](v2/crates/wifi-densepose-wasm-edge/src/lrn_dtw_gesture_learn.rs) | User-teachable gesture recognition — 3-rehearsal protocol, 16 templates | S (<5ms) |
| Anomaly Attractor | [`lrn_anomaly_attractor.rs`](v2/crates/wifi-densepose-wasm-edge/src/lrn_anomaly_attractor.rs) | 4D dynamical system attractor classification with Lyapunov exponents | H (<10ms) |
| Meta Adapt | [`lrn_meta_adapt.rs`](v2/crates/wifi-densepose-wasm-edge/src/lrn_meta_adapt.rs) | Hill-climbing self-optimization with safety rollback | L (<2ms) |
| EWC Lifelong | [`lrn_ewc_lifelong.rs`](v2/crates/wifi-densepose-wasm-edge/src/lrn_ewc_lifelong.rs) | Elastic Weight Consolidation — remembers past tasks while learning new ones | S (<5ms) |
**🗺️ Spatial Reasoning** — Location, proximity, and influence mapping
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| PageRank Influence | [`spt_pagerank_influence.rs`](v2/crates/wifi-densepose-wasm-edge/src/spt_pagerank_influence.rs) | 4x4 cross-correlation graph with power iteration PageRank | L (<2ms) |
| Micro HNSW | [`spt_micro_hnsw.rs`](v2/crates/wifi-densepose-wasm-edge/src/spt_micro_hnsw.rs) | 64-vector navigable small-world graph for nearest-neighbor search | S (<5ms) |
| Spiking Tracker | [`spt_spiking_tracker.rs`](v2/crates/wifi-densepose-wasm-edge/src/spt_spiking_tracker.rs) | 32 LIF neurons + 4 output zone neurons with STDP learning | S (<5ms) |
**⏱️ Temporal Analysis** — Activity patterns, logic verification, autonomous planning
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| Pattern Sequence | [`tmp_pattern_sequence.rs`](v2/crates/wifi-densepose-wasm-edge/src/tmp_pattern_sequence.rs) | Activity routine detection and deviation alerts | S (<5ms) |
| Temporal Logic Guard | [`tmp_temporal_logic_guard.rs`](v2/crates/wifi-densepose-wasm-edge/src/tmp_temporal_logic_guard.rs) | LTL formula verification on CSI event streams | S (<5ms) |
| GOAP Autonomy | [`tmp_goap_autonomy.rs`](v2/crates/wifi-densepose-wasm-edge/src/tmp_goap_autonomy.rs) | Goal-Oriented Action Planning for autonomous module management | S (<5ms) |
**🛡️ AI Security** — Tamper detection and behavioral anomaly profiling
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| Prompt Shield | [`ais_prompt_shield.rs`](v2/crates/wifi-densepose-wasm-edge/src/ais_prompt_shield.rs) | FNV-1a replay detection, injection detection (10x amplitude), jamming (SNR) | L (<2ms) |
| Behavioral Profiler | [`ais_behavioral_profiler.rs`](v2/crates/wifi-densepose-wasm-edge/src/ais_behavioral_profiler.rs) | 6D behavioral profile with Mahalanobis anomaly scoring | S (<5ms) |
**⚛️ Quantum-Inspired** — Quantum computing metaphors applied to CSI analysis
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| Quantum Coherence | [`qnt_quantum_coherence.rs`](v2/crates/wifi-densepose-wasm-edge/src/qnt_quantum_coherence.rs) | Bloch sphere mapping, Von Neumann entropy, decoherence detection | S (<5ms) |
| Interference Search | [`qnt_interference_search.rs`](v2/crates/wifi-densepose-wasm-edge/src/qnt_interference_search.rs) | 16 room-state hypotheses with Grover-inspired oracle + diffusion | S (<5ms) |
**🤖 Autonomous Systems** — Self-governing and self-healing behaviors
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| Psycho-Symbolic | [`aut_psycho_symbolic.rs`](v2/crates/wifi-densepose-wasm-edge/src/aut_psycho_symbolic.rs) | 16-rule forward-chaining knowledge base with contradiction detection | S (<5ms) |
| Self-Healing Mesh | [`aut_self_healing_mesh.rs`](v2/crates/wifi-densepose-wasm-edge/src/aut_self_healing_mesh.rs) | 8-node mesh with health tracking, degradation/recovery, coverage healing | S (<5ms) |
**🔮 Exotic (Vendor)** — Novel mathematical models for CSI interpretation
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| Time Crystal | [`exo_time_crystal.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_time_crystal.rs) | Autocorrelation subharmonic detection in 256-frame history | S (<5ms) |
| Hyperbolic Space | [`exo_hyperbolic_space.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_hyperbolic_space.rs) | Poincare ball embedding with 32 reference locations, hyperbolic distance | S (<5ms) |
**🏥 Medical & Health** (Category 1) — Contactless health monitoring
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| Sleep Apnea | [`med_sleep_apnea.rs`](v2/crates/wifi-densepose-wasm-edge/src/med_sleep_apnea.rs) | Detects breathing pauses during sleep | S (<5ms) |
| Cardiac Arrhythmia | [`med_cardiac_arrhythmia.rs`](v2/crates/wifi-densepose-wasm-edge/src/med_cardiac_arrhythmia.rs) | Monitors heart rate for irregular rhythms | S (<5ms) |
| Respiratory Distress | [`med_respiratory_distress.rs`](v2/crates/wifi-densepose-wasm-edge/src/med_respiratory_distress.rs) | Alerts on abnormal breathing patterns | S (<5ms) |
| Gait Analysis | [`med_gait_analysis.rs`](v2/crates/wifi-densepose-wasm-edge/src/med_gait_analysis.rs) | Tracks walking patterns and detects changes | S (<5ms) |
| Seizure Detection | [`med_seizure_detect.rs`](v2/crates/wifi-densepose-wasm-edge/src/med_seizure_detect.rs) | 6-state machine for tonic-clonic seizure recognition | S (<5ms) |
**🔐 Security & Safety** (Category 2) — Perimeter and threat detection
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| Perimeter Breach | [`sec_perimeter_breach.rs`](v2/crates/wifi-densepose-wasm-edge/src/sec_perimeter_breach.rs) | Detects boundary crossings with approach/departure | S (<5ms) |
| Weapon Detection | [`sec_weapon_detect.rs`](v2/crates/wifi-densepose-wasm-edge/src/sec_weapon_detect.rs) | Metal anomaly detection via CSI amplitude shifts | S (<5ms) |
| Tailgating | [`sec_tailgating.rs`](v2/crates/wifi-densepose-wasm-edge/src/sec_tailgating.rs) | Detects unauthorized follow-through at access points | S (<5ms) |
| Loitering | [`sec_loitering.rs`](v2/crates/wifi-densepose-wasm-edge/src/sec_loitering.rs) | Alerts when someone lingers too long in a zone | S (<5ms) |
| Panic Motion | [`sec_panic_motion.rs`](v2/crates/wifi-densepose-wasm-edge/src/sec_panic_motion.rs) | Detects fleeing, struggling, or panic movement | S (<5ms) |
**🏢 Smart Building** (Category 3) — Automation and energy efficiency
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| HVAC Presence | [`bld_hvac_presence.rs`](v2/crates/wifi-densepose-wasm-edge/src/bld_hvac_presence.rs) | Occupancy-driven HVAC control with departure countdown | S (<5ms) |
| Lighting Zones | [`bld_lighting_zones.rs`](v2/crates/wifi-densepose-wasm-edge/src/bld_lighting_zones.rs) | Auto-dim/off lighting based on zone activity | S (<5ms) |
| Elevator Count | [`bld_elevator_count.rs`](v2/crates/wifi-densepose-wasm-edge/src/bld_elevator_count.rs) | Counts people entering/leaving with overload warning | S (<5ms) |
| Meeting Room | [`bld_meeting_room.rs`](v2/crates/wifi-densepose-wasm-edge/src/bld_meeting_room.rs) | Tracks meeting lifecycle: start, headcount, end, availability | S (<5ms) |
| Energy Audit | [`bld_energy_audit.rs`](v2/crates/wifi-densepose-wasm-edge/src/bld_energy_audit.rs) | Tracks after-hours usage and room utilization rates | S (<5ms) |
**🛒 Retail & Hospitality** (Category 4) — Customer insights without cameras
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| Queue Length | [`ret_queue_length.rs`](v2/crates/wifi-densepose-wasm-edge/src/ret_queue_length.rs) | Estimates queue size and wait times | S (<5ms) |
| Dwell Heatmap | [`ret_dwell_heatmap.rs`](v2/crates/wifi-densepose-wasm-edge/src/ret_dwell_heatmap.rs) | Shows where people spend time (hot/cold zones) | S (<5ms) |
| Customer Flow | [`ret_customer_flow.rs`](v2/crates/wifi-densepose-wasm-edge/src/ret_customer_flow.rs) | Counts ins/outs and tracks net occupancy | S (<5ms) |
| Table Turnover | [`ret_table_turnover.rs`](v2/crates/wifi-densepose-wasm-edge/src/ret_table_turnover.rs) | Restaurant table lifecycle: seated, dining, vacated | S (<5ms) |
| Shelf Engagement | [`ret_shelf_engagement.rs`](v2/crates/wifi-densepose-wasm-edge/src/ret_shelf_engagement.rs) | Detects browsing, considering, and reaching for products | S (<5ms) |
**🏭 Industrial & Specialized** (Category 5) — Safety and compliance
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| Forklift Proximity | [`ind_forklift_proximity.rs`](v2/crates/wifi-densepose-wasm-edge/src/ind_forklift_proximity.rs) | Warns when people get too close to vehicles | S (<5ms) |
| Confined Space | [`ind_confined_space.rs`](v2/crates/wifi-densepose-wasm-edge/src/ind_confined_space.rs) | OSHA-compliant worker monitoring with extraction alerts | S (<5ms) |
| Clean Room | [`ind_clean_room.rs`](v2/crates/wifi-densepose-wasm-edge/src/ind_clean_room.rs) | Occupancy limits and turbulent motion detection | S (<5ms) |
| Livestock Monitor | [`ind_livestock_monitor.rs`](v2/crates/wifi-densepose-wasm-edge/src/ind_livestock_monitor.rs) | Animal presence, stillness, and escape alerts | S (<5ms) |
| Structural Vibration | [`ind_structural_vibration.rs`](v2/crates/wifi-densepose-wasm-edge/src/ind_structural_vibration.rs) | Seismic events, mechanical resonance, structural drift | S (<5ms) |
**🔮 Exotic & Research** (Category 6) — Experimental sensing applications
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| Dream Stage | [`exo_dream_stage.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_dream_stage.rs) | Contactless sleep stage classification (wake/light/deep/REM) | S (<5ms) |
| Emotion Detection | [`exo_emotion_detect.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_emotion_detect.rs) | Arousal, stress, and calm detection from micro-movements | S (<5ms) |
| Gesture Language | [`exo_gesture_language.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_gesture_language.rs) | Sign language letter recognition via WiFi | S (<5ms) |
| Music Conductor | [`exo_music_conductor.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_music_conductor.rs) | Tempo and dynamic tracking from conducting gestures | S (<5ms) |
| Plant Growth | [`exo_plant_growth.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_plant_growth.rs) | Monitors plant growth, circadian rhythms, wilt detection | S (<5ms) |
| Ghost Hunter | [`exo_ghost_hunter.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_ghost_hunter.rs) | Environmental anomaly classification (draft/insect/wind/unknown) | S (<5ms) |
| Rain Detection | [`exo_rain_detect.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_rain_detect.rs) | Detects rain onset, intensity, and cessation via signal scatter | S (<5ms) |
| Breathing Sync | [`exo_breathing_sync.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_breathing_sync.rs) | Detects synchronized breathing between multiple people | S (<5ms) |
</details>
---
<details>
<summary><strong>🧠 Self-Learning WiFi AI (ADR-024)</strong> — Adaptive recognition, self-optimization, and intelligent anomaly detection</summary>
Every WiFi signal that passes through a room creates a unique fingerprint of that space. WiFi-DensePose already reads these fingerprints to track people, but until now it threw away the internal "understanding" after each reading. The Self-Learning WiFi AI captures and preserves that understanding as compact, reusable vectors — and continuously optimizes itself for each new environment.
**What it does in plain terms:**
- Turns any WiFi signal into a 128-number "fingerprint" that uniquely describes what's happening in a room
- Learns entirely on its own from raw WiFi data — no cameras, no labeling, no human supervision needed
- Recognizes rooms, detects intruders, identifies people, and classifies activities using only WiFi
- Runs on an $8 ESP32 chip (the entire model fits in 55 KB of memory)
- Produces both body pose tracking AND environment fingerprints in a single computation
**Key Capabilities**
| What | How it works | Why it matters |
|------|-------------|----------------|
| **Self-supervised learning** | The model watches WiFi signals and teaches itself what "similar" and "different" look like, without any human-labeled data | Deploy anywhere — just plug in a WiFi sensor and wait 10 minutes |
| **Room identification** | Each room produces a distinct WiFi fingerprint pattern | Know which room someone is in without GPS or beacons |
| **Anomaly detection** | An unexpected person or event creates a fingerprint that doesn't match anything seen before | Automatic intrusion and fall detection as a free byproduct |
| **Person re-identification** | Each person disturbs WiFi in a slightly different way, creating a personal signature | Track individuals across sessions without cameras |
| **Environment adaptation** | MicroLoRA adapters (1,792 parameters per room) fine-tune the model for each new space | Adapts to a new room with minimal data — 93% less than retraining from scratch |
| **Memory preservation** | EWC++ regularization remembers what was learned during pretraining | Switching to a new task doesn't erase prior knowledge |
| **Hard-negative mining** | Training focuses on the most confusing examples to learn faster | Better accuracy with the same amount of training data |
**Architecture**
```
WiFi Signal [56 channels] → Transformer + Graph Neural Network
├→ 128-dim environment fingerprint (for search + identification)
└→ 17-joint body pose (for human tracking)
```
**Quick Start**
```bash
# Step 1: Learn from raw WiFi data (no labels needed)
cargo run -p wifi-densepose-sensing-server -- --pretrain --dataset data/csi/ --pretrain-epochs 50
# Step 2: Fine-tune with pose labels for full capability
cargo run -p wifi-densepose-sensing-server -- --train --dataset data/mmfi/ --epochs 100 --save-rvf model.rvf
# Step 3: Use the model — extract fingerprints from live WiFi
cargo run -p wifi-densepose-sensing-server -- --model model.rvf --embed
# Step 4: Search — find similar environments or detect anomalies
cargo run -p wifi-densepose-sensing-server -- --model model.rvf --build-index env
```
**Training Modes**
| Mode | What you need | What you get |
|------|--------------|-------------|
| Self-Supervised | Just raw WiFi data | A model that understands WiFi signal structure |
| Supervised | WiFi data + body pose labels | Full pose tracking + environment fingerprints |
| Cross-Modal | WiFi data + camera footage | Fingerprints aligned with visual understanding |
**Fingerprint Index Types**
| Index | What it stores | Real-world use |
|-------|---------------|----------------|
| `env_fingerprint` | Average room fingerprint | "Is this the kitchen or the bedroom?" |
| `activity_pattern` | Activity boundaries | "Is someone cooking, sleeping, or exercising?" |
| `temporal_baseline` | Normal conditions | "Something unusual just happened in this room" |
| `person_track` | Individual movement signatures | "Person A just entered the living room" |
**Model Size**
| Component | Parameters | Memory (on ESP32) |
|-----------|-----------|-------------------|
| Transformer backbone | ~28,000 | 28 KB |
| Embedding projection head | ~25,000 | 25 KB |
| Per-room MicroLoRA adapter | ~1,800 | 2 KB |
| **Total** | **~55,000** | **55 KB** (of 520 KB available) |
The self-learning system builds on the [AI Backbone (RuVector)](#ai-backbone-ruvector) signal-processing layer — attention, graph algorithms, and compression — adding contrastive learning on top.
See [`docs/adr/ADR-024-contrastive-csi-embedding-model.md`](docs/adr/ADR-024-contrastive-csi-embedding-model.md) for full architectural details.
</details>
→ **Full catalogue + tables: [`docs/use-cases.md`](docs/use-cases.md)**
---

63
assets/sensors/README.md Normal file
View File

@ -0,0 +1,63 @@
# Sensor & antenna hardware inventory
Photos of the deployment's 6-node sensor mesh + external-antenna stock,
captured 2026-05-18. The fleet splits cleanly into two roles: two
**camera-bearing** nodes (1, 2) that can collect ground-truth keypoints
on-device, and four **antenna-upgradeable** nodes (3-6) that supply
spatial coverage.
## Active sensor mesh (6 ESP32-S3 nodes)
| node | IP | Board | Camera | u.FL | Photos |
|---|---|---|---|---|---|
| 1 | 192.168.0.101 | ESP32-S3 + OV camera + microSD + USB-OTG (FFC ribbon) | ✅ | — | [`sensor_06`](sensor_06.jpeg), [`sensor_07`](sensor_07.jpeg) |
| 2 | 192.168.0.100 | same as node 1 | ✅ | — | [`sensor_06`](sensor_06.jpeg), [`sensor_07`](sensor_07.jpeg) |
| 3 | 192.168.0.102 | YD-ESP32-23 V1.3, ESP32-S3-N16R8 + FTDI USB-serial + dual USB-C | — | ✅ | [`sensor_08`](sensor_08.jpeg), [`sensor_09`](sensor_09.jpeg) |
| 4 | 192.168.0.104 | same as node 3 | — | ✅ | same |
| 5 | 192.168.0.105 | same as node 3 | — | ✅ | same |
| 6 | 192.168.0.106 | same as node 3 | — | ✅ | same |
## External antenna stock (for nodes 3-6)
| File | What | Use |
|---|---|---|
| [`sensor_01.jpeg`](sensor_01.jpeg) | 5× u.FL (IPEX-1) pigtail antennas, bare cable | Direct feed via the u.FL connector on YD-ESP32-23 boards (nodes 3-6). Adds gain over the chip antenna; supports polarisation diversity if mounted perpendicular pairs. |
| [`sensor_02.jpeg`](sensor_02.jpeg) | 4× flat PCB-strip 2.4 GHz antennas with 3M double-sided tape backing + u.FL pigtails | Stick-on external antennas — better directivity than the bare pigtail. One per node 3-6 = full set. |
## Auxiliary modality — mmWave radar (vitals ground truth)
| File | What | Use |
|---|---|---|
| [`sensor_03.jpeg`](sensor_03.jpeg) | HLK-LD2402 24 GHz mmWave radar (V1.0, chip `S1KM0008` 2438 batch), TX/RX patch antennas | New sensing modality. mmWave gives sub-mm range to a moving target — ideal for vitals (breathing / pulse) ground truth alongside WiFi CSI. UART output @ 256000 8N1. |
| [`sensor_04.jpeg`](sensor_04.jpeg) | CP2102 USB-to-UART bridge (AMS1117-3.3 LDO, USB-A) | Powers + reads the HLK-LD2402 from the Mac. Pin map: GND / RXT / TXD / 3.3V / RTS / CTS. |
| [`sensor_05.jpeg`](sensor_05.jpeg) | HLK-LD2402 + USB-UART wired together | Plug-and-play setup; module ships with factory firmware, no flashing required. |
## Suggested next moves
* **External antennas for nodes 3-6** — the 4 PCB-strip antennas in
`sensor_02` map 1:1 to the 4 YD-ESP32-23 boards. Power-cycle each
to attach. Per the ADR-118 audit, nodes near the AP currently sit
at sep_ratio ~0.05 — external antennas perpendicular to the body
axis should pull more body modulation into the signal.
* **HLK-LD2402 as vitals ground truth** — connect via USB-UART, log
breathing rate alongside the WiFi vitals detector, compare bias.
Later fuse via `MultistaticFuser` if accuracy delta is material.
Would warrant a fresh ADR.
* **On-device camera capture for WiFlow Pack E.2 retrain** — nodes
1 and 2 already have OV-cameras. Path is:
1. Extend `firmware/esp32-csi-node/` with a parallel
`camera_capture.c` that grabs frames @ ~10 Hz and streams them
to the server as MJPEG over a new UDP port (or HTTP `multipart/x-mixed-replace`).
2. Run MediaPipe Pose on the server (we already have it in
`~/.venv/ruview-train` from this session).
3. Time-align CSI + keypoints via the existing
`scripts/align-ground-truth.js` infrastructure.
4. Train via `scripts/train-wiflow-supervised.js --scale lite`.
This is the cleanest replacement for the awkward "laptop is the
camera AND the server AND in the sensing zone" workaround.
---
These are reference photos. Linked from `docs/use-cases.md` and
`CHECKLIST.md` so future sessions see the available hardware at
a glance.

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

View File

@ -121,23 +121,15 @@ false`.
### D7 — Honest about output quality
The loaded model produces **17 keypoints**, but the **numerical values
are saturated** (most x/y near 0 or 1) — sigmoid extremes meaning the
network has no learned response to our specific deployment's CSI
distribution. This is expected: the model was trained on a different
ESP32 setup, different room, different person, with camera ground truth
we don't have here. **The integration is correct; the model needs
deployment-specific fine-tune to produce useful keypoints.**
The loaded model emits 17 keypoints, but values saturate near 0/1
(sigmoid extremes) — the network was trained on a different ESP32
deployment and has no learned response to ours. Integration is correct;
production-grade output needs deployment-specific fine-tune.
Two paths to usable output, left as follow-ups (Pack E):
1. **Apply `node-1.json` / `node-2.json` LoRA adapters** (ADR-117 candidate)
— they're shipped alongside `wiflow-v1.json` in the same HuggingFace
repo, rank=8, alpha=16, target the encoder + task heads. Loader stub +
forward fold ~2 h.
2. **Re-train via `scripts/train-wiflow-supervised.js` with new ground-
truth capture** (~30 min capture + 19 min training per the model card).
Operator-side work.
Follow-ups (Pack E): apply `node-1`/`node-2` LoRA adapters from the
same HuggingFace repo (~2h), or re-train via
`scripts/train-wiflow-supervised.js` against fresh camera ground-truth
(~30 min capture + 19 min training).
## Files Touched
@ -159,28 +151,12 @@ Binary size delta: 3.0 MB → 3.1 MB.
## Verified Acceptance
Live test on the operator's TP-Link deployment (.103, both nodes
192.168.0.100/.101):
```
$ ./target/release/sensing-server --source esp32 --csi-keepalive-pps 25 \
--wiflow-model data/models/ruview/wiflow-v1/wiflow-v1.json
...
ADR-116 wiflow-v1 loaded from data/models/ruview/wiflow-v1/wiflow-v1.json
(lite scale, 186946 params)
keepalive: learned address for node 2 = 192.168.0.100:63940
keepalive: learned address for node 1 = 192.168.0.101:63844
$ curl :8080/api/v1/info → "pose_estimation": true
$ curl :8080/api/v1/pose/stats → "model_loaded": true, frames_processed: 2699
$ curl :8080/api/v1/pose/current
{ persons: [{id: 1, keypoints: [17 × {name, x, y, z, confidence}], ...}],
total_persons: 1, model_loaded: true }
```
End-to-end: model on disk → loader → forward pass → 17 keypoints → REST &
WS payload. UI's pose canvas (un-gated by ADR-105 D4) now draws what the
model emits.
Live on the operator's TP-Link deployment (Mac .103, nodes .100/.101):
sensing-server log shows `ADR-116 wiflow-v1 loaded ... (lite scale,
186946 params)` + `keepalive: learned address for node 2/1`; `curl
/api/v1/info` returns `"pose_estimation": true`; `curl /api/v1/pose/current`
returns 17 named COCO keypoints under one `persons[0]`. End-to-end:
model on disk → loader → forward pass → 17 keypoints → REST + WS payload.
## Cargo tests
@ -194,23 +170,14 @@ model emits.
## Open Items
* **Pack E.1 — LoRA adapter loader.** `node-1.json` / `node-2.json` rank-8
adapters from the same HF repo, ~21 KB each. The trainer encodes them
in the same custom format as `wiflow-v1.json` (different `format` tag),
so the loader plumbing is small. ~2 h.
* **Pack E.2 — Camera-supervised retraining for this room.** Run
`scripts/collect-ground-truth.py` against this Mac's webcam +
TP-Link/.100/.101 CSI for 5 min, then `scripts/train-wiflow-
supervised.js --scale lite`. Should drop sigmoid saturation and produce
spatially-coherent keypoints. ~1 h operator + 19 min train.
* **Inference rate-limiting.** Currently runs every tick (10 fps). If
multiple WS clients connect, each tick computes once and the result is
reused — fine. If model size grows to small/medium scale (~200K/800K
params), should cache the result per tick instead of computing per-client.
* **Per-node pose tracks.** Right now a single virtual person is emitted;
the broadcaster places it in `zone_1` with a fixed bbox. If/when LoRA
adapters disambiguate per-node viewpoints, fan out to one
`PersonDetection` per node (left/right of the room).
* **Pack E.1 — LoRA adapter loader.** Apply `node-1`/`node-2` rank-8
adapters from the same HF repo (~2 h).
* **Pack E.2 — Camera-supervised retrain for this room.**
`scripts/collect-ground-truth.py` + `scripts/train-wiflow-supervised.js
--scale lite` — should drop sigmoid saturation (~1 h + 19 min train).
* **Inference rate-limit / per-node pose tracks** — currently single
virtual person emitted with fixed `zone_1` bbox; future LoRA-per-node
could fan out to one `PersonDetection` per sensor viewpoint.
## References

View File

@ -10,70 +10,32 @@
## Context
A deep audit pass (4 parallel auditors covering sensors, server, UI, docs)
surfaced two operational fires and a stack of correctness/honesty issues
that had accumulated across ADR-100..116. This ADR collects the immediate
fixes.
A 4-auditor pass (sensors, server, UI, docs) over ADR-100..116
surfaced two operational fires and a stack of correctness/honesty
issues. This ADR collects the immediate fixes.
### Fire 1 — Runaway ping zombies
Live `ps` showed **250+ `/sbin/ping -i 0.040` processes** on the Mac, most
parented to PID 1 (orphans from prior server lifetimes) and **8 fresh
pings to `127.0.0.1` parented to the current server**.
Root cause: a `cargo test --workspace` run sent UDP packets to
`127.0.0.1:5005` from `tests/multi_node_test.rs::test_multi_node_udp_send`
while the production server was bound to `0.0.0.0:5005`. The integration
test injects 55 synthetic frames with `node_ids = [1, 2, 3, 5, 7]`. Each
distinct `node_id` byte in a CSI magic packet triggered a fresh entry in
`NODE_ADDRS`, and the keepalive task spawned exactly one `ping` child
per entry. Combined with macOS not propagating parent death to children
(killed servers leave ping orphans), the count accumulated rapidly.
### Fire 2 — Per-node feature divergence on node 2
Node 2 (192.168.0.100) showed `dominant_freq_hz: 0.05` vs node 1 (.101)
`6.30` — a 126× split in the same room. Pointed to stale gain-lock on
node 2 from a different AP/orientation. Cleared via
`POST /ota/recalibrate` (ADR-109) — sensor re-runs the 300-packet
calibration sampler at next boot.
### Correctness issues (server auditor)
* `run_wiflow_inference` hardcoded keypoint `confidence: 1.0` — lied about
data quality. Real signal: the runtime classifier's `confidence`.
* `wiflow_v1.rs` zero-pad path duplicated subcarrier index 0 instead of
zero-padding when < 35 finite subcarriers comment said "zero the
rest", code did the opposite.
* `nbvi_history.clone()` cloned the entire 600-deep VecDeque (≈270 KB) on
every inference, while only the last 20 frames are used.
* `run_wiflow_inference` picked the node with longest history regardless
of recency — stale data from a dead sensor would keep producing pose.
### UI issues (UI auditor)
* `/` served a static API-index HTML page; users typing `localhost:8080`
never reached the SPA at `/ui/index.html`.
* `<section id="sensing">` was empty; `app.js::SensingTab.mount` queried
`#sensing-container` and rendered into nothing — the Sensing tab was
permanently blank.
* `LiveDemoTab.fetchModels` unconditionally overwrote `activeModelId =
'wiflow-v1'` whenever `/api/v1/info` reported `pose_estimation: true`,
even when the operator had just loaded an RVF model. Dropdown silently
flipped back to WiFlow on every refresh.
### Docs issues (docs auditor)
* `CHECKLIST.md` header: `head c827cde6`, count `43 Done` — stale
by 4 commits and 2 ADRs.
* `ADR-115 References` cited "ADR-100 — TP-Link WISP" (it's ADR-110)
and "ADR-108 / ADR-111" (ADR-111 doesn't exist — folded into ADR-109).
* `espectre-gap-analysis.md::Still open` table listed 8 items as open
that had already shipped (ADR-104, ADR-109, ADR-112, ADR-114).
* `ota-pipeline.md` documented OTA flashing but never mentioned
`/ota/set-target` (ADR-115) or `/ota/recalibrate` (ADR-109) — operator
hitting the "Mac moved networks" scenario wouldn't find the recovery
path.
* **Fire 1 — Ping zombies.** `ps` showed 250+ `/sbin/ping -i 0.040`
processes — orphans from prior server lifetimes + 8 fresh pings to
`127.0.0.1` parented to the current server. Root cause:
`cargo test --workspace` sent UDP frames to `127.0.0.1:5005` from
`tests/multi_node_test.rs` with `node_ids = [1,2,3,5,7]` — each
unique nid registered in `NODE_ADDRS`, keepalive spawned one `ping`
child per nid, macOS doesn't propagate parent death.
* **Fire 2 — Node 2 feature divergence.** `dominant_freq_hz` 0.05 (n2)
vs 6.30 (n1), same room, 126×. Stale gain-lock from prior AP geometry.
Fixed via `POST /ota/recalibrate` (ADR-109).
* **Correctness:** hardcoded keypoint `confidence: 1.0`, `wiflow_v1.rs`
zero-pad path duplicated subcarrier 0, `nbvi_history.clone()` copied
full 600-deep deque per tick, `run_wiflow_inference` ignored node
staleness.
* **UI:** `/` served static API index (SPA was at `/ui/index.html`),
`<section id="sensing">` was empty (no `sensing-container` div),
`LiveDemoTab.fetchModels` overrode the operator's RVF selection on
every poll.
* **Docs:** `CHECKLIST.md` header stale by 4 commits / 2 ADRs;
`ADR-115` cited wrong sister ADRs ("ADR-100" → ADR-110, "ADR-111" → ADR-109);
`espectre-gap-analysis.md` listed 8 shipped items as open;
`ota-pipeline.md` never documented the post-flash REST endpoints.
## Decisions
@ -212,25 +174,17 @@ $ curl http://localhost:8080/api/v1/pose/current | jq '.persons[0].keypoints[0]'
## Out of Scope (intentional non-fixes)
* **Health endpoint fake constants** (cpu:2.5, mem:1.8, disk:15.0) —
flagged by the auditor as critical. Replacing with `sysinfo` crate
would add a dependency for low-value telemetry; the orchestrator
readiness probe today is only used by Docker compose, not Kubernetes
liveness. Deferred. Real fix: `/health/ready` only reports
`model_loaded` + `node_count > 0`.
* **`derive_pose_from_sensing` call-site cleanup** — function returns
`Vec::new()` since ADR-105; removing the 5 call sites is a no-op
refactor with no behaviour change. Skipped to keep diff focused.
* **`tracker_bridge:10` unused imports warning** — module is integrated
via `tracker_bridge::tracker_update` (4 callers), the import list
just has dead names. Cosmetic. `cargo fix` deferred.
* **Health endpoint fake constants** (cpu/mem/disk hardcoded) — adding
`sysinfo` crate just for orchestrator telemetry is heavy. Deferred.
* **`derive_pose_from_sensing` call-site cleanup** — already returns
`Vec::new()` (ADR-105); removing 5 call sites is no-op refactor.
* **`tracker_bridge:10` unused-imports warning** — module is integrated
via `tracker_update` (4 callers); import list has dead names. Cosmetic.
* **CLI training flags** (`--train`, `--dataset`, `--epochs`,
`--checkpoint-dir`, `--pretrain*`) — silent no-ops; training is via
REST. Removing the flags would break any operator script that passes
them harmlessly. Deferred to a separate flag-audit pass.
* **OTA PSK provisioning** — operator workflow change, not a code
change. Note added to ADR-115 open items. Operator can set
`security/ota_psk` via USB provision.py whenever convenient.
REST. Removing flags would break operator scripts. Deferred audit.
* **OTA PSK provisioning** — workflow change, not a code change.
Logged in ADR-115 open items.
## References

View File

@ -176,27 +176,18 @@ docs/adr/ADR-120-windowed-temporal-classifier.md (this)
## Out of Scope / Follow-ups
* **Held-out test set** — must record fresh data and evaluate the saved
model cold. Critical to confirm 90% is not training-set memorisation.
* **TCN replacing stacked-MLP** — true 1D convolutions over time would
use weights more efficiently (~5k vs 28k) and generalise better.
Stack-MLP works but is parameter-heavy. Worth a follow-up if data
scales 10×.
* **Sliding output smoothing**`classify_window` emits one decision
per tick (~10 Hz). Adjacent windows are 19/20 identical, so adjacent
predictions should agree. They mostly do (98%+) but flicker at class
boundaries — could apply a 3-tick majority filter.
* **`sitting` vs `standing` split** — both currently merge into
`present_still`. The W-MLP gets them both right at 100% as a combined
class. Splitting them would test whether temporal RF signatures
differ between sitting (chair anchor) and standing (free body).
* **Class imbalance**`present_still` has 2× the windows of other
classes (sitting + standing both contribute). Acceptable since it's
the "neutral" class, but oversampling minority classes might lift
accuracy 1-2 pts further.
* **Smaller window size experiments** — 20 frames = 2 sec at ~10 Hz.
Could try 10 frames (1 sec, faster reaction) or 30 (3 sec, more
context). 20 was a reasonable first guess.
* **Held-out test set** — record fresh data, evaluate cold to confirm
90% isn't memorisation.
* **TCN instead of stacked-MLP** — 1D conv over time would use weights
more efficiently (~5k vs 28k). Worth pursuing if dataset scales 10×.
* **Output smoothing** — shipped via two-layer mode+confirm filter on the
adaptive output, see ADR-120 follow-up commits.
* **Split `sitting`/`standing`** — currently merged into `present_still`;
separating them would test whether the temporal RF signatures differ.
* **Class imbalance**`present_still` has 2× windows; oversampling
minority classes might lift accuracy 1-2 pts.
* **Window size experiments** — 20 frames is a reasonable first guess;
10 (faster) or 30 (more context) untested.
## References

View File

@ -0,0 +1,159 @@
# ADR-121 — HLK-LD2402 24 GHz mmWave Radar (auxiliary modality)
**Status**: Accepted (single-modality readout). Fusion deferred.
**Date**: 2026-05-18
**Scope**: `v2/crates/wifi-densepose-sensing-server/src/mmwave.rs` (new),
`Cargo.toml` (serialport dep), `main.rs` (CLI flags `--mmwave-port` /
`--mmwave-baud`, spawn reader, `mmwave_latest` REST handler, route),
`ui/components/SensingTab.js` (new card, poll integration).
## Context
The operator has an HLK-LD2402 24 GHz mmWave radar module attached via
a CP2102 USB-to-UART bridge. Factory firmware emits ASCII
`distance:<cm>\r\n` lines at 115200 8N1, ~6 Hz, in Normal Mode.
This module is a useful **auxiliary modality**: sub-mm range to a
moving target, very different physical principle than WiFi CSI, runs
fully independent. Concrete uses:
1. **Live readout in the UI** — easiest. Operator sees the radar
distance alongside the WiFi sensing.
2. **Vitals ground-truth** — at 6 Hz the data is too slow for HR but
captures breathing rate (0.2-0.4 Hz). Compare against the WiFi-CSI
vitals detector (ADR-021) for calibration.
3. **Multi-modal fusion** — feed the mmWave distance + WiFi features
into a future classifier. Different physics, very different
confusion set — high-value addition.
This ADR ships #1 only. #2 and #3 are follow-ups.
## Decisions
### D1 — Dedicated blocking reader thread, not async
`serialport` is a sync API. Wrapping it with `tokio::spawn_blocking`
adds overhead for a single-port reader running indefinitely. A
plain `std::thread` named `mmwave-reader` reads the port, parses
lines, and writes the latest reading into a global
`OnceLock<Mutex<Option<MmwaveReading>>>`.
### D2 — Graceful absence
`--mmwave-port` is **optional**. When unset, the server runs as
before. When set but the port can't be opened, the reader thread
logs a single warning and exits — server keeps running with WiFi
sensing only. No retries, no panics. (Operator can hot-plug; if
auto-reconnect is wanted we add it later.)
### D3 — Stale-after policy
`mmwave::current(staleness)` returns `None` if the most recent
reading is older than `staleness`. The REST endpoint uses 2 seconds
— at the module's 6 Hz cadence, 2 s = ~12 missed frames, plenty of
slack for a brief USB hiccup but tight enough to flag a dead module.
### D4 — Single new REST endpoint, no SensingUpdate change
`GET /api/v1/mmwave/latest` returns:
```json
{ "available": true, "distance_cm": 152, "age_ms": 90 }
```
or
```json
{ "available": false }
```
Not embedded in `SensingUpdate` because:
* The WS stream is already busy with per-tick CSI broadcasts; a
separate poll lets the UI throttle the mmWave refresh
independently (saves bandwidth if many clients connect).
* Keeps the SensingUpdate schema stable — older WS consumers don't
need a migration.
UI polls the endpoint once per visible WS tick. ~5-10 Hz refresh.
### D5 — UI badge in `SensingTab`, hidden when unavailable
New card "mmWave Radar (24 GHz)" with a blue badge showing
`<distance> cm` and an age bar (100 % at 0 ms → 0 % at 2 s). The
whole card hides via `display: none` when the endpoint reports
`available: false`, so deployments without the radar see no
clutter.
### D6 — Parse only the `distance:<n>` Normal Mode format
HLK-LD2402 also has an "Engineering Mode" emitting binary frames
with per-range-gate energy. Out of scope for v1 — Normal Mode
covers the live-readout use case. Engineering Mode parsing is a
separate ADR if/when we need per-gate data for vitals fusion.
## Files Touched
```
v2/crates/wifi-densepose-sensing-server/Cargo.toml
+ serialport.workspace = true
v2/crates/wifi-densepose-sensing-server/src/mmwave.rs (new, ~130 LoC)
+ pub struct MmwaveReading { distance_cm: u32, at: Instant }
+ static LATEST: OnceLock<Mutex<Option<MmwaveReading>>>
+ pub fn current(staleness) -> Option<MmwaveReading>
+ pub fn spawn_reader(port, baud)
+ fn parse_distance(line: &str) -> Option<u32>
+ 1 unit test
v2/crates/wifi-densepose-sensing-server/src/lib.rs
+ pub mod mmwave;
v2/crates/wifi-densepose-sensing-server/src/main.rs
+ Args { mmwave_port, mmwave_baud }
+ spawn_reader call in main()
+ async fn mmwave_latest
+ route /api/v1/mmwave/latest
ui/components/SensingTab.js
+ #mmwaveCard hidden-by-default card with #mmwaveLabel + age bar
+ fetch /api/v1/mmwave/latest each visible tick, show/hide card
docs/adr/ADR-121-mmwave-hlk-ld2402.md (this)
```
## Verified Acceptance
Live with the module attached:
```
$ ./target/release/sensing-server --mmwave-port /dev/cu.usbserial-1140 …
ADR-121 mmWave reader: opened /dev/cu.usbserial-1140 @ 115200
$ curl :8080/api/v1/mmwave/latest
{"age_ms":55,"available":true,"distance_cm":149}
{"age_ms":90,"available":true,"distance_cm":152}
{"age_ms":127,"available":true,"distance_cm":153}
```
Live without module attached (port arg omitted): server starts cleanly,
endpoint returns `{"available": false}`, Sensing tab card hidden.
## Out of Scope / Follow-ups
* **Engineering Mode binary parser** — needed if we want per-gate
energy for vitals (breathing band) or person-counting from
per-gate occupancy.
* **Vitals fusion (ADR-021 cross-check)** — log mmWave breathing
rate side-by-side with WiFi-CSI vitals for 5 min, compute
Pearson correlation, decide whether to weight one over the other
in the final vitals output.
* **W-MLP feature input** — once vitals fusion proves out, expose
mmWave distance as a 23rd feature in the W-MLP and retrain.
Would warrant ADR-122.
* **Auto-reconnect** — current behaviour: open fails or read errors
exit the reader thread. Add a retry loop with 2-second backoff
if the operator wants USB hot-plug recovery.
## References
* ADR-021 — WiFi-CSI vitals detector (the candidate cross-check
partner for HLK-LD2402 breathing-rate output).
* `assets/sensors/sensor_03.jpeg` / `_04.jpeg` / `_05.jpeg`
hardware photos and inventory entry for the module + CP2102
bridge.

34
docs/architecture.md Normal file
View File

@ -0,0 +1,34 @@
# RuView · How It Works
Extracted from the main README to keep the landing page short. See
[`../README.md`](../README.md) for the high-level overview.
---
WiFi routers flood every room with radio waves. When a person moves — or even breathes — those waves scatter differently. WiFi DensePose reads that scattering pattern and reconstructs what happened:
```
WiFi Router → radio waves pass through room → hit human body → scatter
ESP32 mesh (4-6 nodes) captures CSI on channels 1/6/11 via TDM protocol
Multi-Band Fusion: 3 channels × 56 subcarriers = 168 virtual subcarriers per link
Multistatic Fusion: N×(N-1) links → attention-weighted cross-viewpoint embedding
Coherence Gate: accept/reject measurements → stable for days without tuning
Signal Processing: Hampel, SpotFi, Fresnel, BVP, spectrogram → clean features
AI Backbone (RuVector): attention, graph algorithms, compression, field model
Signal-Line Protocol (CRV): 6-stage gestalt → sensory → topology → coherence → search → model
Neural Network: processed signals → 17 body keypoints + vital signs + room model
Output: real-time pose, breathing, heart rate, room fingerprint, drift alerts
```
No training cameras required — the [Self-Learning system (ADR-024)](adr/ADR-024-contrastive-csi-embedding-model.md) bootstraps from raw WiFi data alone. [MERIDIAN (ADR-027)](adr/ADR-027-cross-environment-domain-generalization.md) ensures the model works in any room, not just the one it trained in.

82
docs/claude-swarm.md Normal file
View File

@ -0,0 +1,82 @@
# RuView · Claude-Flow Swarm Handbook
V3 CLI commands, agent types, memory operations and configuration
extracted from `CLAUDE.md`. Active when working through claude-flow's
swarm coordination tools.
## V3 CLI Commands
### Core Commands
| Command | Subcommands | Description |
|---------|-------------|-------------|
| `init` | 4 | Project initialization |
| `agent` | 8 | Agent lifecycle management |
| `swarm` | 6 | Multi-agent swarm coordination |
| `memory` | 11 | AgentDB memory with HNSW search |
| `task` | 6 | Task creation and lifecycle |
| `session` | 7 | Session state management |
| `hooks` | 17 | Self-learning hooks + 12 workers |
| `hive-mind` | 6 | Byzantine fault-tolerant consensus |
### Quick CLI Examples
```bash
npx @claude-flow/cli@latest init --wizard
npx @claude-flow/cli@latest agent spawn -t coder --name my-coder
npx @claude-flow/cli@latest swarm init --v3-mode
npx @claude-flow/cli@latest memory search --query "authentication patterns"
npx @claude-flow/cli@latest doctor --fix
```
## Available Agents (60+ Types)
### Core Development
`coder`, `reviewer`, `tester`, `planner`, `researcher`
### Specialized
`security-architect`, `security-auditor`, `memory-specialist`, `performance-engineer`
### Swarm Coordination
`hierarchical-coordinator`, `mesh-coordinator`, `adaptive-coordinator`
### GitHub & Repository
`pr-manager`, `code-review-swarm`, `issue-tracker`, `release-manager`
### SPARC Methodology
`sparc-coord`, `sparc-coder`, `specification`, `pseudocode`, `architecture`
## Memory Commands Reference
```bash
# Store (REQUIRED: --key, --value; OPTIONAL: --namespace, --ttl, --tags)
npx @claude-flow/cli@latest memory store --key "pattern-auth" --value "JWT with refresh" --namespace patterns
# Search (REQUIRED: --query; OPTIONAL: --namespace, --limit, --threshold)
npx @claude-flow/cli@latest memory search --query "authentication patterns"
# List (OPTIONAL: --namespace, --limit)
npx @claude-flow/cli@latest memory list --namespace patterns --limit 10
# Retrieve (REQUIRED: --key; OPTIONAL: --namespace)
npx @claude-flow/cli@latest memory retrieve --key "pattern-auth" --namespace patterns
```
## Quick Setup
```bash
claude mcp add claude-flow -- npx -y @claude-flow/cli@latest
npx @claude-flow/cli@latest daemon start
npx @claude-flow/cli@latest doctor --fix
```
## Claude Code vs CLI Tools
- Claude Code's Task tool handles ALL execution: agents, file ops, code generation, git
- CLI tools handle coordination via Bash: swarm init, memory, hooks, routing
- NEVER use CLI tools as a substitute for Task tool agents
## Support
- Documentation: https://github.com/ruvnet/claude-flow
- Issues: https://github.com/ruvnet/claude-flow/issues

212
docs/dev-handbook.md Normal file
View File

@ -0,0 +1,212 @@
# RuView · Developer Handbook
All the detailed crate maps, module tables, build commands, firmware
flashing recipes, publishing order, witness verification, swarm tooling
and memory commands extracted from `CLAUDE.md` to keep that file
under 200 lines. CLAUDE.md retains only the rules-of-engagement
sections; everything else lives here.
---
## Rust Workspace (v2/)
### Key Rust Crates
| Crate | Description |
|-------|-------------|
| `wifi-densepose-core` | Core types, traits, error types, CSI frame primitives |
| `wifi-densepose-signal` | SOTA signal processing + RuvSense multistatic sensing (14 modules) |
| `wifi-densepose-nn` | Neural network inference (ONNX, PyTorch, Candle backends) |
| `wifi-densepose-train` | Training pipeline with ruvector integration + ruview_metrics |
| `wifi-densepose-mat` | Mass Casualty Assessment Tool — disaster survivor detection |
| `wifi-densepose-hardware` | ESP32 aggregator, TDM protocol, channel hopping firmware |
| `wifi-densepose-ruvector` | RuVector v2.0.4 integration + cross-viewpoint fusion (5 modules) |
| `wifi-densepose-api` | REST API (Axum) |
| `wifi-densepose-db` | Database layer (Postgres, SQLite, Redis) |
| `wifi-densepose-config` | Configuration management |
| `wifi-densepose-wasm` | WebAssembly bindings for browser deployment |
| `wifi-densepose-cli` | CLI tool (`wifi-densepose` binary) |
| `wifi-densepose-sensing-server` | Lightweight Axum server for WiFi sensing UI |
| `wifi-densepose-wifiscan` | Multi-BSSID WiFi scanning (ADR-022) |
| `wifi-densepose-vitals` | ESP32 CSI-grade vital sign extraction (ADR-021) |
| `nvsim` | Deterministic NV-diamond magnetometer pipeline simulator (ADR-089) — standalone leaf, WASM-ready |
| `vendor/rvcsi` (submodule) | **rvCSI** — edge RF sensing runtime (ADR-095/096): 9 crates (`rvcsi-core`/`-dsp`/`-events`/`-adapter-file`/`-adapter-nexmon`/`-ruvector`/`-runtime`/`-node`/`-cli`). Lives in its own repo ([github.com/ruvnet/rvcsi](https://github.com/ruvnet/rvcsi)), vendored here under `vendor/rvcsi`, published to crates.io as `rvcsi-* 0.3.x` and to npm as `@ruv/rvcsi`. Not a `v2/` workspace member — depend on the published crates (or the submodule's `crates/rvcsi-*` paths). Normalized `CsiFrame`/`CsiWindow`/`CsiEvent` schema, validate-before-FFI, reusable DSP, typed confidence-scored events, the napi-c Nexmon shim (real nexmon_csi `.pcap` from a Raspberry Pi 5 / 4 / 3B+ — BCM43455c0), the napi-rs SDK, the `rvcsi` CLI, a Claude Code plugin. |
### RuvSense Modules (`signal/src/ruvsense/`)
| Module | Purpose |
|--------|---------|
| `multiband.rs` | Multi-band CSI frame fusion, cross-channel coherence |
| `phase_align.rs` | Iterative LO phase offset estimation, circular mean |
| `multistatic.rs` | Attention-weighted fusion, geometric diversity |
| `coherence.rs` | Z-score coherence scoring, DriftProfile |
| `coherence_gate.rs` | Accept/PredictOnly/Reject/Recalibrate gate decisions |
| `pose_tracker.rs` | 17-keypoint Kalman tracker with AETHER re-ID embeddings |
| `field_model.rs` | SVD room eigenstructure, perturbation extraction |
| `tomography.rs` | RF tomography, ISTA L1 solver, voxel grid |
| `longitudinal.rs` | Welford stats, biomechanics drift detection |
| `intention.rs` | Pre-movement lead signals (200-500ms) |
| `cross_room.rs` | Environment fingerprinting, transition graph |
| `gesture.rs` | DTW template matching gesture classifier |
| `adversarial.rs` | Physically impossible signal detection, multi-link consistency |
### Cross-Viewpoint Fusion (`ruvector/src/viewpoint/`)
| Module | Purpose |
|--------|---------|
| `attention.rs` | CrossViewpointAttention, GeometricBias, softmax with G_bias |
| `geometry.rs` | GeometricDiversityIndex, Cramer-Rao bounds, Fisher Information |
| `coherence.rs` | Phase phasor coherence, hysteresis gate |
| `fusion.rs` | MultistaticArray aggregate root, domain events |
### RuVector v2.0.4 Integration (ADR-016 complete, ADR-017 proposed)
All 5 ruvector crates integrated in workspace:
- `ruvector-mincut``metrics.rs` (DynamicPersonMatcher) + `subcarrier_selection.rs`
- `ruvector-attn-mincut``model.rs` (apply_antenna_attention) + `spectrogram.rs`
- `ruvector-temporal-tensor``dataset.rs` (CompressedCsiBuffer) + `breathing.rs`
- `ruvector-solver``subcarrier.rs` (sparse interpolation 114→56) + `triangulation.rs`
- `ruvector-attention``model.rs` (apply_spatial_attention) + `bvp.rs`
## Architecture Decisions, Hardware, Build Commands
### Architecture Decisions
43 ADRs in `docs/adr/` (ADR-001 through ADR-043). Key ones:
- ADR-014: SOTA signal processing (Accepted)
- ADR-015: MM-Fi + Wi-Pose training datasets (Accepted)
- ADR-016: RuVector training pipeline integration (Accepted — complete)
- ADR-017: RuVector signal + MAT integration (Proposed — next target)
- ADR-024: Contrastive CSI embedding / AETHER (Accepted)
- ADR-027: Cross-environment domain generalization / MERIDIAN (Accepted)
- ADR-028: ESP32 capability audit + witness verification (Accepted)
- ADR-029: RuvSense multistatic sensing mode (Proposed)
- ADR-030: RuvSense persistent field model (Proposed)
- ADR-031: RuView sensing-first RF mode (Proposed)
- ADR-032: Multistatic mesh security hardening (Proposed)
### Supported Hardware
| Device | Port | Chip | Role | Cost |
|--------|------|------|------|------|
| ESP32-S3 (8MB flash) | COM7 | Xtensa dual-core | WiFi CSI sensing node | ~$9 |
| ESP32-S3 SuperMini (4MB) | — | Xtensa dual-core | WiFi CSI (compact) | ~$6 |
| ESP32-C6 + Seeed MR60BHA2 | COM4 | RISC-V + 60 GHz FMCW | mmWave HR/BR/presence | ~$15 |
| HLK-LD2410 | — | 24 GHz FMCW | Presence + distance | ~$3 |
**Not supported:** ESP32 (original), ESP32-C3 — single-core, can't run CSI DSP pipeline.
### Build & Test Commands (this repo)
```bash
# Rust — full workspace tests (1,031+ tests, ~2 min)
cd v2
cargo test --workspace --no-default-features
# Rust — single crate check (no GPU needed)
cargo check -p wifi-densepose-train --no-default-features
# Python — deterministic proof verification (SHA-256)
python archive/v1/data/proof/verify.py
# Python — test suite
cd archive/v1 && python -m pytest tests/ -x -q
```
## Firmware Build + Flash + Provision + Release + Publish
### ESP32 Firmware Build (Windows — Python subprocess required)
```bash
# Build 8MB firmware (real WiFi CSI mode, no mocks)
# See CLAUDE.local.md for the full Python subprocess command
# Key: must strip MSYSTEM env vars for ESP-IDF v5.4 on Git Bash
# Build 4MB firmware
cp sdkconfig.defaults.4mb sdkconfig.defaults
# then same build process
# Flash to COM7
# [python, idf_py, '-p', 'COM7', 'flash']
# Provision WiFi
python firmware/esp32-csi-node/provision.py --port COM7 \
--ssid "YourWiFi" --password "secret" --target-ip 192.168.1.20
# Monitor serial
python -m serial.tools.miniterm COM7 115200
```
### Firmware Release Process
1. Build 8MB from `sdkconfig.defaults.template` (no mock)
2. Build 4MB from `sdkconfig.defaults.4mb` (no mock)
3. Save 6 binaries: `esp32-csi-node.bin`, `bootloader.bin`, `partition-table.bin`, `ota_data_initial.bin`, `esp32-csi-node-4mb.bin`, `partition-table-4mb.bin`
4. Tag: `git tag v0.X.Y-esp32 && git push origin v0.X.Y-esp32`
5. Release: `gh release create v0.X.Y-esp32 <binaries> --title "..." --notes-file ...`
6. Verify on real hardware (COM7) before publishing
7. **CRITICAL:** Always test with real WiFi CSI, not mock mode — mock missed the Kconfig threshold bug
### Crate Publishing Order
Crates must be published in dependency order:
1. `wifi-densepose-core` (no internal deps)
2. `wifi-densepose-vitals` (no internal deps)
3. `wifi-densepose-wifiscan` (no internal deps)
4. `wifi-densepose-hardware` (no internal deps)
5. `wifi-densepose-config` (no internal deps)
6. `wifi-densepose-db` (no internal deps)
7. `wifi-densepose-signal` (depends on core)
8. `wifi-densepose-nn` (no internal deps, workspace only)
9. `wifi-densepose-ruvector` (no internal deps, workspace only)
10. `wifi-densepose-train` (depends on signal, nn)
11. `wifi-densepose-mat` (depends on core, signal, nn)
12. `wifi-densepose-api` (no internal deps)
13. `wifi-densepose-wasm` (depends on mat)
14. `wifi-densepose-sensing-server` (depends on wifiscan)
15. `wifi-densepose-cli` (depends on mat)
## Validation & Witness Verification (ADR-028)
### Validation & Witness Verification (ADR-028)
**After any significant code change, run the full validation:**
```bash
# 1. Rust tests — must be 1,031+ passed, 0 failed
cd v2
cargo test --workspace --no-default-features
# 2. Python proof — must print VERDICT: PASS
cd ..
python archive/v1/data/proof/verify.py
# 3. Generate witness bundle (includes both above + firmware hashes)
bash scripts/generate-witness-bundle.sh
# 4. Self-verify the bundle — must be 7/7 PASS
cd dist/witness-bundle-ADR028-*/
bash VERIFY.sh
```
**If the Python proof hash changes** (e.g., numpy/scipy version update):
```bash
# Regenerate the expected hash, then verify it passes
python archive/v1/data/proof/verify.py --generate-hash
python archive/v1/data/proof/verify.py
```
**Witness bundle contents** (`dist/witness-bundle-ADR028-<sha>.tar.gz`):
- `WITNESS-LOG-028.md` — 33-row attestation matrix with evidence per capability
- `ADR-028-esp32-capability-audit.md` — Full audit findings
- `proof/verify.py` + `expected_features.sha256` — Deterministic pipeline proof
- `test-results/rust-workspace-tests.log` — Full cargo test output
- `firmware-manifest/source-hashes.txt` — SHA-256 of all 7 ESP32 firmware files
- `crate-manifest/versions.txt` — All 15 crates with versions
- `VERIFY.sh` — One-command self-verification for recipients
**Key proof artifacts:**
- `archive/v1/data/proof/verify.py` — Trust Kill Switch: feeds reference signal through production pipeline, hashes output
- `archive/v1/data/proof/expected_features.sha256` — Published expected hash
- `archive/v1/data/proof/sample_csi_data.json` — 1,000 synthetic CSI frames (seed=42)
- `docs/WITNESS-LOG-028.md` — 11-step reproducible verification procedure
- `docs/adr/ADR-028-esp32-capability-audit.md` — Complete audit record
### Branch
Default branch: `main`
Active feature branch: `ruvsense-full-implementation` (PR #77)

347
docs/use-cases.md Normal file
View File

@ -0,0 +1,347 @@
# RuView · Use Cases & Applications
This file lists every concrete deployment scenario RuView is designed to
serve, plus the 60-module ADR-041 Edge Intelligence catalogue and the
ADR-024 self-learning system. Pulled out of the main README to keep the
landing page short — see [`../README.md`](../README.md) for the
high-level overview and quick start.
---
WiFi sensing works anywhere WiFi exists. No new hardware in most cases — just software on existing access points or a $8 ESP32 add-on. Because there are no cameras, deployments avoid privacy regulations (GDPR video, HIPAA imaging) by design.
**Scaling:** Each AP distinguishes ~3-5 people (56 subcarriers). Multi-AP multiplies linearly — a 4-AP retail mesh covers ~15-20 occupants. No hard software limit; the practical ceiling is signal physics.
| | Why WiFi sensing wins | Traditional alternative |
|---|----------------------|----------------------|
| 🔒 | **No video, no GDPR/HIPAA imaging rules** | Cameras require consent, signage, data retention policies |
| 🧱 | **Works through walls, shelving, debris** | Cameras need line-of-sight per room |
| 🌙 | **Works in total darkness** | Cameras need IR or visible light |
| 💰 | **$0-$8 per zone** (existing WiFi or ESP32) | Camera systems: $200-$2,000 per zone |
| 🔌 | **WiFi already deployed everywhere** | PIR/radar sensors require new wiring per room |
<details>
<summary><strong>🏥 Everyday</strong> — Healthcare, retail, office, hospitality (commodity WiFi)</summary>
| Use Case | What It Does | Hardware | Key Metric | Edge Module |
|----------|-------------|----------|------------|-------------|
| **Elderly care / assisted living** | Fall detection, nighttime activity monitoring, breathing rate during sleep — no wearable compliance needed | 1 ESP32-S3 per room ($8) | Fall alert <2s | [Sleep Apnea](edge-modules/medical.md), [Gait Analysis](edge-modules/medical.md) |
| **Hospital patient monitoring** | Continuous breathing + heart rate for non-critical beds without wired sensors; nurse alert on anomaly | 1-2 APs per ward | Breathing: 6-30 BPM | [Respiratory Distress](edge-modules/medical.md), [Cardiac Arrhythmia](edge-modules/medical.md) |
| **Emergency room triage** | Automated occupancy count + wait-time estimation; detect patient distress (abnormal breathing) in waiting areas | Existing hospital WiFi | Occupancy accuracy >95% | [Queue Length](edge-modules/retail.md), [Panic Motion](edge-modules/security.md) |
| **Retail occupancy & flow** | Real-time foot traffic, dwell time by zone, queue length — no cameras, no opt-in, GDPR-friendly | Existing store WiFi + 1 ESP32 | Dwell resolution ~1m | [Customer Flow](edge-modules/retail.md), [Dwell Heatmap](edge-modules/retail.md) |
| **Office space utilization** | Which desks/rooms are actually occupied, meeting room no-shows, HVAC optimization based on real presence | Existing enterprise WiFi | Presence latency <1s | [Meeting Room](edge-modules/building.md), [HVAC Presence](edge-modules/building.md) |
| **Hotel & hospitality** | Room occupancy without door sensors, minibar/bathroom usage patterns, energy savings on empty rooms | Existing hotel WiFi | 15-30% HVAC savings | [Energy Audit](edge-modules/building.md), [Lighting Zones](edge-modules/building.md) |
| **Restaurants & food service** | Table turnover tracking, kitchen staff presence, restroom occupancy displays — no cameras in dining areas | Existing WiFi | Queue wait ±30s | [Table Turnover](edge-modules/retail.md), [Queue Length](edge-modules/retail.md) |
| **Parking garages** | Pedestrian presence in stairwells and elevators where cameras have blind spots; security alert if someone lingers | Existing WiFi | Through-concrete walls | [Loitering](edge-modules/security.md), [Elevator Count](edge-modules/building.md) |
</details>
<details>
<summary><strong>🏟️ Specialized</strong> — Events, fitness, education, civic (CSI-capable hardware)</summary>
| Use Case | What It Does | Hardware | Key Metric | Edge Module |
|----------|-------------|----------|------------|-------------|
| **Smart home automation** | Room-level presence triggers (lights, HVAC, music) that work through walls — no dead zones, no motion-sensor timeouts | 2-3 ESP32-S3 nodes ($24) | Through-wall range ~5m | [HVAC Presence](edge-modules/building.md), [Lighting Zones](edge-modules/building.md) |
| **Fitness & sports** | Rep counting, posture correction, breathing cadence during exercise — no wearable, no camera in locker rooms | 3+ ESP32-S3 mesh | Pose: 17 keypoints | [Breathing Sync](edge-modules/exotic.md), [Gait Analysis](edge-modules/medical.md) |
| **Childcare & schools** | Naptime breathing monitoring, playground headcount, restricted-area alerts — privacy-safe for minors | 2-4 ESP32-S3 per zone | Breathing: ±1 BPM | [Sleep Apnea](edge-modules/medical.md), [Perimeter Breach](edge-modules/security.md) |
| **Event venues & concerts** | Crowd density mapping, crush-risk detection via breathing compression, emergency evacuation flow tracking | Multi-AP mesh (4-8 APs) | Density per m² | [Customer Flow](edge-modules/retail.md), [Panic Motion](edge-modules/security.md) |
| **Stadiums & arenas** | Section-level occupancy for dynamic pricing, concession staffing, emergency egress flow modeling | Enterprise AP grid | 15-20 per AP mesh | [Dwell Heatmap](edge-modules/retail.md), [Queue Length](edge-modules/retail.md) |
| **Houses of worship** | Attendance counting without facial recognition — privacy-sensitive congregations, multi-room campus tracking | Existing WiFi | Zone-level accuracy | [Elevator Count](edge-modules/building.md), [Energy Audit](edge-modules/building.md) |
| **Warehouse & logistics** | Worker safety zones, forklift proximity alerts, occupancy in hazardous areas — works through shelving and pallets | Industrial AP mesh | Alert latency <500ms | [Forklift Proximity](edge-modules/industrial.md), [Confined Space](edge-modules/industrial.md) |
| **Civic infrastructure** | Public restroom occupancy (no cameras possible), subway platform crowding, shelter headcount during emergencies | Municipal WiFi + ESP32 | Real-time headcount | [Customer Flow](edge-modules/retail.md), [Loitering](edge-modules/security.md) |
| **Museums & galleries** | Visitor flow heatmaps, exhibit dwell time, crowd bottleneck alerts — no cameras near artwork (flash/theft risk) | Existing WiFi | Zone dwell ±5s | [Dwell Heatmap](edge-modules/retail.md), [Shelf Engagement](edge-modules/retail.md) |
</details>
<details>
<summary><strong>🤖 Robotics & Industrial</strong> — Autonomous systems, manufacturing, android spatial awareness</summary>
WiFi sensing gives robots and autonomous systems a spatial awareness layer that works where LIDAR and cameras fail — through dust, smoke, fog, and around corners. The CSI signal field acts as a "sixth sense" for detecting humans in the environment without requiring line-of-sight.
| Use Case | What It Does | Hardware | Key Metric | Edge Module |
|----------|-------------|----------|------------|-------------|
| **Cobot safety zones** | Detect human presence near collaborative robots — auto-slow or stop before contact, even behind obstructions | 2-3 ESP32-S3 per cell | Presence latency <100ms | [Forklift Proximity](edge-modules/industrial.md), [Perimeter Breach](edge-modules/security.md) |
| **Warehouse AMR navigation** | Autonomous mobile robots sense humans around blind corners, through shelving racks — no LIDAR occlusion | ESP32 mesh along aisles | Through-shelf detection | [Forklift Proximity](edge-modules/industrial.md), [Loitering](edge-modules/security.md) |
| **Android / humanoid spatial awareness** | Ambient human pose sensing for social robots — detect gestures, approach direction, and personal space without cameras always on | Onboard ESP32-S3 module | 17-keypoint pose | [Gesture Language](edge-modules/exotic.md), [Emotion Detection](edge-modules/exotic.md) |
| **Manufacturing line monitoring** | Worker presence at each station, ergonomic posture alerts, headcount for shift compliance — works through equipment | Industrial AP per zone | Pose + breathing | [Confined Space](edge-modules/industrial.md), [Gait Analysis](edge-modules/medical.md) |
| **Construction site safety** | Exclusion zone enforcement around heavy machinery, fall detection from scaffolding, personnel headcount | Ruggedized ESP32 mesh | Alert <2s, through-dust | [Panic Motion](edge-modules/security.md), [Structural Vibration](edge-modules/industrial.md) |
| **Agricultural robotics** | Detect farm workers near autonomous harvesters in dusty/foggy field conditions where cameras are unreliable | Weatherproof ESP32 nodes | Range ~10m open field | [Forklift Proximity](edge-modules/industrial.md), [Rain Detection](edge-modules/exotic.md) |
| **Drone landing zones** | Verify landing area is clear of humans — WiFi sensing works in rain, dust, and low light where downward cameras fail | Ground ESP32 nodes | Presence: >95% accuracy | [Perimeter Breach](edge-modules/security.md), [Tailgating](edge-modules/security.md) |
| **Clean room monitoring** | Personnel tracking without cameras (particle contamination risk from camera fans) — gown compliance via pose | Existing cleanroom WiFi | No particulate emission | [Clean Room](edge-modules/industrial.md), [Livestock Monitor](edge-modules/industrial.md) |
</details>
<details>
<summary><strong>🔥 Extreme</strong> — Through-wall, disaster, defense, underground</summary>
These scenarios exploit WiFi's ability to penetrate solid materials — concrete, rubble, earth — where no optical or infrared sensor can reach. The WiFi-Mat disaster module (ADR-001) is specifically designed for this tier.
| Use Case | What It Does | Hardware | Key Metric | Edge Module |
|----------|-------------|----------|------------|-------------|
| **Search & rescue (WiFi-Mat)** | Detect survivors through rubble/debris via breathing signature, START triage color classification, 3D localization | Portable ESP32 mesh + laptop | Through 30cm concrete | [Respiratory Distress](edge-modules/medical.md), [Seizure Detection](edge-modules/medical.md) |
| **Firefighting** | Locate occupants through smoke and walls before entry; breathing detection confirms life signs remotely | Portable mesh on truck | Works in zero visibility | [Sleep Apnea](edge-modules/medical.md), [Panic Motion](edge-modules/security.md) |
| **Prison & secure facilities** | Cell occupancy verification, distress detection (abnormal vitals), perimeter sensing — no camera blind spots | Dedicated AP infrastructure | 24/7 vital signs | [Cardiac Arrhythmia](edge-modules/medical.md), [Loitering](edge-modules/security.md) |
| **Military / tactical** | Through-wall personnel detection, room clearing confirmation, hostage vital signs at standoff distance | Directional WiFi + custom FW | Range: 5m through wall | [Perimeter Breach](edge-modules/security.md), [Weapon Detection](edge-modules/security.md) |
| **Border & perimeter security** | Detect human presence in tunnels, behind fences, in vehicles — passive sensing, no active illumination to reveal position | Concealed ESP32 mesh | Passive / covert | [Perimeter Breach](edge-modules/security.md), [Tailgating](edge-modules/security.md) |
| **Mining & underground** | Worker presence in tunnels where GPS/cameras fail, breathing detection after collapse, headcount at safety points | Ruggedized ESP32 mesh | Through rock/earth | [Confined Space](edge-modules/industrial.md), [Respiratory Distress](edge-modules/medical.md) |
| **Maritime & naval** | Below-deck personnel tracking through steel bulkheads (limited range, requires tuning), man-overboard detection | Ship WiFi + ESP32 | Through 1-2 bulkheads | [Structural Vibration](edge-modules/industrial.md), [Panic Motion](edge-modules/security.md) |
| **Wildlife research** | Non-invasive animal activity monitoring in enclosures or dens — no light pollution, no visual disturbance | Weatherproof ESP32 nodes | Zero light emission | [Livestock Monitor](edge-modules/industrial.md), [Dream Stage](edge-modules/exotic.md) |
</details>
<details>
<summary><strong>🧩 Edge Intelligence (<a href="docs/adr/ADR-041-wasm-module-collection.md">ADR-041</a>)</strong> — 60 WASM modules across 13 categories, all implemented (609 tests)</summary>
Small programs that run directly on the ESP32 sensor — no internet needed, no cloud fees, instant response. Each module is a tiny WASM file (5-30 KB) that you upload to the device over-the-air. It reads WiFi signal data and makes decisions locally in under 10 ms. [ADR-041](adr/ADR-041-wasm-module-collection.md) defines 60 modules across 13 categories — all 60 are implemented with 609 tests passing.
| | Category | Examples |
|---|----------|---------|
| 🏥 | [**Medical & Health**](edge-modules/medical.md) | Sleep apnea detection, cardiac arrhythmia, gait analysis, seizure detection |
| 🔐 | [**Security & Safety**](edge-modules/security.md) | Intrusion detection, perimeter breach, loitering, panic motion |
| 🏢 | [**Smart Building**](edge-modules/building.md) | Zone occupancy, HVAC control, elevator counting, meeting room tracking |
| 🛒 | [**Retail & Hospitality**](edge-modules/retail.md) | Queue length, dwell heatmaps, customer flow, table turnover |
| 🏭 | [**Industrial**](edge-modules/industrial.md) | Forklift proximity, confined space monitoring, structural vibration |
| 🔮 | [**Exotic & Research**](edge-modules/exotic.md) | Sleep staging, emotion detection, sign language, breathing sync |
| 📡 | [**Signal Intelligence**](edge-modules/signal-intelligence.md) | Cleans and sharpens raw WiFi signals — focuses on important regions, filters noise, fills in missing data, and tracks which person is which |
| 🧠 | [**Adaptive Learning**](edge-modules/adaptive-learning.md) | The sensor learns new gestures and patterns on its own over time — no cloud needed, remembers what it learned even after updates |
| 🗺️ | [**Spatial Reasoning**](edge-modules/spatial-temporal.md) | Figures out where people are in a room, which zones matter most, and tracks movement across areas using graph-based spatial logic |
| ⏱️ | [**Temporal Analysis**](edge-modules/spatial-temporal.md) | Learns daily routines, detects when patterns break (someone didn't get up), and verifies safety rules are being followed over time |
| 🛡️ | [**AI Security**](edge-modules/ai-security.md) | Detects signal replay attacks, WiFi jamming, injection attempts, and flags abnormal behavior that could indicate tampering |
| ⚛️ | [**Quantum-Inspired**](edge-modules/autonomous.md) | Uses quantum-inspired math to map room-wide signal coherence and search for optimal sensor configurations |
| 🤖 | [**Autonomous & Exotic**](edge-modules/autonomous.md) | Self-managing sensor mesh — auto-heals dropped nodes, plans its own actions, and explores experimental signal representations |
All implemented modules are `no_std` Rust, share a [common utility library](../v2/crates/wifi-densepose-wasm-edge/src/vendor_common.rs), and talk to the host through a 12-function API. Full documentation: [**Edge Modules Guide**](edge-modules/README.md). See the [complete implemented module list](#edge-module-list) below.
</details>
<details id="edge-module-list">
<summary><strong>🧩 Edge Intelligence — <a href="docs/edge-modules/README.md">All 65 Modules Implemented</a></strong> (ADR-041 complete)</summary>
All 60 modules are implemented, tested (609 tests passing), and ready to deploy. They compile to `wasm32-unknown-unknown`, run on ESP32-S3 via WASM3, and share a [common utility library](../v2/crates/wifi-densepose-wasm-edge/src/vendor_common.rs). Source: [`crates/wifi-densepose-wasm-edge/src/`](../v2/crates/wifi-densepose-wasm-edge/src/)
**Core modules** (ADR-040 flagship + early implementations):
| Module | File | What It Does |
|--------|------|-------------|
| Gesture Classifier | [`gesture.rs`](../v2/crates/wifi-densepose-wasm-edge/src/gesture.rs) | DTW template matching for hand gestures |
| Coherence Filter | [`coherence.rs`](../v2/crates/wifi-densepose-wasm-edge/src/coherence.rs) | Phase coherence gating for signal quality |
| Adversarial Detector | [`adversarial.rs`](../v2/crates/wifi-densepose-wasm-edge/src/adversarial.rs) | Detects physically impossible signal patterns |
| Intrusion Detector | [`intrusion.rs`](../v2/crates/wifi-densepose-wasm-edge/src/intrusion.rs) | Human vs non-human motion classification |
| Occupancy Counter | [`occupancy.rs`](../v2/crates/wifi-densepose-wasm-edge/src/occupancy.rs) | Zone-level person counting |
| Vital Trend | [`vital_trend.rs`](../v2/crates/wifi-densepose-wasm-edge/src/vital_trend.rs) | Long-term breathing and heart rate trending |
| RVF Parser | [`rvf.rs`](../v2/crates/wifi-densepose-wasm-edge/src/rvf.rs) | RVF container format parsing |
**Vendor-integrated modules** (24 modules, ADR-041 Category 7):
**📡 Signal Intelligence** — Real-time CSI analysis and feature extraction
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| Flash Attention | [`sig_flash_attention.rs`](../v2/crates/wifi-densepose-wasm-edge/src/sig_flash_attention.rs) | Tiled attention over 8 subcarrier groups — finds spatial focus regions and entropy | S (<5ms) |
| Coherence Gate | [`sig_coherence_gate.rs`](../v2/crates/wifi-densepose-wasm-edge/src/sig_coherence_gate.rs) | Z-score phasor gating with hysteresis: Accept / PredictOnly / Reject / Recalibrate | L (<2ms) |
| Temporal Compress | [`sig_temporal_compress.rs`](../v2/crates/wifi-densepose-wasm-edge/src/sig_temporal_compress.rs) | 3-tier adaptive quantization (8-bit hot / 5-bit warm / 3-bit cold) | L (<2ms) |
| Sparse Recovery | [`sig_sparse_recovery.rs`](../v2/crates/wifi-densepose-wasm-edge/src/sig_sparse_recovery.rs) | ISTA L1 reconstruction for dropped subcarriers | H (<10ms) |
| Person Match | [`sig_mincut_person_match.rs`](../v2/crates/wifi-densepose-wasm-edge/src/sig_mincut_person_match.rs) | Hungarian-lite bipartite assignment for multi-person tracking | S (<5ms) |
| Optimal Transport | [`sig_optimal_transport.rs`](../v2/crates/wifi-densepose-wasm-edge/src/sig_optimal_transport.rs) | Sliced Wasserstein-1 distance with 4 projections | L (<2ms) |
**🧠 Adaptive Learning** — On-device learning without cloud connectivity
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| DTW Gesture Learn | [`lrn_dtw_gesture_learn.rs`](../v2/crates/wifi-densepose-wasm-edge/src/lrn_dtw_gesture_learn.rs) | User-teachable gesture recognition — 3-rehearsal protocol, 16 templates | S (<5ms) |
| Anomaly Attractor | [`lrn_anomaly_attractor.rs`](../v2/crates/wifi-densepose-wasm-edge/src/lrn_anomaly_attractor.rs) | 4D dynamical system attractor classification with Lyapunov exponents | H (<10ms) |
| Meta Adapt | [`lrn_meta_adapt.rs`](../v2/crates/wifi-densepose-wasm-edge/src/lrn_meta_adapt.rs) | Hill-climbing self-optimization with safety rollback | L (<2ms) |
| EWC Lifelong | [`lrn_ewc_lifelong.rs`](../v2/crates/wifi-densepose-wasm-edge/src/lrn_ewc_lifelong.rs) | Elastic Weight Consolidation — remembers past tasks while learning new ones | S (<5ms) |
**🗺️ Spatial Reasoning** — Location, proximity, and influence mapping
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| PageRank Influence | [`spt_pagerank_influence.rs`](../v2/crates/wifi-densepose-wasm-edge/src/spt_pagerank_influence.rs) | 4x4 cross-correlation graph with power iteration PageRank | L (<2ms) |
| Micro HNSW | [`spt_micro_hnsw.rs`](../v2/crates/wifi-densepose-wasm-edge/src/spt_micro_hnsw.rs) | 64-vector navigable small-world graph for nearest-neighbor search | S (<5ms) |
| Spiking Tracker | [`spt_spiking_tracker.rs`](../v2/crates/wifi-densepose-wasm-edge/src/spt_spiking_tracker.rs) | 32 LIF neurons + 4 output zone neurons with STDP learning | S (<5ms) |
**⏱️ Temporal Analysis** — Activity patterns, logic verification, autonomous planning
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| Pattern Sequence | [`tmp_pattern_sequence.rs`](../v2/crates/wifi-densepose-wasm-edge/src/tmp_pattern_sequence.rs) | Activity routine detection and deviation alerts | S (<5ms) |
| Temporal Logic Guard | [`tmp_temporal_logic_guard.rs`](../v2/crates/wifi-densepose-wasm-edge/src/tmp_temporal_logic_guard.rs) | LTL formula verification on CSI event streams | S (<5ms) |
| GOAP Autonomy | [`tmp_goap_autonomy.rs`](../v2/crates/wifi-densepose-wasm-edge/src/tmp_goap_autonomy.rs) | Goal-Oriented Action Planning for autonomous module management | S (<5ms) |
**🛡️ AI Security** — Tamper detection and behavioral anomaly profiling
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| Prompt Shield | [`ais_prompt_shield.rs`](../v2/crates/wifi-densepose-wasm-edge/src/ais_prompt_shield.rs) | FNV-1a replay detection, injection detection (10x amplitude), jamming (SNR) | L (<2ms) |
| Behavioral Profiler | [`ais_behavioral_profiler.rs`](../v2/crates/wifi-densepose-wasm-edge/src/ais_behavioral_profiler.rs) | 6D behavioral profile with Mahalanobis anomaly scoring | S (<5ms) |
**⚛️ Quantum-Inspired** — Quantum computing metaphors applied to CSI analysis
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| Quantum Coherence | [`qnt_quantum_coherence.rs`](../v2/crates/wifi-densepose-wasm-edge/src/qnt_quantum_coherence.rs) | Bloch sphere mapping, Von Neumann entropy, decoherence detection | S (<5ms) |
| Interference Search | [`qnt_interference_search.rs`](../v2/crates/wifi-densepose-wasm-edge/src/qnt_interference_search.rs) | 16 room-state hypotheses with Grover-inspired oracle + diffusion | S (<5ms) |
**🤖 Autonomous Systems** — Self-governing and self-healing behaviors
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| Psycho-Symbolic | [`aut_psycho_symbolic.rs`](../v2/crates/wifi-densepose-wasm-edge/src/aut_psycho_symbolic.rs) | 16-rule forward-chaining knowledge base with contradiction detection | S (<5ms) |
| Self-Healing Mesh | [`aut_self_healing_mesh.rs`](../v2/crates/wifi-densepose-wasm-edge/src/aut_self_healing_mesh.rs) | 8-node mesh with health tracking, degradation/recovery, coverage healing | S (<5ms) |
**🔮 Exotic (Vendor)** — Novel mathematical models for CSI interpretation
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| Time Crystal | [`exo_time_crystal.rs`](../v2/crates/wifi-densepose-wasm-edge/src/exo_time_crystal.rs) | Autocorrelation subharmonic detection in 256-frame history | S (<5ms) |
| Hyperbolic Space | [`exo_hyperbolic_space.rs`](../v2/crates/wifi-densepose-wasm-edge/src/exo_hyperbolic_space.rs) | Poincare ball embedding with 32 reference locations, hyperbolic distance | S (<5ms) |
**🏥 Medical & Health** (Category 1) — Contactless health monitoring
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| Sleep Apnea | [`med_sleep_apnea.rs`](../v2/crates/wifi-densepose-wasm-edge/src/med_sleep_apnea.rs) | Detects breathing pauses during sleep | S (<5ms) |
| Cardiac Arrhythmia | [`med_cardiac_arrhythmia.rs`](../v2/crates/wifi-densepose-wasm-edge/src/med_cardiac_arrhythmia.rs) | Monitors heart rate for irregular rhythms | S (<5ms) |
| Respiratory Distress | [`med_respiratory_distress.rs`](../v2/crates/wifi-densepose-wasm-edge/src/med_respiratory_distress.rs) | Alerts on abnormal breathing patterns | S (<5ms) |
| Gait Analysis | [`med_gait_analysis.rs`](../v2/crates/wifi-densepose-wasm-edge/src/med_gait_analysis.rs) | Tracks walking patterns and detects changes | S (<5ms) |
| Seizure Detection | [`med_seizure_detect.rs`](../v2/crates/wifi-densepose-wasm-edge/src/med_seizure_detect.rs) | 6-state machine for tonic-clonic seizure recognition | S (<5ms) |
**🔐 Security & Safety** (Category 2) — Perimeter and threat detection
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| Perimeter Breach | [`sec_perimeter_breach.rs`](../v2/crates/wifi-densepose-wasm-edge/src/sec_perimeter_breach.rs) | Detects boundary crossings with approach/departure | S (<5ms) |
| Weapon Detection | [`sec_weapon_detect.rs`](../v2/crates/wifi-densepose-wasm-edge/src/sec_weapon_detect.rs) | Metal anomaly detection via CSI amplitude shifts | S (<5ms) |
| Tailgating | [`sec_tailgating.rs`](../v2/crates/wifi-densepose-wasm-edge/src/sec_tailgating.rs) | Detects unauthorized follow-through at access points | S (<5ms) |
| Loitering | [`sec_loitering.rs`](../v2/crates/wifi-densepose-wasm-edge/src/sec_loitering.rs) | Alerts when someone lingers too long in a zone | S (<5ms) |
| Panic Motion | [`sec_panic_motion.rs`](../v2/crates/wifi-densepose-wasm-edge/src/sec_panic_motion.rs) | Detects fleeing, struggling, or panic movement | S (<5ms) |
**🏢 Smart Building** (Category 3) — Automation and energy efficiency
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| HVAC Presence | [`bld_hvac_presence.rs`](../v2/crates/wifi-densepose-wasm-edge/src/bld_hvac_presence.rs) | Occupancy-driven HVAC control with departure countdown | S (<5ms) |
| Lighting Zones | [`bld_lighting_zones.rs`](../v2/crates/wifi-densepose-wasm-edge/src/bld_lighting_zones.rs) | Auto-dim/off lighting based on zone activity | S (<5ms) |
| Elevator Count | [`bld_elevator_count.rs`](../v2/crates/wifi-densepose-wasm-edge/src/bld_elevator_count.rs) | Counts people entering/leaving with overload warning | S (<5ms) |
| Meeting Room | [`bld_meeting_room.rs`](../v2/crates/wifi-densepose-wasm-edge/src/bld_meeting_room.rs) | Tracks meeting lifecycle: start, headcount, end, availability | S (<5ms) |
| Energy Audit | [`bld_energy_audit.rs`](../v2/crates/wifi-densepose-wasm-edge/src/bld_energy_audit.rs) | Tracks after-hours usage and room utilization rates | S (<5ms) |
**🛒 Retail & Hospitality** (Category 4) — Customer insights without cameras
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| Queue Length | [`ret_queue_length.rs`](../v2/crates/wifi-densepose-wasm-edge/src/ret_queue_length.rs) | Estimates queue size and wait times | S (<5ms) |
| Dwell Heatmap | [`ret_dwell_heatmap.rs`](../v2/crates/wifi-densepose-wasm-edge/src/ret_dwell_heatmap.rs) | Shows where people spend time (hot/cold zones) | S (<5ms) |
| Customer Flow | [`ret_customer_flow.rs`](../v2/crates/wifi-densepose-wasm-edge/src/ret_customer_flow.rs) | Counts ins/outs and tracks net occupancy | S (<5ms) |
| Table Turnover | [`ret_table_turnover.rs`](../v2/crates/wifi-densepose-wasm-edge/src/ret_table_turnover.rs) | Restaurant table lifecycle: seated, dining, vacated | S (<5ms) |
| Shelf Engagement | [`ret_shelf_engagement.rs`](../v2/crates/wifi-densepose-wasm-edge/src/ret_shelf_engagement.rs) | Detects browsing, considering, and reaching for products | S (<5ms) |
**🏭 Industrial & Specialized** (Category 5) — Safety and compliance
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| Forklift Proximity | [`ind_forklift_proximity.rs`](../v2/crates/wifi-densepose-wasm-edge/src/ind_forklift_proximity.rs) | Warns when people get too close to vehicles | S (<5ms) |
| Confined Space | [`ind_confined_space.rs`](../v2/crates/wifi-densepose-wasm-edge/src/ind_confined_space.rs) | OSHA-compliant worker monitoring with extraction alerts | S (<5ms) |
| Clean Room | [`ind_clean_room.rs`](../v2/crates/wifi-densepose-wasm-edge/src/ind_clean_room.rs) | Occupancy limits and turbulent motion detection | S (<5ms) |
| Livestock Monitor | [`ind_livestock_monitor.rs`](../v2/crates/wifi-densepose-wasm-edge/src/ind_livestock_monitor.rs) | Animal presence, stillness, and escape alerts | S (<5ms) |
| Structural Vibration | [`ind_structural_vibration.rs`](../v2/crates/wifi-densepose-wasm-edge/src/ind_structural_vibration.rs) | Seismic events, mechanical resonance, structural drift | S (<5ms) |
**🔮 Exotic & Research** (Category 6) — Experimental sensing applications
| Module | File | What It Does | Budget |
|--------|------|-------------|--------|
| Dream Stage | [`exo_dream_stage.rs`](../v2/crates/wifi-densepose-wasm-edge/src/exo_dream_stage.rs) | Contactless sleep stage classification (wake/light/deep/REM) | S (<5ms) |
| Emotion Detection | [`exo_emotion_detect.rs`](../v2/crates/wifi-densepose-wasm-edge/src/exo_emotion_detect.rs) | Arousal, stress, and calm detection from micro-movements | S (<5ms) |
| Gesture Language | [`exo_gesture_language.rs`](../v2/crates/wifi-densepose-wasm-edge/src/exo_gesture_language.rs) | Sign language letter recognition via WiFi | S (<5ms) |
| Music Conductor | [`exo_music_conductor.rs`](../v2/crates/wifi-densepose-wasm-edge/src/exo_music_conductor.rs) | Tempo and dynamic tracking from conducting gestures | S (<5ms) |
| Plant Growth | [`exo_plant_growth.rs`](../v2/crates/wifi-densepose-wasm-edge/src/exo_plant_growth.rs) | Monitors plant growth, circadian rhythms, wilt detection | S (<5ms) |
| Ghost Hunter | [`exo_ghost_hunter.rs`](../v2/crates/wifi-densepose-wasm-edge/src/exo_ghost_hunter.rs) | Environmental anomaly classification (draft/insect/wind/unknown) | S (<5ms) |
| Rain Detection | [`exo_rain_detect.rs`](../v2/crates/wifi-densepose-wasm-edge/src/exo_rain_detect.rs) | Detects rain onset, intensity, and cessation via signal scatter | S (<5ms) |
| Breathing Sync | [`exo_breathing_sync.rs`](../v2/crates/wifi-densepose-wasm-edge/src/exo_breathing_sync.rs) | Detects synchronized breathing between multiple people | S (<5ms) |
</details>
---
<details>
<summary><strong>🧠 Self-Learning WiFi AI (ADR-024)</strong> — Adaptive recognition, self-optimization, and intelligent anomaly detection</summary>
Every WiFi signal that passes through a room creates a unique fingerprint of that space. WiFi-DensePose already reads these fingerprints to track people, but until now it threw away the internal "understanding" after each reading. The Self-Learning WiFi AI captures and preserves that understanding as compact, reusable vectors — and continuously optimizes itself for each new environment.
**What it does in plain terms:**
- Turns any WiFi signal into a 128-number "fingerprint" that uniquely describes what's happening in a room
- Learns entirely on its own from raw WiFi data — no cameras, no labeling, no human supervision needed
- Recognizes rooms, detects intruders, identifies people, and classifies activities using only WiFi
- Runs on an $8 ESP32 chip (the entire model fits in 55 KB of memory)
- Produces both body pose tracking AND environment fingerprints in a single computation
**Key Capabilities**
| What | How it works | Why it matters |
|------|-------------|----------------|
| **Self-supervised learning** | The model watches WiFi signals and teaches itself what "similar" and "different" look like, without any human-labeled data | Deploy anywhere — just plug in a WiFi sensor and wait 10 minutes |
| **Room identification** | Each room produces a distinct WiFi fingerprint pattern | Know which room someone is in without GPS or beacons |
| **Anomaly detection** | An unexpected person or event creates a fingerprint that doesn't match anything seen before | Automatic intrusion and fall detection as a free byproduct |
| **Person re-identification** | Each person disturbs WiFi in a slightly different way, creating a personal signature | Track individuals across sessions without cameras |
| **Environment adaptation** | MicroLoRA adapters (1,792 parameters per room) fine-tune the model for each new space | Adapts to a new room with minimal data — 93% less than retraining from scratch |
| **Memory preservation** | EWC++ regularization remembers what was learned during pretraining | Switching to a new task doesn't erase prior knowledge |
| **Hard-negative mining** | Training focuses on the most confusing examples to learn faster | Better accuracy with the same amount of training data |
**Architecture**
```
WiFi Signal [56 channels] → Transformer + Graph Neural Network
├→ 128-dim environment fingerprint (for search + identification)
└→ 17-joint body pose (for human tracking)
```
**Quick Start**
```bash
# Step 1: Learn from raw WiFi data (no labels needed)
cargo run -p wifi-densepose-sensing-server -- --pretrain --dataset data/csi/ --pretrain-epochs 50
# Step 2: Fine-tune with pose labels for full capability
cargo run -p wifi-densepose-sensing-server -- --train --dataset data/mmfi/ --epochs 100 --save-rvf model.rvf
# Step 3: Use the model — extract fingerprints from live WiFi
cargo run -p wifi-densepose-sensing-server -- --model model.rvf --embed
# Step 4: Search — find similar environments or detect anomalies
cargo run -p wifi-densepose-sensing-server -- --model model.rvf --build-index env
```
**Training Modes**
| Mode | What you need | What you get |
|------|--------------|-------------|
| Self-Supervised | Just raw WiFi data | A model that understands WiFi signal structure |
| Supervised | WiFi data + body pose labels | Full pose tracking + environment fingerprints |
| Cross-Modal | WiFi data + camera footage | Fingerprints aligned with visual understanding |
**Fingerprint Index Types**
| Index | What it stores | Real-world use |
|-------|---------------|----------------|
| `env_fingerprint` | Average room fingerprint | "Is this the kitchen or the bedroom?" |
| `activity_pattern` | Activity boundaries | "Is someone cooking, sleeping, or exercising?" |
| `temporal_baseline` | Normal conditions | "Something unusual just happened in this room" |
| `person_track` | Individual movement signatures | "Person A just entered the living room" |
**Model Size**
| Component | Parameters | Memory (on ESP32) |
|-----------|-----------|-------------------|
| Transformer backbone | ~28,000 | 28 KB |
| Embedding projection head | ~25,000 | 25 KB |
| Per-room MicroLoRA adapter | ~1,800 | 2 KB |
| **Total** | **~55,000** | **55 KB** (of 520 KB available) |
The self-learning system builds on the AI Backbone (RuVector) signal-processing layer — attention, graph algorithms, and compression — adding contrastive learning on top. See [`architecture.md`](architecture.md) for the full pipeline.
See [`docs/adr/ADR-024-contrastive-csi-embedding-model.md`](adr/ADR-024-contrastive-csi-embedding-model.md) for full architectural details.
</details>

View File

@ -105,6 +105,19 @@ export class SensingTab {
</div>
</div>
<!-- ADR-121: mmWave radar (HLK-LD2402) auxiliary range modality -->
<div class="sensing-card" id="mmwaveCard" style="display:none;">
<div class="sensing-card-title">mmWave Radar (24 GHz)</div>
<div class="sensing-classification">
<div class="sensing-class-label" id="mmwaveLabel" style="background:rgba(33,150,243,0.15);color:rgb(33,150,243);"> cm</div>
<div class="sensing-confidence">
<label>Age</label>
<div class="sensing-bar"><div class="sensing-bar-fill confidence" id="mmwaveAgeBar" style="background:rgb(33,150,243);"></div></div>
<span class="sensing-meter-val" id="mmwaveAge"> ms</span>
</div>
</div>
</div>
<!-- Setup info -->
<div class="sensing-card">
<div class="sensing-card-title">About This Data</div>
@ -268,6 +281,35 @@ export class SensingTab {
const confPct = ((c.confidence || 0) * 100).toFixed(0);
this._setBar('barConfidence', c.confidence, 1.0, 'valConfidence', confPct + '%');
// ADR-121: poll mmWave radar in parallel with the WS-driven update.
// Kick once per visible update; skip if already in flight.
if (!this._mmwaveBusy) {
this._mmwaveBusy = true;
fetch('/api/v1/mmwave/latest')
.then(r => r.json())
.then(j => {
const card = this.container.querySelector('#mmwaveCard');
if (!card) { this._mmwaveBusy = false; return; }
if (j && j.available) {
card.style.display = '';
const lbl = this.container.querySelector('#mmwaveLabel');
if (lbl) lbl.textContent = j.distance_cm + ' cm';
const age = this.container.querySelector('#mmwaveAge');
if (age) age.textContent = (j.age_ms || 0) + ' ms';
const bar = this.container.querySelector('#mmwaveAgeBar');
if (bar) {
// Age 0..2000 ms → 100..0% width (fresher = fuller bar).
const pct = Math.max(0, 100 - (j.age_ms || 0) / 20);
bar.style.width = pct + '%';
}
} else {
card.style.display = 'none';
}
})
.catch(() => { /* server down or no port — silently hide */ })
.finally(() => { this._mmwaveBusy = false; });
}
// Details
this._setText('valDomFreq', (f.dominant_freq_hz || 0).toFixed(3) + ' Hz');
this._setText('valChangePoints', String(f.change_points || 0));

583
ui/raw.html Normal file
View File

@ -0,0 +1,583 @@
<!doctype html>
<html lang="en"><head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>RuView — Raw Signals</title>
<style>
:root { color-scheme: dark; }
body { margin:0; padding:14px; font-family:-apple-system,Inter,system-ui,sans-serif;
background:#0a0e13; color:#e6edf3; font-size:12px; }
h1 { font-size:15px; font-weight:600; margin:0 0 2px; }
.sub { font-size:11px; color:#888; margin:0 0 12px; }
.topbar { display:flex; gap:14px; align-items:center; margin-bottom:10px; flex-wrap:wrap; }
.pill { padding:4px 10px; border-radius:4px; font-family:JetBrains Mono,monospace; font-size:11px;
background:#1c2128; }
.pill.dis { background:#3a1418; color:#ff6a6a; }
.pill.ok { background:#0e2a1a; color:#7ce38b; }
button { background:#21262d; color:#e6edf3; border:1px solid #30363d; border-radius:4px;
padding:4px 10px; font-size:11px; cursor:pointer; }
.node { background:#161b22; border:1px solid #30363d; border-radius:6px;
padding:10px 12px; margin-bottom:10px; }
.node h2 { margin:0 0 6px; font-size:12px; font-weight:600; color:#7cb6ff;
font-family:JetBrains Mono,monospace; display:flex; gap:14px; align-items:baseline; }
.node h2 .stat { color:#888; font-weight:normal; font-size:11px; }
.node h2 .stat b { color:#e6edf3; font-weight:600; }
.badge { font-family:JetBrains Mono,monospace; font-size:11px; padding:2px 8px; border-radius:3px; }
.badge.absent { background:#21262d; color:#888; }
.badge.present_still { background:#1c3a55; color:#7cb6ff; }
.badge.present_moving{ background:#3a5520; color:#90d36b; }
.badge.active { background:#552020; color:#ff7a7a; }
.row { display:grid; grid-template-columns: 1fr 360px; gap:10px; }
@media (max-width: 900px) { .row { grid-template-columns: 1fr; } }
canvas { display:block; width:100%; background:#0a0e13; border-radius:3px; }
canvas.bars { height: 130px; }
canvas.trace { height: 130px; }
canvas.spark { height: 48px; margin-top: 6px; }
.lbl { color:#666; font-size:10px; font-family:JetBrains Mono,monospace; margin:2px 0 0; }
.controls { display:flex; gap:8px; margin-left:auto; }
.controls label { font-size:11px; color:#aaa; }
</style>
</head>
<body>
<h1>RuView — Raw CSI signals</h1>
<p class="sub">Per-node subcarrier amplitudes + RSSI/broadband traces. No DSP, no classification. Stream straight from the sensor.</p>
<div class="topbar">
<span id="status" class="pill dis">disconnected</span>
<span class="pill" id="rate">0 fps</span>
<span class="pill" id="lastTs">last: --</span>
<span class="badge absent" id="globalBadge" style="font-size:13px;padding:4px 12px;">absent</span>
<span class="pill" id="globalCV">CV 0%</span>
<!-- ADR-021: WiFi-CSI vital signs — breathing + heart rate (computed server-side). -->
<span class="pill" id="brPill"
style="background:rgba(63,185,80,0.18); color:rgb(63,185,80); border:1px solid rgb(63,185,80);"
title="WiFi-CSI breathing rate from bandpass 0.1-0.5 Hz on broadband amplitude (ADR-021)">
🫁 <b id="brBpm">— BPM</b> <span id="brConf" style="opacity:0.7;font-size:11px">·</span>
</span>
<span class="pill" id="hrPill"
style="background:rgba(248,81,73,0.18); color:rgb(248,81,73); border:1px solid rgb(248,81,73);"
title="WiFi-CSI heart rate from bandpass 0.8-2.0 Hz on broadband amplitude (ADR-021)">
💓 <b id="hrBpm">— BPM</b> <span id="hrConf" style="opacity:0.7;font-size:11px">·</span>
</span>
<!-- ADR-121: HLK-LD2402 24 GHz mmWave radar pill — hidden until first reading. -->
<span class="pill" id="mmwavePill" style="display:none; background:rgba(33,150,243,0.18);
color:rgb(33,150,243); border:1px solid rgb(33,150,243);"
title="HLK-LD2402 24 GHz radar — distance to closest target">
📡 mmWave <b id="mmwaveDist">— cm</b> <span id="mmwaveAge" style="opacity:0.7;font-size:11px">·</span>
</span>
<div class="controls">
<label>peak-hold <input type="checkbox" id="peakHold" checked></label>
<label>log-y <input type="checkbox" id="logY"></label>
<button onclick="resetState()">reset</button>
<button id="calibrateBtn" onclick="startCalibrate()" title="Step out of the room, click, wait 90 s">calibrate empty</button>
<span class="pill" id="calibStatus" style="display:none"></span>
<!-- ADR-107: visible progress bar shown while baseline capture runs. -->
<div id="calibProgress" style="display:none; position:relative; width:140px; height:14px;
border:1px solid #30363d; border-radius:7px; overflow:hidden;
background:#0a0e13;">
<div id="calibProgressFill" style="position:absolute; left:0; top:0; bottom:0; width:0%;
background:linear-gradient(90deg,#1f6feb,#3fb950);
transition: width 0.4s linear;"></div>
<span id="calibProgressLabel" style="position:absolute; inset:0; display:flex;
align-items:center; justify-content:center;
font-size:10px; font-family:JetBrains Mono,monospace;
color:#e6edf3; text-shadow:0 0 2px #000;"></span>
</div>
</div>
</div>
<div id="nodes"></div>
<script>
// ── State ──────────────────────────────────────────────────────────
const TRACE_SEC = 30; // seconds of history per node
const TRACE_MAX_PTS = 1200; // safety cap
const state = new Map(); // node_id -> { amp, peak, rssiHist[], meanAmpHist[], lastTs, frames }
let frameCount = 0;
let lastRateTs = performance.now();
let rateFps = 0;
let logY = false;
let peakHold = true;
function resetState() {
state.clear();
document.getElementById('nodes').innerHTML = '';
frameCount = 0;
}
document.getElementById('peakHold').addEventListener('change', e => { peakHold = e.target.checked; });
document.getElementById('logY').addEventListener('change', e => { logY = e.target.checked; });
// ── Per-node block factory ─────────────────────────────────────────
function ensureNodeBlock(nodeId) {
if (state.has(nodeId)) return state.get(nodeId);
const ent = {
amp: [],
peak: [],
rssiHist: [], // { t, v }
meanAmpHist: [],
driftHist: [], // { t, v } — ADR-104 per-sub drift score
lastTs: 0,
frames: 0,
lastFrameWall: performance.now(),
fps: 0,
};
state.set(nodeId, ent);
const wrap = document.createElement('div');
wrap.className = 'node';
wrap.id = 'node-' + nodeId;
wrap.innerHTML = `
<h2>
Node ${nodeId}
<span class="badge absent" id="n${nodeId}-badge">absent</span>
<span class="stat">CV <b id="n${nodeId}-cv">0%</b></span>
<span class="stat">subc <b id="n${nodeId}-sub">0</b></span>
<span class="stat">rssi <b id="n${nodeId}-rssi">--</b> dBm</span>
<span class="stat">mean A <b id="n${nodeId}-meanA">0</b></span>
<span class="stat">peak A <b id="n${nodeId}-peakA">0</b></span>
<span class="stat">drift <b id="n${nodeId}-drift">--</b></span>
<span class="stat">node fps <b id="n${nodeId}-fps">0</b></span>
</h2>
<div class="row">
<div>
<canvas class="bars" id="n${nodeId}-bars"></canvas>
<p class="lbl">subcarrier amplitude bars (left → low freq, right → high freq)</p>
</div>
<div>
<canvas class="trace" id="n${nodeId}-trace"></canvas>
<p class="lbl"><span style="color:#8b949e">RSSI</span> &nbsp; <span style="color:#3fb950">broadband mean amplitude</span> &nbsp; (last ${TRACE_SEC}s)</p>
<canvas class="spark" id="n${nodeId}-driftSpark"></canvas>
<p class="lbl"><span style="color:#d29922">per-sub drift</span> — off-axis presence channel (ADR-104); dashed line = presence threshold 0.10</p>
</div>
</div>`;
document.getElementById('nodes').appendChild(wrap);
return ent;
}
// ── Drawing ────────────────────────────────────────────────────────
function drawBars(canvas, amps, peaks) {
const w = canvas.clientWidth, h = canvas.clientHeight;
if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#0a0e13'; ctx.fillRect(0, 0, w, h);
if (!amps.length) return;
// Determine scale
let maxV = peakHold && peaks.length
? Math.max(...peaks)
: Math.max(...amps);
if (!isFinite(maxV) || maxV <= 0) maxV = 1;
const n = amps.length;
const bw = w / n;
const margin = 4;
// Bars
for (let i = 0; i < n; i++) {
let v = amps[i];
let pv = peaks[i] || 0;
if (logY) {
v = v > 0 ? Math.log10(v + 1) : 0;
pv = pv > 0 ? Math.log10(pv + 1) : 0;
}
const scaleMax = logY ? Math.log10(maxV + 1) : maxV;
const bh = Math.max(1, (v / scaleMax) * (h - margin));
const ph = Math.max(1, (pv / scaleMax) * (h - margin));
const x = i * bw;
// peak (faint)
if (peakHold && pv > 0) {
ctx.fillStyle = '#1f3a5a';
ctx.fillRect(x, h - ph, Math.max(1, bw - 1), 1.5);
}
// bar (active)
const hue = 200 + (i / n) * 100;
ctx.fillStyle = `hsl(${hue}, 70%, 55%)`;
ctx.fillRect(x, h - bh, Math.max(1, bw - 1), bh);
}
// Y-axis label
ctx.fillStyle = '#555'; ctx.font = '9px monospace';
ctx.fillText('max=' + maxV.toFixed(0), 4, 10);
ctx.fillText('n=' + n, w - 40, 10);
}
function drawTrace(canvas, rssiHist, meanAmpHist) {
const w = canvas.clientWidth, h = canvas.clientHeight;
if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#0a0e13'; ctx.fillRect(0, 0, w, h);
const now = performance.now() / 1000;
const t0 = now - TRACE_SEC;
const drawSeries = (arr, color, getRange) => {
if (arr.length < 2) return;
const visible = arr.filter(p => p.t >= t0);
if (visible.length < 2) return;
const { min, max } = getRange(visible);
const span = (max - min) || 1;
ctx.strokeStyle = color; ctx.lineWidth = 1.5; ctx.beginPath();
for (let i = 0; i < visible.length; i++) {
const p = visible[i];
const x = ((p.t - t0) / TRACE_SEC) * w;
const y = h - ((p.v - min) / span) * (h - 8) - 4;
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.stroke();
// y-range text
ctx.fillStyle = color; ctx.font = '9px monospace';
return { min, max };
};
const rssiR = drawSeries(rssiHist, '#8b949e', arr => {
const vals = arr.map(p => p.v);
return { min: Math.min(...vals), max: Math.max(...vals) };
});
const ampR = drawSeries(meanAmpHist, '#3fb950', arr => {
const vals = arr.map(p => p.v);
return { min: 0, max: Math.max(...vals) };
});
// labels
ctx.font = '9px monospace';
if (rssiR) { ctx.fillStyle = '#8b949e'; ctx.fillText(`rssi ${rssiR.min.toFixed(0)}…${rssiR.max.toFixed(0)} dBm`, 4, 10); }
if (ampR) { ctx.fillStyle = '#3fb950'; ctx.fillText(`A ${ampR.min.toFixed(0)}…${ampR.max.toFixed(0)}`, 4, 22); }
// grid line at now
ctx.strokeStyle = '#1c2128'; ctx.beginPath();
ctx.moveTo(w - 1, 0); ctx.lineTo(w - 1, h); ctx.stroke();
}
// ADR-104: per-sub drift sparkline. Fixed Y range [0, 0.30] so the
// presence threshold (0.10, dashed) and warning threshold (0.15) are
// directly readable across nodes — re-scaling per node would make it
// impossible to tell "Node 0 fired" from "Node 1 fired" at a glance.
const DRIFT_PRESENCE_THRESH = 0.10;
const DRIFT_WARN_THRESH = 0.15;
const DRIFT_MAX = 0.30;
function drawDriftSpark(canvas, hist) {
const w = canvas.clientWidth, h = canvas.clientHeight;
if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#0a0e13'; ctx.fillRect(0, 0, w, h);
const now = performance.now() / 1000;
const t0 = now - TRACE_SEC;
const yOf = v => h - (Math.min(v, DRIFT_MAX) / DRIFT_MAX) * (h - 4) - 2;
// Threshold lines.
ctx.setLineDash([3, 3]);
ctx.strokeStyle = '#5a4a1a'; ctx.lineWidth = 1; ctx.beginPath();
ctx.moveTo(0, yOf(DRIFT_PRESENCE_THRESH)); ctx.lineTo(w, yOf(DRIFT_PRESENCE_THRESH));
ctx.stroke();
ctx.strokeStyle = '#7a3030'; ctx.beginPath();
ctx.moveTo(0, yOf(DRIFT_WARN_THRESH)); ctx.lineTo(w, yOf(DRIFT_WARN_THRESH));
ctx.stroke();
ctx.setLineDash([]);
const visible = hist.filter(p => p.t >= t0);
if (visible.length >= 2) {
ctx.strokeStyle = '#d29922'; ctx.lineWidth = 1.5; ctx.beginPath();
for (let i = 0; i < visible.length; i++) {
const p = visible[i];
const x = ((p.t - t0) / TRACE_SEC) * w;
const y = yOf(p.v);
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.stroke();
}
// Axis text.
ctx.fillStyle = '#666'; ctx.font = '9px monospace';
ctx.fillText('0', 2, h - 2);
ctx.fillText(DRIFT_MAX.toFixed(2), 2, 10);
}
// ── Frame ingestion ────────────────────────────────────────────────
function handleSensingUpdate(d) {
const nodes = d.nodes || [];
const ts = d.timestamp || (Date.now() / 1000);
const now = performance.now() / 1000;
for (const n of nodes) {
const id = n.node_id;
const amps = n.amplitude || [];
// Skip empty-amp ticks (feature_state path doesn't carry raw CSI).
// Bars/traces only refresh on real raw-CSI frames so what you see
// is always a live snapshot, not a repeated stale vector.
if (!amps.length) continue;
const ent = ensureNodeBlock(id);
ent.amp = amps;
// peak-hold update
if (ent.peak.length !== amps.length) ent.peak = amps.slice();
else for (let i = 0; i < amps.length; i++) if (amps[i] > ent.peak[i]) ent.peak[i] = amps[i];
const meanA = amps.reduce((s, x) => s + x, 0) / amps.length;
// Only push valid (non-zero) RSSI samples so the trace doesn't
// jump between real dBm values and the "0 = no data" sentinel.
if (n.rssi_dbm && n.rssi_dbm !== 0) {
ent.rssiHist.push({ t: now, v: n.rssi_dbm });
}
ent.meanAmpHist.push({ t: now, v: meanA });
const cutoff = now - TRACE_SEC;
while (ent.rssiHist.length && ent.rssiHist[0].t < cutoff) ent.rssiHist.shift();
while (ent.meanAmpHist.length && ent.meanAmpHist[0].t < cutoff) ent.meanAmpHist.shift();
if (ent.rssiHist.length > TRACE_MAX_PTS) ent.rssiHist.splice(0, ent.rssiHist.length - TRACE_MAX_PTS);
if (ent.meanAmpHist.length > TRACE_MAX_PTS) ent.meanAmpHist.splice(0, ent.meanAmpHist.length - TRACE_MAX_PTS);
// per-node fps: count frames in the last second, refresh once a sec
// (instantaneous 1/dt was wildly noisy because multiple WS paths
// emit duplicate per-node updates back-to-back).
ent.fpsCounter = (ent.fpsCounter || 0) + 1;
const nowMs = performance.now();
if (!ent.fpsWindowStart) ent.fpsWindowStart = nowMs;
if (nowMs - ent.fpsWindowStart >= 1000) {
ent.fps = ent.fpsCounter * 1000 / (nowMs - ent.fpsWindowStart);
ent.fpsCounter = 0;
ent.fpsWindowStart = nowMs;
}
ent.lastFrameWall = nowMs;
ent.frames++;
ent.lastTs = ts;
document.getElementById(`n${id}-sub`).textContent = amps.length;
// n.rssi_dbm comes from sensing_update.nodes[]; it can be 0 on
// early ticks (history not yet populated). Coerce to "--" so the
// operator doesn't think the AP is dead.
const rssiVal = (n.rssi_dbm && Number.isFinite(n.rssi_dbm) && n.rssi_dbm !== 0)
? n.rssi_dbm.toFixed(1)
: '--';
document.getElementById(`n${id}-rssi`).textContent = rssiVal;
// Push to RSSI trace history if non-zero (so the chart shows the
// real ladder of dBm steps, not a fake "0 → -54" jump on boot).
if (n.rssi_dbm && n.rssi_dbm !== 0) {
// (handled by ent.rssiHist push below)
}
document.getElementById(`n${id}-meanA`).textContent = meanA.toFixed(1);
document.getElementById(`n${id}-peakA`).textContent = Math.max(...ent.peak).toFixed(1);
document.getElementById(`n${id}-fps`).textContent = ent.fps.toFixed(1);
}
document.getElementById('lastTs').textContent = 'last: ' + new Date(ts * 1000).toLocaleTimeString();
// Global classification badge (ADR-101 fused).
const gcl = d.classification || {};
const glvl = gcl.motion_level || 'absent';
const gb = document.getElementById('globalBadge');
if (gb) { gb.textContent = glvl; gb.className = 'badge ' + glvl; gb.style.fontSize = '13px'; gb.style.padding = '4px 12px'; }
const gcv = document.getElementById('globalCV');
if (gcv) gcv.textContent = 'CV ' + ((gcl.confidence || 0) * 100).toFixed(1) + '%';
// ADR-021 — WiFi-CSI vital signs (breathing + heart rate).
// `vital_signs` is embedded in SensingUpdate; values may be null
// when the detector hasn't accumulated enough history yet (~10s).
const vs = d.vital_signs || {};
const brBpm = document.getElementById('brBpm');
const brConf = document.getElementById('brConf');
const hrBpm = document.getElementById('hrBpm');
const hrConf = document.getElementById('hrConf');
const brPill = document.getElementById('brPill');
const hrPill = document.getElementById('hrPill');
if (vs && typeof vs.breathing_rate_bpm === 'number' && Number.isFinite(vs.breathing_rate_bpm) && vs.breathing_rate_bpm > 0) {
if (brBpm) brBpm.textContent = vs.breathing_rate_bpm.toFixed(1) + ' BPM';
if (brConf) brConf.textContent = '· ' + ((vs.breathing_confidence || 0) * 100).toFixed(0) + '%';
if (brPill) brPill.style.opacity = (vs.breathing_confidence || 0) < 0.2 ? '0.5' : '1.0';
} else {
if (brBpm) brBpm.textContent = '— BPM';
if (brConf) brConf.textContent = '·';
if (brPill) brPill.style.opacity = '0.5';
}
if (vs && typeof vs.heart_rate_bpm === 'number' && Number.isFinite(vs.heart_rate_bpm) && vs.heart_rate_bpm > 0) {
if (hrBpm) hrBpm.textContent = vs.heart_rate_bpm.toFixed(0) + ' BPM';
if (hrConf) hrConf.textContent = '· ' + ((vs.heartbeat_confidence || 0) * 100).toFixed(0) + '%';
if (hrPill) hrPill.style.opacity = (vs.heartbeat_confidence || 0) < 0.2 ? '0.5' : '1.0';
} else {
if (hrBpm) hrBpm.textContent = '— BPM';
if (hrConf) hrConf.textContent = '·';
if (hrPill) hrPill.style.opacity = '0.5';
}
// Per-node level badge from node_features[i].classification (ADR-101).
const nfNow = performance.now() / 1000;
const nf = d.node_features || [];
for (const f of nf) {
const id = f.node_id;
const cls = f.classification || {};
const lvl = cls.motion_level || 'absent';
const badge = document.getElementById(`n${id}-badge`);
if (badge) {
badge.textContent = lvl;
badge.className = 'badge ' + lvl;
}
const cvEl = document.getElementById(`n${id}-cv`);
if (cvEl) cvEl.textContent = ((cls.confidence || 0) * 100).toFixed(1) + '%';
// ADR-104 per-sub drift score (off-axis presence). May be absent
// when no per-sub baseline is loaded for this node — show '--'
// instead of '0.000' so the operator can tell the channel is
// unknown vs. known and stable.
const driftEl = document.getElementById(`n${id}-drift`);
const driftLive = state.get(id);
if (typeof f.drift_score === 'number' && Number.isFinite(f.drift_score)) {
if (driftEl) driftEl.textContent = f.drift_score.toFixed(3);
if (driftLive) {
driftLive.driftHist.push({ t: nfNow, v: f.drift_score });
const cutoff = nfNow - TRACE_SEC;
while (driftLive.driftHist.length && driftLive.driftHist[0].t < cutoff) {
driftLive.driftHist.shift();
}
if (driftLive.driftHist.length > TRACE_MAX_PTS) {
driftLive.driftHist.splice(0, driftLive.driftHist.length - TRACE_MAX_PTS);
}
}
} else if (driftEl) {
driftEl.textContent = '--';
}
}
frameCount++;
}
function renderTick() {
for (const [id, ent] of state) {
const bars = document.getElementById('n' + id + '-bars');
const trace = document.getElementById('n' + id + '-trace');
const spark = document.getElementById('n' + id + '-driftSpark');
if (bars) drawBars(bars, ent.amp, ent.peak);
if (trace) drawTrace(trace, ent.rssiHist, ent.meanAmpHist);
if (spark) drawDriftSpark(spark, ent.driftHist);
}
// fps pill
const now = performance.now();
if (now - lastRateTs > 500) {
rateFps = (frameCount * 1000) / (now - lastRateTs);
document.getElementById('rate').textContent = rateFps.toFixed(1) + ' fps total';
frameCount = 0;
lastRateTs = now;
}
requestAnimationFrame(renderTick);
}
requestAnimationFrame(renderTick);
// ── ADR-107: baseline calibrate button + progress bar ─────────────
let calibPollTimer = null;
const CALIB_DURATION_SEC = 90;
function setCalibProgress(pct, label) {
const bar = document.getElementById('calibProgress');
const fill = document.getElementById('calibProgressFill');
const txt = document.getElementById('calibProgressLabel');
if (!bar || !fill || !txt) return;
bar.style.display = pct < 0 ? 'none' : 'inline-block';
fill.style.width = Math.max(0, Math.min(100, pct)) + '%';
txt.textContent = label || '';
}
async function startCalibrate() {
if (!confirm(`Step OUT of the room now. Calibration will record for ${CALIB_DURATION_SEC} s.\nClick OK when you are out.`)) return;
const btn = document.getElementById('calibrateBtn');
const stat = document.getElementById('calibStatus');
btn.disabled = true; btn.textContent = 'recording…';
// Hide the text-pill while the progress bar is the primary indicator;
// it reappears only on terminal status messages (error / complete).
stat.style.display = 'none';
setCalibProgress(0, 'starting…');
try {
const res = await fetch('/api/v1/baseline/calibrate', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ duration_sec: CALIB_DURATION_SEC, trim_sec: 15, clean_window_sec: 30 }),
});
const j = await res.json();
if (!j.started) {
setCalibProgress(-1, '');
stat.style.display = 'inline-block';
stat.textContent = j.reason || 'failed to start';
btn.disabled = false; btn.textContent = 'calibrate empty';
return;
}
} catch (e) {
setCalibProgress(-1, '');
stat.style.display = 'inline-block';
stat.textContent = 'network error';
btn.disabled = false; btn.textContent = 'calibrate empty';
return;
}
if (calibPollTimer) clearInterval(calibPollTimer);
let elapsed = 0;
calibPollTimer = setInterval(async () => {
elapsed += 2;
try {
const r = await fetch('/api/v1/baseline'); const j = await r.json();
const s = j.calibration_status || 'idle';
if (s.startsWith('running')) {
const pct = Math.min(99, (elapsed / CALIB_DURATION_SEC) * 100);
setCalibProgress(pct, `${elapsed}/${CALIB_DURATION_SEC} s`);
} else {
clearInterval(calibPollTimer); calibPollTimer = null;
btn.disabled = false; btn.textContent = 'calibrate empty';
if (s === 'complete') {
setCalibProgress(100, 'done');
stat.style.display = 'inline-block';
stat.textContent = 'baseline updated ✓';
setTimeout(() => setCalibProgress(-1, ''), 3000);
} else {
setCalibProgress(-1, '');
stat.style.display = 'inline-block';
stat.textContent = s;
}
}
} catch (e) {}
}, 2000);
}
// ── WS ─────────────────────────────────────────────────────────────
function connect() {
const ws = new WebSocket('ws://' + location.hostname + ':8765/ws/sensing');
ws.onopen = () => {
const p = document.getElementById('status');
p.textContent = 'connected'; p.className = 'pill ok';
};
ws.onclose = () => {
const p = document.getElementById('status');
p.textContent = 'disconnected — reconnecting'; p.className = 'pill dis';
setTimeout(connect, 1500);
};
ws.onmessage = (e) => {
try {
const d = JSON.parse(e.data);
if (d.type === 'sensing_update') handleSensingUpdate(d);
} catch (_) {}
};
}
connect();
// ── ADR-121: poll HLK-LD2402 mmWave radar @ 5 Hz ─────────────────────
const mmwavePill = document.getElementById('mmwavePill');
const mmwaveDist = document.getElementById('mmwaveDist');
const mmwaveAge = document.getElementById('mmwaveAge');
let mmwaveBusy = false;
async function pollMmwave() {
if (mmwaveBusy) return; mmwaveBusy = true;
try {
const r = await fetch('/api/v1/mmwave/latest', { cache: 'no-store' });
if (!r.ok) throw new Error('http ' + r.status);
const j = await r.json();
if (j && j.available) {
mmwavePill.style.display = '';
mmwaveDist.textContent = j.distance_cm + ' cm';
const age = Math.round(j.age_ms || 0);
mmwaveAge.textContent = '· ' + age + ' ms';
// Fade pill if stale (>1.5 s) before server hides at 2 s.
mmwavePill.style.opacity = age > 1500 ? '0.5' : '1.0';
} else {
mmwavePill.style.display = 'none';
}
} catch (_) {
mmwavePill.style.display = 'none';
} finally { mmwaveBusy = false; }
}
pollMmwave();
setInterval(pollMmwave, 200);
</script>
</body></html>

View File

@ -2033,6 +2033,22 @@ canvas {
color: var(--color-error);
}
/* ADR-117 follow-up: extended class vocabulary from adaptive_classifier. */
.sensing-class-label.present_moving {
background: rgba(var(--color-warning-rgb), 0.15);
color: var(--color-warning);
}
.sensing-class-label.waving {
background: rgba(155, 89, 182, 0.15); /* purple — gestures, body still */
color: rgb(155, 89, 182);
}
.sensing-class-label.transition {
background: rgba(230, 126, 34, 0.18); /* orange — discrete event */
color: rgb(230, 126, 34);
}
.sensing-confidence {
display: grid;
grid-template-columns: 70px 1fr 40px;

1
v2/Cargo.lock generated
View File

@ -8550,6 +8550,7 @@ dependencies = [
"ruvector-mincut",
"serde",
"serde_json",
"serialport",
"tempfile",
"tokio",
"tower 0.4.13",

View File

@ -56,6 +56,10 @@ wifi-densepose-signal = { version = "0.3.0", path = "../wifi-densepose-signal",
midstreamer-temporal-compare = "0.2" # DTW / LCS / Edit-Distance pattern matching
midstreamer-attractor = "0.2" # Lyapunov + regime classification
# ADR-121: HLK-LD2402 24 GHz mmWave radar over UART (auxiliary vitals/
# range modality). Optional — server runs fine without the module attached.
serialport.workspace = true
[dev-dependencies]
tempfile = "3.10"
# `tower::ServiceExt::oneshot` for in-process Router tests (bearer_auth).

View File

@ -21,3 +21,5 @@ pub mod sparse_inference;
pub mod embedding;
/// ADR-116: WiFlow-v1 supervised pose model loader + Rust forward pass.
pub mod wiflow_v1;
/// ADR-121: HLK-LD2402 24 GHz mmWave radar reader (auxiliary modality).
pub mod mmwave;

View File

@ -1174,6 +1174,20 @@ struct Args {
/// Independent from `--model` (RVF container) and `--load-rvf`.
#[arg(long, value_name = "PATH")]
wiflow_model: Option<PathBuf>,
/// ADR-121: Path to HLK-LD2402 24 GHz mmWave radar UART (via
/// CP2102 USB bridge). Example: `/dev/cu.usbserial-1140` (macOS)
/// or `/dev/ttyUSB0` (Linux). When set, the server reads
/// `distance:<cm>` lines at 6 Hz and surfaces them on the
/// `mmwave` field of every SensingUpdate plus the
/// `/api/v1/mmwave/latest` REST endpoint. Missing port = no
/// mmWave, server still runs.
#[arg(long, value_name = "PATH")]
mmwave_port: Option<String>,
/// ADR-121: HLK-LD2402 baud rate. Factory default is 115200 8N1.
#[arg(long, default_value = "115200")]
mmwave_baud: u32,
}
/// ADR-116: globally-shared WiFlow-v1 model. Loaded once at startup if
@ -5069,6 +5083,23 @@ async fn adaptive_debug() -> Json<serde_json::Value> {
}))
}
/// ADR-121: GET /api/v1/mmwave/latest — latest HLK-LD2402 reading or
/// `{ available: false }` when the reader thread isn't running OR the
/// most recent reading is stale (>2 seconds old).
async fn mmwave_latest() -> Json<serde_json::Value> {
use wifi_densepose_sensing_server::mmwave;
match mmwave::current(std::time::Duration::from_secs(2)) {
Some(r) => Json(serde_json::json!({
"available": true,
"distance_cm": r.distance_cm,
"age_ms": r.at.elapsed().as_millis() as u64,
})),
None => Json(serde_json::json!({
"available": false,
})),
}
}
/// POST /api/v1/adaptive/unload — unload the adaptive model (revert to thresholds).
async fn adaptive_unload(State(state): State<SharedState>) -> Json<serde_json::Value> {
let mut s = state.write().await;
@ -7364,6 +7395,11 @@ async fn main() {
}
};
// ADR-121: spawn the HLK-LD2402 mmWave reader thread when --mmwave-port is set.
if let Some(port) = args.mmwave_port.clone() {
wifi_densepose_sensing_server::mmwave::spawn_reader(port, args.mmwave_baud);
}
// Load trained model via --model (uses progressive loading if --progressive set)
let model_path = args.model.as_ref().or(args.load_rvf.as_ref());
let mut progressive_loader: Option<ProgressiveLoader> = None;
@ -7648,6 +7684,7 @@ async fn main() {
.route("/api/v1/adaptive/train", post(adaptive_train))
.route("/api/v1/adaptive/status", get(adaptive_status))
.route("/api/v1/adaptive/debug", get(adaptive_debug))
.route("/api/v1/mmwave/latest", get(mmwave_latest))
.route("/api/v1/adaptive/unload", post(adaptive_unload))
// Field model calibration (eigenvalue-based person counting)
.route("/api/v1/calibration/start", post(calibration_start))

View File

@ -0,0 +1,116 @@
//! ADR-121: HLK-LD2402 24 GHz mmWave radar reader.
//!
//! Auxiliary range/vitals modality, attached over a CP2102 USB-UART
//! bridge. The module ships factory firmware that emits ASCII
//! `distance:<cm>\r\n` lines @ 115200 baud, ~6 Hz, in Normal Mode.
//!
//! This reader runs in a dedicated thread (blocking serial I/O is
//! awkward inside tokio) and pushes the latest reading + monotonic
//! timestamp into a global `OnceLock<Mutex<…>>` that the broadcast
//! tick task reads.
//!
//! Cold-start tolerance: if the port cannot be opened, the thread
//! logs once and exits cleanly — the server keeps running with WiFi
//! sensing only. No panics, no retries (operator can hot-plug; if
//! they want auto-reconnect we can add it later).
use std::io::Read;
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};
/// Latest mmWave reading + when it landed.
#[derive(Debug, Clone, Copy)]
pub struct MmwaveReading {
pub distance_cm: u32,
pub at: Instant,
}
static LATEST: OnceLock<Mutex<Option<MmwaveReading>>> = OnceLock::new();
fn latest() -> &'static Mutex<Option<MmwaveReading>> {
LATEST.get_or_init(|| Mutex::new(None))
}
/// Returns the most recent reading if it landed within `staleness`.
pub fn current(staleness: Duration) -> Option<MmwaveReading> {
let g = latest().lock().unwrap();
let r = (*g)?;
if r.at.elapsed() <= staleness { Some(r) } else { None }
}
/// Spawn the blocking serial reader thread. Returns immediately.
/// `port` example: `/dev/cu.usbserial-1140` (macOS) or `/dev/ttyUSB0`
/// (Linux). `baud` should be 115200 for HLK-LD2402 default firmware.
pub fn spawn_reader(port: String, baud: u32) {
std::thread::Builder::new()
.name("mmwave-reader".into())
.spawn(move || run(port, baud))
.expect("failed to spawn mmwave-reader thread");
}
fn run(port: String, baud: u32) {
let mut serial = match serialport::new(&port, baud)
.timeout(Duration::from_millis(500))
.open()
{
Ok(s) => {
tracing::info!("ADR-121 mmWave reader: opened {port} @ {baud}");
s
}
Err(e) => {
tracing::warn!("ADR-121 mmWave reader: cannot open {port} @ {baud}: {e}");
return;
}
};
let mut buf = Vec::with_capacity(256);
let mut tmp = [0u8; 128];
loop {
match serial.read(&mut tmp) {
Ok(0) => continue,
Ok(n) => buf.extend_from_slice(&tmp[..n]),
Err(e) => {
if e.kind() == std::io::ErrorKind::TimedOut { continue; }
tracing::warn!("ADR-121 mmWave reader: read error: {e}");
return;
}
}
// Drain complete lines.
while let Some(pos) = buf.iter().position(|&b| b == b'\n') {
let raw_line: Vec<u8> = buf.drain(..=pos).collect();
let line = String::from_utf8_lossy(&raw_line).trim().to_string();
if let Some(cm) = parse_distance(&line) {
*latest().lock().unwrap() = Some(MmwaveReading {
distance_cm: cm,
at: Instant::now(),
});
} else if !line.is_empty() {
tracing::trace!("mmwave non-distance line: {line:?}");
}
}
// Guard against runaway buffer if module emits non-newline garbage.
if buf.len() > 1024 { buf.clear(); }
}
}
/// Parse `distance:<digits>` (HLK-LD2402 Normal Mode line format).
pub fn parse_distance(line: &str) -> Option<u32> {
let lower = line.trim().to_ascii_lowercase();
let rest = lower.strip_prefix("distance:")?;
rest.parse::<u32>().ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_distance_lines() {
assert_eq!(parse_distance("distance:228"), Some(228));
assert_eq!(parse_distance("Distance:0"), Some(0));
assert_eq!(parse_distance(" distance:42 "), Some(42));
assert_eq!(parse_distance("OFF"), None);
assert_eq!(parse_distance(""), None);
assert_eq!(parse_distance("distance:abc"), None);
}
}

View File

@ -48,6 +48,23 @@
<span class="pill" id="lastTs">last: --</span>
<span class="badge absent" id="globalBadge" style="font-size:13px;padding:4px 12px;">absent</span>
<span class="pill" id="globalCV">CV 0%</span>
<!-- ADR-021: WiFi-CSI vital signs — breathing + heart rate (computed server-side). -->
<span class="pill" id="brPill"
style="background:rgba(63,185,80,0.18); color:rgb(63,185,80); border:1px solid rgb(63,185,80);"
title="WiFi-CSI breathing rate from bandpass 0.1-0.5 Hz on broadband amplitude (ADR-021)">
🫁 <b id="brBpm">— BPM</b> <span id="brConf" style="opacity:0.7;font-size:11px">·</span>
</span>
<span class="pill" id="hrPill"
style="background:rgba(248,81,73,0.18); color:rgb(248,81,73); border:1px solid rgb(248,81,73);"
title="WiFi-CSI heart rate from bandpass 0.8-2.0 Hz on broadband amplitude (ADR-021)">
💓 <b id="hrBpm">— BPM</b> <span id="hrConf" style="opacity:0.7;font-size:11px">·</span>
</span>
<!-- ADR-121: HLK-LD2402 24 GHz mmWave radar pill — hidden until first reading. -->
<span class="pill" id="mmwavePill" style="display:none; background:rgba(33,150,243,0.18);
color:rgb(33,150,243); border:1px solid rgb(33,150,243);"
title="HLK-LD2402 24 GHz radar — distance to closest target">
📡 mmWave <b id="mmwaveDist">— cm</b> <span id="mmwaveAge" style="opacity:0.7;font-size:11px">·</span>
</span>
<div class="controls">
<label>peak-hold <input type="checkbox" id="peakHold" checked></label>
<label>log-y <input type="checkbox" id="logY"></label>
@ -352,6 +369,35 @@ function handleSensingUpdate(d) {
const gcv = document.getElementById('globalCV');
if (gcv) gcv.textContent = 'CV ' + ((gcl.confidence || 0) * 100).toFixed(1) + '%';
// ADR-021 — WiFi-CSI vital signs (breathing + heart rate).
// `vital_signs` is embedded in SensingUpdate; values may be null
// when the detector hasn't accumulated enough history yet (~10s).
const vs = d.vital_signs || {};
const brBpm = document.getElementById('brBpm');
const brConf = document.getElementById('brConf');
const hrBpm = document.getElementById('hrBpm');
const hrConf = document.getElementById('hrConf');
const brPill = document.getElementById('brPill');
const hrPill = document.getElementById('hrPill');
if (vs && typeof vs.breathing_rate_bpm === 'number' && Number.isFinite(vs.breathing_rate_bpm) && vs.breathing_rate_bpm > 0) {
if (brBpm) brBpm.textContent = vs.breathing_rate_bpm.toFixed(1) + ' BPM';
if (brConf) brConf.textContent = '· ' + ((vs.breathing_confidence || 0) * 100).toFixed(0) + '%';
if (brPill) brPill.style.opacity = (vs.breathing_confidence || 0) < 0.2 ? '0.5' : '1.0';
} else {
if (brBpm) brBpm.textContent = '— BPM';
if (brConf) brConf.textContent = '·';
if (brPill) brPill.style.opacity = '0.5';
}
if (vs && typeof vs.heart_rate_bpm === 'number' && Number.isFinite(vs.heart_rate_bpm) && vs.heart_rate_bpm > 0) {
if (hrBpm) hrBpm.textContent = vs.heart_rate_bpm.toFixed(0) + ' BPM';
if (hrConf) hrConf.textContent = '· ' + ((vs.heartbeat_confidence || 0) * 100).toFixed(0) + '%';
if (hrPill) hrPill.style.opacity = (vs.heartbeat_confidence || 0) < 0.2 ? '0.5' : '1.0';
} else {
if (hrBpm) hrBpm.textContent = '— BPM';
if (hrConf) hrConf.textContent = '·';
if (hrPill) hrPill.style.opacity = '0.5';
}
// Per-node level badge from node_features[i].classification (ADR-101).
const nfNow = performance.now() / 1000;
const nf = d.node_features || [];
@ -505,5 +551,33 @@ function connect() {
};
}
connect();
// ── ADR-121: poll HLK-LD2402 mmWave radar @ 5 Hz ─────────────────────
const mmwavePill = document.getElementById('mmwavePill');
const mmwaveDist = document.getElementById('mmwaveDist');
const mmwaveAge = document.getElementById('mmwaveAge');
let mmwaveBusy = false;
async function pollMmwave() {
if (mmwaveBusy) return; mmwaveBusy = true;
try {
const r = await fetch('/api/v1/mmwave/latest', { cache: 'no-store' });
if (!r.ok) throw new Error('http ' + r.status);
const j = await r.json();
if (j && j.available) {
mmwavePill.style.display = '';
mmwaveDist.textContent = j.distance_cm + ' cm';
const age = Math.round(j.age_ms || 0);
mmwaveAge.textContent = '· ' + age + ' ms';
// Fade pill if stale (>1.5 s) before server hides at 2 s.
mmwavePill.style.opacity = age > 1500 ? '0.5' : '1.0';
} else {
mmwavePill.style.display = 'none';
}
} catch (_) {
mmwavePill.style.display = 'none';
} finally { mmwaveBusy = false; }
}
pollMmwave();
setInterval(pollMmwave, 200);
</script>
</body></html>

View File

@ -1,267 +0,0 @@
{
"class_stats": [
{
"label": "absent",
"count": 862,
"mean": [
66.68196972264862,
67.23973219951662,
65.0340640002779,
205.65861248066514,
1.2587006960556917,
8.192575406032482,
0.0,
9.823395623712905,
6.970045450727901,
-0.04488812678641681,
-0.9594767860850162,
10.78889030301701,
0.8330000846014487,
22.47189099978742,
22.47189099978742
],
"stddev": [
64.0493846652119,
90.27545165651007,
40.157907144682206,
161.60550836256004,
1.3807130815029451,
3.2814660018571113,
0.0,
2.219723108446689,
1.6521309619598676,
0.342852106459665,
0.30620004291079783,
3.529722483499124,
0.17574148506941875,
5.519861526721805,
5.519861526721805
]
},
{
"label": "present_still",
"count": 852,
"mean": [
66.39259262094396,
64.42298266818027,
68.34546366405283,
203.34049479166666,
1.1900821596244182,
8.200704225352112,
0.0,
10.032339700775715,
7.234479413048846,
0.027056637948278107,
-0.9161490234231624,
10.991429347401095,
0.8298622589530178,
23.588978503428145,
23.588978503428145
],
"stddev": [
59.144593976065984,
82.61098004853669,
40.08306971525127,
152.89405234329087,
1.2031203046363153,
3.0571012493320526,
0.0,
2.22294769203091,
1.6508044238677446,
0.3315329147240876,
0.29437997092330526,
3.3214071045026303,
0.17096813624285292,
5.622953396738593,
5.622953396738593
]
},
{
"label": "present_moving",
"count": 808,
"mean": [
65.17005228763453,
66.55424930761484,
63.785855267654334,
208.73719832920793,
1.3400990099009942,
7.570544554455446,
0.0,
10.069915394050431,
6.923405617584522,
-0.1440461642917184,
-1.0022460352626226,
10.664608744841848,
0.8384559212414682,
21.798331033369895,
21.798331033369895
],
"stddev": [
66.1800697503931,
93.22042148141067,
42.07226450730718,
164.93282045618218,
1.3706144246607475,
3.1453995481213224,
0.0,
2.431170975696439,
1.672707406405861,
0.35643090355922863,
0.30897080072710387,
3.325911716352165,
0.1806597020966414,
5.418714527442832,
5.418714527442832
]
},
{
"label": "active",
"count": 794,
"mean": [
61.85289600233076,
61.12723986655727,
62.468831971775344,
193.2018524349286,
1.2329974811083138,
8.083123425692696,
0.0,
9.747035051350043,
7.009904234422278,
0.007176072447431498,
-0.9950501087764124,
11.015545839210892,
0.8278984910895401,
22.445656559614797,
22.445656559614797
],
"stddev": [
50.44687370766278,
74.07914900524236,
31.558067649516538,
121.0762294406304,
1.2507304998955402,
3.4503520526220344,
0.0,
2.2730029390882156,
1.6768264387667406,
0.3214256392367928,
0.31003127617615406,
3.1187829194728285,
0.1772099351197549,
5.595050695741912,
5.595050695741912
]
}
],
"weights": [
[
0.9923736589617821,
-0.4600422332552322,
-0.3922101552522972,
-0.1686954616947851,
-0.08471937018349271,
0.033940973559074515,
0.0,
-1.116294981490482,
-0.213861080404439,
-0.41727297566573723,
0.08025552056009382,
0.20864577739519874,
0.36814779033318357,
0.46242679535538855,
0.46242679535538855,
0.09475205040199337
],
[
0.04661470129518883,
0.7974124099989739,
0.3953040913806362,
-1.2708868935843511,
0.10073070355913086,
0.0735810797517633,
0.0,
-0.3957608057630568,
0.22091779039114648,
-0.43105406953304665,
0.24907697332262252,
-0.17604200203759515,
-0.5059663705836186,
0.5740861193153091,
0.5740861193153091,
0.020569218347928304
],
[
-0.5295363836864718,
0.14729609046092632,
0.16131671233151712,
0.15039859740752318,
0.08189110214725194,
-0.1429062024394049,
0.0,
2.459247211223509,
-0.162133339181718,
0.6345474095048843,
0.16626892477248892,
0.2710091094981082,
-0.08197569509399917,
-1.2007197895193034,
-1.2007197895193034,
-0.10027402587742726
],
[
-0.5094519765704947,
-0.48466626720467487,
-0.1644106484598614,
1.2891837578716183,
-0.0979024355228887,
0.0353841491285671,
0.0,
-0.9471914239699604,
0.15507662919500606,
0.2137796356938993,
-0.49560141865520463,
-0.30361288485571664,
0.21979427534444013,
0.16420687484859928,
0.16420687484859928,
-0.015047242872495047
]
],
"global_mean": [
65.08291570815048,
64.88537161757283,
64.96650236787292,
202.8304440905207,
1.25474969843183,
8.016887816646562,
0.0,
9.918865477040464,
7.036167472733628,
-0.038097952045357715,
-0.9672836370393502,
10.86491812646321,
0.8323017200972911,
22.58850497890069,
22.58850497890069
],
"global_std": [
60.376895354908775,
85.49291935872783,
38.814475392686795,
151.54766198012683,
1.3049002582695195,
3.2446975526483737,
1e-9,
2.2904371592847603,
1.667114434239705,
0.34470363318292857,
0.3067332188136679,
3.334427501751985,
0.17614366955910027,
5.577838072123601,
5.577838072123601
],
"trained_frames": 3316,
"training_accuracy": 0.4149577804583836,
"version": 1
}