Compare commits

..

3 Commits

Author SHA1 Message Date
ruv 4a518723d5 fix: add --tdm-slot, --tdm-total, and --edge-tier to provision.py
The user guide and release notes document TDM and edge intelligence
provisioning flags but provision.py only accepted --ssid, --password,
--target-ip, --target-port, and --node-id.

Add all NVS keys the firmware actually reads:
- --tdm-slot / --tdm-total: TDM mesh slot assignment
- --edge-tier: edge processing tier (0=off, 1=stats, 2=vitals)
- --pres-thresh, --fall-thresh: detection thresholds
- --vital-win, --vital-int: vitals timing parameters
- --subk-count: top-K subcarrier selection

Also validates that --tdm-slot and --tdm-total are specified together
and that slot < total.

Closes #130

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-03 15:50:52 -05:00
rUv 86f08303e6 docs: update changelog, user guide, and README for ADR-043 (#128)
- CHANGELOG: add ADR-043 entries (14 new API endpoints, WebSocket fix,
  mobile WS fix, 25 real mobile tests)
- README: update ADR count from 41 to 43
- CLAUDE.md: update ADR count from 32 to 43
- User guide: add 14 new REST endpoints to API reference table, note
  that /ws/sensing is available on the HTTP port, update ADR count
2026-03-03 15:21:52 -05:00
rUv d4fb7d30d3 fix: complete sensing server API, WebSocket connectivity, and mobile tests (#125)
The web UI had persistent 404 errors on model, recording, and training
endpoints, and the sensing WebSocket never connected on Dashboard/Live
Demo tabs because sensingService.start() was only called lazily on
Sensing tab visit.

Server (main.rs):
- Add 14 fully-functional Axum handlers: model CRUD (7), recording
  lifecycle (4), training control (3)
- Scan data/models/ and data/recordings/ at startup
- Recording writes CSI frames to .jsonl via tokio background task
- Model load/unload lifecycle with state tracking

Web UI (app.js):
- Import and start sensingService early in initializeServices() so
  Dashboard and Live Demo tabs connect to /ws/sensing immediately

Mobile (ws.service.ts):
- Fix WebSocket URL builder to use same-origin port instead of
  hardcoded port 3001

Mobile (jest.config.js):
- Fix testPathIgnorePatterns that was ignoring the entire test directory

Mobile (25 test files):
- Replace all it.todo() placeholder tests with real implementations
  covering components, services, stores, hooks, screens, and utils

ADR-043 documents all changes.
2026-03-03 13:27:03 -05:00
5 changed files with 103 additions and 22 deletions
+10
View File
@@ -8,6 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- **Sensing server UI API completion (ADR-043)** — 14 fully-functional REST endpoints for model management, CSI recording, and training control
- Model CRUD: `GET /api/v1/models`, `GET /api/v1/models/active`, `POST /api/v1/models/load`, `POST /api/v1/models/unload`, `DELETE /api/v1/models/:id`, `GET /api/v1/models/lora/profiles`, `POST /api/v1/models/lora/activate`
- CSI recording: `GET /api/v1/recording/list`, `POST /api/v1/recording/start`, `POST /api/v1/recording/stop`, `DELETE /api/v1/recording/:id`
- Training control: `GET /api/v1/train/status`, `POST /api/v1/train/start`, `POST /api/v1/train/stop`
- Recording writes CSI frames to `.jsonl` files via tokio background task
- Model/recording directories scanned at startup, state managed via `Arc<RwLock<AppStateInner>>`
- **25 real mobile tests** replacing `it.todo()` placeholders — 205 assertions covering components, services, stores, hooks, screens, and utils
- **Project MERIDIAN (ADR-027)** — Cross-environment domain generalization for WiFi pose estimation (1,858 lines, 72 tests)
- `HardwareNormalizer` — Catmull-Rom cubic interpolation resamples any hardware CSI to canonical 56 subcarriers; z-score + phase sanitization
- `DomainFactorizer` + `GradientReversalLayer` — adversarial disentanglement of pose-relevant vs environment-specific features
@@ -23,6 +30,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- ADR-025: macOS CoreWLAN WiFi Sensing (ORCA)
### Fixed
- **WebSocket "RECONNECTING" on Dashboard/Live Demo** — `sensingService.start()` now called on app init in `app.js` so WebSocket connects immediately instead of waiting for Sensing tab visit
- **Mobile WebSocket port** — `ws.service.ts` `buildWsUrl()` uses same-origin port instead of hardcoded port 3001
- **Mobile Jest config** — `testPathIgnorePatterns` no longer silently ignores the entire test directory
- Removed synthetic byte counters from Python `MacosWifiCollector` — now reports `tx_bytes=0, rx_bytes=0` instead of fake incrementing values
---
+2 -2
View File
@@ -57,7 +57,7 @@ All 5 ruvector crates integrated in workspace:
- `ruvector-attention``model.rs` (apply_spatial_attention) + `bvp.rs`
### Architecture Decisions
32 ADRs in `docs/adr/` (ADR-001 through ADR-032). Key ones:
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)
@@ -173,7 +173,7 @@ Active feature branch: `ruvsense-full-implementation` (PR #77)
## File Organization
- NEVER save to root folder — use the directories below
- `docs/adr/` — Architecture Decision Records (32 ADRs)
- `docs/adr/` — Architecture Decision Records (43 ADRs)
- `docs/ddd/` — Domain-Driven Design models
- `rust-port/wifi-densepose-rs/crates/` — Rust workspace crates (15 crates)
- `rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/` — RuvSense multistatic modules (14 files)
+1 -1
View File
@@ -50,7 +50,7 @@ docker run -p 3000:3000 ruvnet/wifi-densepose:latest
| [User Guide](docs/user-guide.md) | Step-by-step guide: installation, first run, API usage, hardware setup, training |
| [WiFi-Mat User Guide](docs/wifi-mat-user-guide.md) | Disaster response module: search & rescue, START triage |
| [Build Guide](docs/build-guide.md) | Building from source (Rust and Python) |
| [Architecture Decisions](docs/adr/) | 41 ADRs covering signal processing, training, hardware, security, domain generalization, multistatic sensing, CRV signal-line integration, edge intelligence |
| [Architecture Decisions](docs/adr/) | 43 ADRs covering signal processing, training, hardware, security, domain generalization, multistatic sensing, CRV signal-line integration, edge intelligence, WASM edge modules, UI API completion |
| [DDD Domain Model](docs/ddd/ruvsense-domain-model.md) | RuvSense bounded contexts, aggregates, domain events, and ubiquitous language |
---
+18 -2
View File
@@ -301,6 +301,20 @@ Base URL: `http://localhost:3000` (Docker) or `http://localhost:8080` (binary de
| `GET` | `/api/v1/model/layers` | Progressive model loading status | Layer A/B/C load state |
| `GET` | `/api/v1/model/sona/profiles` | SONA adaptation profiles | List of environment profiles |
| `POST` | `/api/v1/model/sona/activate` | Activate a SONA profile for a specific room | `{"profile":"kitchen"}` |
| `GET` | `/api/v1/models` | List available RVF model files | `{"models":[...],"count":0}` |
| `GET` | `/api/v1/models/active` | Currently loaded model (or null) | `{"model":null}` |
| `POST` | `/api/v1/models/load` | Load a model by ID | `{"status":"loaded","model_id":"..."}` |
| `POST` | `/api/v1/models/unload` | Unload the active model | `{"status":"unloaded"}` |
| `DELETE` | `/api/v1/models/:id` | Delete a model file from disk | `{"status":"deleted"}` |
| `GET` | `/api/v1/models/lora/profiles` | List LoRA adapter profiles | `{"profiles":[]}` |
| `POST` | `/api/v1/models/lora/activate` | Activate a LoRA profile | `{"status":"activated"}` |
| `GET` | `/api/v1/recording/list` | List CSI recording sessions | `{"recordings":[...],"count":0}` |
| `POST` | `/api/v1/recording/start` | Start recording CSI frames to JSONL | `{"status":"recording","session_id":"..."}` |
| `POST` | `/api/v1/recording/stop` | Stop the active recording | `{"status":"stopped","duration_secs":...}` |
| `DELETE` | `/api/v1/recording/:id` | Delete a recording file | `{"status":"deleted"}` |
| `GET` | `/api/v1/train/status` | Training run status | `{"phase":"idle"}` |
| `POST` | `/api/v1/train/start` | Start a training run | `{"status":"started"}` |
| `POST` | `/api/v1/train/stop` | Stop the active training run | `{"status":"stopped"}` |
### Example: Get Vital Signs
@@ -347,7 +361,9 @@ curl -s http://localhost:3000/api/v1/pose/current | python -m json.tool
Real-time sensing data is available via WebSocket.
**URL:** `ws://localhost:3001/ws/sensing` (Docker) or `ws://localhost:8765/ws/sensing` (binary default).
**URL:** `ws://localhost:3000/ws/sensing` (same port as HTTP — recommended) or `ws://localhost:3001/ws/sensing` (dedicated WS port).
> **Note:** The `/ws/sensing` WebSocket endpoint is available on both the HTTP port (3000) and the dedicated WebSocket port (3001/8765). The web UI uses the HTTP port so only one port needs to be exposed. The dedicated WS port remains available for backward compatibility.
### Python Example
@@ -812,7 +828,7 @@ The Rust implementation (v2) is 810x faster than Python (v1) for the full CSI pi
## Further Reading
- [Architecture Decision Records](../docs/adr/) - 33 ADRs covering all design decisions
- [Architecture Decision Records](../docs/adr/) - 43 ADRs covering all design decisions
- [WiFi-Mat Disaster Response Guide](wifi-mat-user-guide.md) - Search & rescue module
- [Build Guide](build-guide.md) - Detailed build instructions
- [RuVector](https://github.com/ruvnet/ruvector) - Signal intelligence crate ecosystem
+72 -17
View File
@@ -30,22 +30,40 @@ NVS_PARTITION_OFFSET = 0x9000
NVS_PARTITION_SIZE = 0x6000 # 24 KiB
def build_nvs_csv(ssid, password, target_ip, target_port, node_id):
def build_nvs_csv(args):
"""Build an NVS CSV string for the csi_cfg namespace."""
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(["key", "type", "encoding", "value"])
writer.writerow(["csi_cfg", "namespace", "", ""])
if ssid:
writer.writerow(["ssid", "data", "string", ssid])
if password is not None:
writer.writerow(["password", "data", "string", password])
if target_ip:
writer.writerow(["target_ip", "data", "string", target_ip])
if target_port is not None:
writer.writerow(["target_port", "data", "u16", str(target_port)])
if node_id is not None:
writer.writerow(["node_id", "data", "u8", str(node_id)])
if args.ssid:
writer.writerow(["ssid", "data", "string", args.ssid])
if args.password is not None:
writer.writerow(["password", "data", "string", args.password])
if args.target_ip:
writer.writerow(["target_ip", "data", "string", args.target_ip])
if args.target_port is not None:
writer.writerow(["target_port", "data", "u16", str(args.target_port)])
if args.node_id is not None:
writer.writerow(["node_id", "data", "u8", str(args.node_id)])
# TDM mesh settings
if args.tdm_slot is not None:
writer.writerow(["tdm_slot", "data", "u8", str(args.tdm_slot)])
if args.tdm_total is not None:
writer.writerow(["tdm_nodes", "data", "u8", str(args.tdm_total)])
# Edge intelligence settings (ADR-039)
if args.edge_tier is not None:
writer.writerow(["edge_tier", "data", "u8", str(args.edge_tier)])
if args.pres_thresh is not None:
writer.writerow(["pres_thresh", "data", "u16", str(args.pres_thresh)])
if args.fall_thresh is not None:
writer.writerow(["fall_thresh", "data", "u16", str(args.fall_thresh)])
if args.vital_win is not None:
writer.writerow(["vital_win", "data", "u16", str(args.vital_win)])
if args.vital_int is not None:
writer.writerow(["vital_int", "data", "u16", str(args.vital_int)])
if args.subk_count is not None:
writer.writerow(["subk_count", "data", "u8", str(args.subk_count)])
return buf.getvalue()
@@ -127,14 +145,37 @@ def main():
parser.add_argument("--target-ip", help="Aggregator host IP (e.g. 192.168.1.20)")
parser.add_argument("--target-port", type=int, help="Aggregator UDP port (default: 5005)")
parser.add_argument("--node-id", type=int, help="Node ID 0-255 (default: 1)")
# TDM mesh settings
parser.add_argument("--tdm-slot", type=int, help="TDM slot index for this node (0-based)")
parser.add_argument("--tdm-total", type=int, help="Total number of TDM nodes in mesh")
# Edge intelligence settings (ADR-039)
parser.add_argument("--edge-tier", type=int, choices=[0, 1, 2],
help="Edge processing tier: 0=off, 1=stats, 2=vitals")
parser.add_argument("--pres-thresh", type=int, help="Presence detection threshold (default: 50)")
parser.add_argument("--fall-thresh", type=int, help="Fall detection threshold (default: 500)")
parser.add_argument("--vital-win", type=int, help="Phase history window in frames (default: 300)")
parser.add_argument("--vital-int", type=int, help="Vitals packet interval in ms (default: 1000)")
parser.add_argument("--subk-count", type=int, help="Top-K subcarrier count (default: 32)")
parser.add_argument("--dry-run", action="store_true", help="Generate NVS binary but don't flash")
args = parser.parse_args()
if not any([args.ssid, args.password is not None, args.target_ip,
args.target_port, args.node_id is not None]):
parser.error("At least one config value must be specified "
"(--ssid, --password, --target-ip, --target-port, --node-id)")
has_value = any([
args.ssid, args.password is not None, args.target_ip,
args.target_port, args.node_id is not None,
args.tdm_slot is not None, args.tdm_total is not None,
args.edge_tier is not None, args.pres_thresh is not None,
args.fall_thresh is not None, args.vital_win is not None,
args.vital_int is not None, args.subk_count is not None,
])
if not has_value:
parser.error("At least one config value must be specified")
# Validate TDM: if one is given, both should be
if (args.tdm_slot is not None) != (args.tdm_total is not None):
parser.error("--tdm-slot and --tdm-total must be specified together")
if args.tdm_slot is not None and args.tdm_slot >= args.tdm_total:
parser.error(f"--tdm-slot ({args.tdm_slot}) must be less than --tdm-total ({args.tdm_total})")
print("Building NVS configuration:")
if args.ssid:
@@ -147,9 +188,23 @@ def main():
print(f" Target Port: {args.target_port}")
if args.node_id is not None:
print(f" Node ID: {args.node_id}")
if args.tdm_slot is not None:
print(f" TDM Slot: {args.tdm_slot} of {args.tdm_total}")
if args.edge_tier is not None:
tier_desc = {0: "off (raw CSI)", 1: "stats", 2: "vitals"}
print(f" Edge Tier: {args.edge_tier} ({tier_desc.get(args.edge_tier, '?')})")
if args.pres_thresh is not None:
print(f" Pres Thresh: {args.pres_thresh}")
if args.fall_thresh is not None:
print(f" Fall Thresh: {args.fall_thresh}")
if args.vital_win is not None:
print(f" Vital Window: {args.vital_win} frames")
if args.vital_int is not None:
print(f" Vital Interval:{args.vital_int} ms")
if args.subk_count is not None:
print(f" Top-K Subcarr: {args.subk_count}")
csv_content = build_nvs_csv(args.ssid, args.password, args.target_ip,
args.target_port, args.node_id)
csv_content = build_nvs_csv(args)
try:
nvs_bin = generate_nvs_binary(csv_content, NVS_PARTITION_SIZE)