mirror of
https://github.com/ruvnet/RuView
synced 2026-07-06 14:43:17 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a518723d5 | |||
| 86f08303e6 | |||
| d4fb7d30d3 |
@@ -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
|
||||
|
||||
---
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user