Compare commits
9 Commits
12e1cf9d5e
...
f6adcb2014
| Author | SHA1 | Date |
|---|---|---|
|
|
f6adcb2014 | |
|
|
e53a2e1f5c | |
|
|
b74ffd958a | |
|
|
a36af57d19 | |
|
|
cb6e24ed57 | |
|
|
831602b584 | |
|
|
2538fa2fab | |
|
|
4075b6082d | |
|
|
e2c68191a2 |
|
|
@ -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
|
||||
|
|
|
|||
70
CHECKLIST.md
|
|
@ -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
|
|
@ -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 (var→const, 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
|
|
@ -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.
|
||||
|
||||
[](https://www.rust-lang.org/)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/ruvnet/RuView)
|
||||
[](https://hub.docker.com/r/ruvnet/wifi-densepose)
|
||||
[](#vital-sign-detection)
|
||||
[](#esp32-s3-hardware-pipeline)
|
||||
[](docs/use-cases.md)
|
||||
[](docs/use-cases.md)
|
||||
[](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
|
|||
|
|
||||
<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)**
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 197 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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> <span style="color:#3fb950">broadband mean amplitude</span> (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>
|
||||
16
ui/style.css
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -8550,6 +8550,7 @@ dependencies = [
|
|||
"ruvector-mincut",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serialport",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tower 0.4.13",
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||