Skip to content

feat(server): accuracy sprint 001 — Kalman tracker, multi-node fusion, eigenvalue counting#20

Open
taylorjdawson wants to merge 12 commits intomainfrom
feat/accuracy-sprint-001
Open

feat(server): accuracy sprint 001 — Kalman tracker, multi-node fusion, eigenvalue counting#20
taylorjdawson wants to merge 12 commits intomainfrom
feat/accuracy-sprint-001

Conversation

@taylorjdawson
Copy link
Copy Markdown
Owner

Summary

Wire three existing signal-crate components into the live sensing path to replace heuristic person counting with physics-grounded approaches:

  • Kalman Tracker (tracker_bridge.rs): Stable person IDs via PoseTracker with greedy Mahalanobis assignment, proper lifecycle transitions (Tentative→Active→Lost→Terminated), and Kalman-smoothed keypoints — eliminates frame-to-frame jitter and ephemeral 0-based IDs
  • Multi-Node Fusion (multistatic_bridge.rs): MultistaticFuser replaces naive .sum() of per-node person counts with attention-weighted CSI fusion — two nodes seeing one person now reports 1 (not 2). Fallback uses max to avoid double-counting overlapping coverage
  • Eigenvalue Person Counting (field_model.rs upgrade): Full covariance matrix + ndarray-linalg eigendecomposition with Marcenko-Pastur noise threshold replaces diagonal variance approximation. estimate_occupancy() for runtime counting, calibration API (/calibration/start|stop|status)

Upstream ADR Alignment

Step ADR What it operationalizes
Kalman Tracker ADR-029 § 2.7 Existing PoseTracker wired into live sensing path
Multi-Node Fusion ADR-029 § 2.4, ADR-031 Existing MultistaticFuser replaces naive sum
Eigenvalue Counting ADR-037 Phase 1, ADR-030 § 2.3 Full SVD replaces diagonal variance approximation

Also submitted upstream as ruvnet#341.

Changes

New files

File Lines Purpose
sensing-server/src/tracker_bridge.rs 397 f64/f32 bridge, greedy assignment, COCO-17 mapping
sensing-server/src/multistatic_bridge.rs 263 NodeState→MultiBandCsiFrame conversion, fusion fallback
sensing-server/src/field_bridge.rs 143 Occupancy fallback chain, calibration feeding, position parsing

Modified files

File +/- Changes
sensing-server/Cargo.toml +3 Add wifi-densepose-signal dependency
sensing-server/src/main.rs +279/-81 AppStateInner fields, 5 tracker call sites, 2 fusion sites, 4 eigenvalue sites, calibration API, CLI args
signal/Cargo.toml +1 Add ndarray-linalg dependency
signal/src/ruvsense/field_model.rs +495/-81 Covariance accumulator, SVD finalization, estimate_occupancy(), baseline_eigenvalue_count

Test plan

  • cargo test -p wifi-densepose-signal --no-default-features — existing 49+ tests + 4 new field_model tests
  • cargo test -p wifi-densepose-sensing-server — existing server tests + new bridge tests
  • cargo test --workspace --no-default-features — full 1031+ test suite green
  • Manual: connect ESP32 node, verify stable person IDs in WebSocket JSON
  • Manual: connect 2+ nodes, verify single person counts as 1 (not 2)
  • Manual: POST /calibration/start, wait, POST /calibration/stop — verify baseline eigenvalue count

Refs: .swarm/plans/accuracy-sprint-001.md

🤖 Generated with claude-flow

ruvnet and others added 12 commits March 23, 2026 21:37
…uvnet#295)

The person-count heuristic was causing widespread flickering (ruvnet#237, ruvnet#249,
ruvnet#280, ruvnet#292) because:

1. Threshold 0.50 for 2-persons was too low — multipath reflections in
   small rooms easily exceeded it
2. No actual hysteresis despite the comment claiming asymmetric thresholds
3. EMA smoothing (α=0.15) was too responsive to transient spikes

Changes:
- Raise up-thresholds: 1→2 persons at 0.65 (was 0.50), 2→3 at 0.85 (was 0.80)
- Add true hysteresis with asymmetric down-thresholds: 2→1 at 0.45, 3→2 at 0.70
- Track prev_person_count in SensingState for state-aware transitions
- Increase EMA smoothing to α=0.10 (~2s time constant at 20 Hz)
- Update all 4 call sites (ESP32, Windows WiFi, multi-BSSID, simulated)

Fixes ruvnet#292, ruvnet#280, ruvnet#237

Co-authored-by: Reuven <cohen@ruv-mac-mini.local>
4-phase plan to upgrade core ruvector dependencies and adopt new crates:
- Phase 1: Bump 5 core crates 2.0.4→2.0.5 (10-30% mincut perf, security fixes)
- Phase 2: Add ruvector-coherence for spectral multi-node CSI coherence
- Phase 3: Add SONA adaptive learning to replace manual logistic regression
- Phase 4: Evaluate ruvector-core ONNX embeddings for CSI pattern matching

Co-Authored-By: claude-flow <ruv@ruv.net>
…net#300)

The source field was set to "esp32" on the first UDP frame but never
reverted when frames stopped arriving. This caused the UI to show
"Real hardware connected" indefinitely after powering off all nodes.

Changes:
- Add last_esp32_frame timestamp to AppStateInner
- Add effective_source() method with 5-second timeout
- Source becomes "esp32:offline" when no frames received within 5s
- Health endpoint shows "degraded" instead of "healthy" when offline
- All 6 status/health/info API endpoints use effective_source()

Fixes ruvnet#297

Co-authored-by: Reuven <cohen@ruv-mac-mini.local>
Updated project description to include Cognitum.One.
The README Quick Start tells users to `pip install wifi-densepose` and then
`from wifi_densepose import WiFiDensePose`, but no `wifi_densepose` Python
package existed — only `v1/src`. This adds a top-level `wifi_densepose/`
package with a WiFiDensePose facade class matching the documented API, and
updates pyproject.toml to include it in the distribution.

Closes ruvnet#314
…uvnet#321, ruvnet#323)

* fix(firmware,server): watchdog crash on busy LANs + no detection from edge vitals (ruvnet#321, ruvnet#323)

**Firmware (ruvnet#321):** edge_dsp task now batch-limits frame processing to 4
frames before a 10ms yield. On corporate LANs with high CSI frame rates,
the previous 1-tick-per-frame yield wasn't enough to prevent IDLE1
starvation and task watchdog triggers.

**Sensing server (ruvnet#323):** When ESP32 runs the edge DSP pipeline (Tier 2+),
it sends vitals packets (magic 0xC5110002) instead of raw CSI frames.
Previously, the server broadcast these as raw edge_vitals but never
generated a sensing_update, so the UI showed "connected" but "0 persons".
Now synthesizes a full sensing_update from vitals data including
classification, person count, and pose generation.

Closes ruvnet#321
Closes ruvnet#323

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(firmware): address review findings — idle busy-spin and observability

- Fix pdMS_TO_TICKS(5)==0 at 100Hz causing busy-spin in idle path (use
  vTaskDelay(1) instead)
- Post-batch yield now 2 ticks (20ms) for genuinely longer pause
- Add s_ring_drops counter to ring_push for diagnosing frame drops
- Expose drop count in periodic vitals log line

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(server): set breathing_band_power for skeleton animation from vitals

When presence is detected via edge vitals, set breathing_band_power to
0.5 so the UI's torso breathing animation works. Previously hardcoded
to 0.0 which made the skeleton appear static even when breathing rate
was being reported.

Co-Authored-By: claude-flow <ruv@ruv.net>
List specific known issues (multi-node detection, training plateau,
no pre-trained weights, hardware compatibility) to set expectations
for new users.

Co-Authored-By: claude-flow <ruv@ruv.net>
…vnet#249)

Documents the architectural change from single shared state to per-node
HashMap<u8, NodeState> in the sensing server. Includes scaling analysis
(256 nodes < 13 MB), QEMU validation plan, and aggregation strategy.

Also links README hero image to the explainer video.

Co-Authored-By: claude-flow <ruv@ruv.net>
)

* docs(adr): ADR-068 per-node state pipeline for multi-node sensing (ruvnet#249)

Documents the architectural change from single shared state to per-node
HashMap<u8, NodeState> in the sensing server. Includes scaling analysis
(256 nodes < 13 MB), QEMU validation plan, and aggregation strategy.

Also links README hero image to the explainer video.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(server): per-node state pipeline for multi-node sensing (ADR-068, ruvnet#249)

Replaces the single shared state pipeline with per-node HashMap<u8, NodeState>.
Each ESP32 node now gets independent:
- frame_history (temporal analysis)
- smoothed_person_score / prev_person_count
- smoothed_motion / baseline / debounce state
- vital sign detector + smoothing buffers
- RSSI history

Multi-node aggregation:
- Person count = sum of per-node counts for active nodes (seen <10s)
- SensingUpdate.nodes includes all active nodes
- estimated_persons reflects cross-node aggregate

Single-node deployments behave identically (HashMap has one entry).
Simulated data path unchanged for backward compatibility.

Closes ruvnet#249
Refs ruvnet#237, ruvnet#276, ruvnet#282

Co-Authored-By: claude-flow <ruv@ruv.net>
Complements ruvnet#326 (per-node state pipeline) with additional features:

- Dynamic adaptive classifier: discover activity classes from training
  data filenames instead of hardcoded array. Users add classes via
  filename convention (train_<class>_<desc>.jsonl), no code changes.
- Per-node UI cards: SensingTab shows individual node status with
  color-coded markers, RSSI, variance, and classification per node.
- Colored node markers in 3D gaussian splat view (8-color palette).
- Per-node RSSI history tracking in sensing service.
- XSS fix: UI uses createElement/textContent instead of innerHTML.
- RSSI sign fix: ensure dBm values are always negative.
- GET /api/v1/nodes endpoint for per-node health monitoring.
- node_features field in WebSocket SensingUpdate messages.
- Firmware watchdog fix: yield after every frame to prevent IDLE1 starvation.

Addresses ruvnet#237, ruvnet#276, ruvnet#282

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…, eigenvalue counting

Wire three existing signal-crate components into the live sensing path:

Step 1 — Kalman Tracker (tracker_bridge.rs):
- PoseTracker from wifi-densepose-signal wired into all 5 mutable
  derive_pose_from_sensing call sites
- Stable TrackId-based person IDs replace ephemeral 0-based indices
- Greedy Mahalanobis assignment with proper lifecycle transitions
  (Tentative → Active → Lost → Terminated)
- Kalman-smoothed keypoint positions reduce frame-to-frame jitter

Step 2 — Multi-Node Fusion (multistatic_bridge.rs):
- MultistaticFuser replaces naive .sum() aggregation at both ESP32 paths
- Attention-weighted CSI fusion across nodes with cosine-similarity weights
- Fallback uses max (not sum) to avoid double-counting overlapping coverage
- Node positions configurable via --node-positions CLI arg
- Single-node passthrough preserved (min_nodes=1)

Step 3 — Eigenvalue Person Counting (field_model.rs upgrade):
- Full covariance matrix accumulation (replaces diagonal variance approx)
- True eigendecomposition via ndarray-linalg Eigh (Marcenko-Pastur threshold)
- estimate_occupancy() for runtime eigenvalue-based counting
- Calibration API: POST /calibration/start|stop, GET /calibration/status
- Graceful fallback to score_to_person_count when uncalibrated

New files: tracker_bridge.rs, multistatic_bridge.rs, field_bridge.rs
Modified: sensing-server main.rs, Cargo.toml; signal field_model.rs, Cargo.toml

Refs: .swarm/plans/accuracy-sprint-001.md

Co-Authored-By: claude-flow <ruv@ruv.net>
Critical fixes:
- C1: FieldModel created with n_links=1 (single_link_config) so
  feed_calibration/extract_perturbation no longer get DimensionMismatch
- C2: variance_explained now uses centered covariance trace (E[x²]-E[x]²)
  matching mode_energies normalization
- C3: MP ratio uses total_obs = frames * links for consistent threshold
  between calibration and runtime
- C4: Noise estimator filters to positive eigenvalues only, preventing
  collapse to ~0 on rank-deficient matrices (p > n)
- C5: ESP32 paths gate total_persons on presence — empty room reports 0

High fixes:
- H1: Bounding box computed from observed keypoints only (confidence > 0),
  preventing collapse from centroid-filled unobserved slots
- H2: fuse_or_fallback returns Option<usize> instead of sentinel 0,
  eliminating type ambiguity between "fusion succeeded" and "zero people"
- H3: Monotonic epoch-relative timestamps replace wall-clock/Instant mixing,
  preventing spurious TimestampMismatch on NTP steps
- H5: ndarray-linalg gated behind "eigenvalue" feature flag (default=on),
  diagonal fallback used with --no-default-features

Moderate fixes:
- M1: calibration_start guards against replacing Fresh calibration
- M2: parse_node_positions logs warning for malformed entries

Co-Authored-By: claude-flow <ruv@ruv.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants