Files
ruvnet--ruview/docs/adr/ADR-068-per-node-state-pipeline.md
wehub-resource-sync 9740bc64c9
Firmware QEMU Tests (ADR-061) / QEMU Test (edge-tier1) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (full-adr060) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (tdm-3node) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / Swarm Test (ADR-062) (push) Has been skipped
npm packages / tools/ruview-mcp (node 22) (push) Failing after 1s
nvsim-server → ghcr.io / build-and-publish (push) Failing after 1s
ruview-swarm CI guard / tests (full+train) (push) Failing after 2s
Bench Regression Guard / bench compile-verify (--no-run) (push) Failing after 0s
Bench Regression Guard / bench fast-run (informational, non-gating) (push) Has been skipped
Firmware CI / Verify version.txt matches release tag (push) Has been skipped
Dashboard a11y + cross-browser / a11y (push) Failing after 0s
nvsim Dashboard → GitHub Pages / build-and-deploy (push) Failing after 2s
Firmware CI / Build firmware (esp32s3 / 4mb) (push) Failing after 15s
Firmware QEMU Tests (ADR-061) / Build Espressif QEMU (push) Failing after 1s
Firmware QEMU Tests (ADR-061) / Fuzz Testing (ADR-061 Layer 6) (push) Failing after 1s
Continuous Deployment / Pre-deployment Checks (push) Has been skipped
Firmware CI / Build firmware (esp32c6 / c6-4mb) (push) Failing after 15s
Firmware CI / Build firmware (esp32s3 / 8mb) (push) Failing after 15s
Firmware QEMU Tests (ADR-061) / QEMU Test (boundary-max) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (boundary-min) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (default) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (edge-tier0) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / NVS Matrix Generation (push) Failing after 1s
Security Scanning / Security Policy Compliance (push) Failing after 0s
Security Scanning / Dependency Vulnerability Scan (push) Failing after 0s
Security Scanning / Static Application Security Testing (push) Failing after 1s
Security Scanning / Infrastructure Security Scan (push) Failing after 1s
Security Scanning / Secret Scanning (push) Failing after 1s
npm packages / harness/ruview (node 22) (push) Failing after 17s
Security Scanning / License Compliance Scan (push) Failing after 1s
Security Scanning / Container Security Scan (push) Failing after 4s
three.js demos → GitHub Pages / build-and-deploy (push) Failing after 1s
Verify Pipeline Determinism / Verify Pipeline Determinism (3.11) (push) Failing after 1s
Fix-Marker Regression Guard / Verify fix markers (push) Failing after 1s
ADR-115 MQTT integration tests / mqtt-integration (push) Failing after 1s
npm packages / harness/ruview (node 20) (push) Failing after 1s
npm packages / tools/ruview-mcp (node 20) (push) Failing after 1s
npm packages / tools/ruview-cli (node 20) (push) Failing after 1s
npm packages / tools/ruview-cli (node 22) (push) Failing after 1s
BFLD MQTT Integration / cargo test --features mqtt (live mosquitto) (push) Failing after 29s
ruview-swarm CI guard / build train_marl bin (push) Failing after 2s
ruview-swarm CI guard / clippy (-D warnings, --no-deps) (push) Failing after 3s
ruview-swarm CI guard / tests (ruflo) (push) Failing after 1s
ruview-swarm CI guard / tests (train) (push) Failing after 2s
ruview-swarm CI guard / tests (default) (push) Failing after 2s
Point Cloud Viewer → GitHub Pages / build-and-deploy (push) Failing after 8s
ruview-swarm CI guard / ITAR / publish guard (push) Failing after 0s
wifi-densepose sensing-server → Docker Hub + ghcr.io / build · push · smoke-test (push) Failing after 1s
Continuous Deployment / Deploy to Production (push) Has been cancelled
Continuous Deployment / Rollback Deployment (push) Has been cancelled
Continuous Deployment / Post-deployment Monitoring (push) Has been cancelled
Continuous Deployment / Notify Deployment Status (push) Has been cancelled
Continuous Deployment / Deploy to Staging (push) Has been cancelled
Security Scanning / Security Report (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 11:59:54 +08:00

8.4 KiB

ADR-068: Per-Node State Pipeline for Multi-Node Sensing

Field Value
Status Accepted
Date 2026-03-27
Authors rUv, claude-flow
Drivers #249, #237, #276, #282
Supersedes

Context

The sensing server (wifi-densepose-sensing-server) was originally designed for single-node operation. When multiple ESP32 nodes send CSI frames simultaneously, all data is mixed into a single shared pipeline:

  • One frame_history VecDeque for all nodes
  • One smoothed_person_score / smoothed_motion / vital sign buffers
  • One baseline and debounce state

This means the classification, person count, and vital signs reported to the UI are an uncontrolled aggregate of all nodes' data. The result: the detection window shows identical output regardless of how many nodes are deployed, where people stand, or how many people are in the room (#249 — 24 comments, the most reported issue).

Root Cause Verified

Investigation of AppStateInner (main.rs lines 279-367) confirmed:

Shared field Impact
frame_history Temporal analysis mixes all nodes' CSI data
smoothed_person_score Person count aggregates all nodes
smoothed_motion Motion classification undifferentiated
smoothed_hr / br Vital signs are global, not per-node
baseline_motion Adaptive baseline learned from mixed data
debounce_counter All nodes share debounce state

Decision

Introduce per-node state tracking via a HashMap<u8, NodeState> in AppStateInner. Each ESP32 node (identified by its node_id byte) gets an independent sensing pipeline with its own temporal history, smoothing buffers, baseline, and classification state.

Architecture

                     ┌─────────────────────────────────────────┐
   UDP frames        │           AppStateInner                  │
   ───────────►      │                                         │
   node_id=1    ──►  │  node_states: HashMap<u8, NodeState>    │
   node_id=2    ──►  │    ├── 1: NodeState { frame_history,    │
   node_id=3    ──►  │    │      smoothed_motion, vitals, ... }│
                     │    ├── 2: NodeState { ... }              │
                     │    └── 3: NodeState { ... }              │
                     │                                         │
                     │  ┌── Per-Node Pipeline ──┐               │
                     │  │ extract_features()     │               │
                     │  │ smooth_and_classify()  │               │
                     │  │ smooth_vitals()        │               │
                     │  │ score_to_person_count()│               │
                     │  └────────────────────────┘               │
                     │                                         │
                     │  ┌── Multi-Node Fusion ──┐               │
                     │  │ Aggregate person count │               │
                     │  │ Per-node classification│               │
                     │  │ All-nodes WebSocket msg│               │
                     │  └────────────────────────┘               │
                     │                                         │
                     │  ──► WebSocket broadcast (sensing_update) │
                     └─────────────────────────────────────────┘

NodeState Struct

struct NodeState {
    frame_history: VecDeque<Vec<f64>>,
    smoothed_person_score: f64,
    prev_person_count: usize,
    smoothed_motion: f64,
    current_motion_level: String,
    debounce_counter: u32,
    debounce_candidate: String,
    baseline_motion: f64,
    baseline_frames: u64,
    smoothed_hr: f64,
    smoothed_br: f64,
    smoothed_hr_conf: f64,
    smoothed_br_conf: f64,
    hr_buffer: VecDeque<f64>,
    br_buffer: VecDeque<f64>,
    rssi_history: VecDeque<f64>,
    vital_detector: VitalSignDetector,
    latest_vitals: VitalSigns,
    last_frame_time: Option<std::time::Instant>,
    edge_vitals: Option<Esp32VitalsPacket>,
}

Multi-Node Aggregation

  • Person count: Sum of per-node prev_person_count for active nodes (seen within last 10 seconds).
  • Classification: Per-node classification included in SensingUpdate.nodes.
  • Vital signs: Per-node vital signs; UI can render per-node or aggregate.
  • Signal field: Generated from the most-recently-updated node's features.
  • Stale nodes: Nodes with no frame for >10 seconds are excluded from aggregation and marked offline (consistent with PR #300).

Backward Compatibility

  • The simulated data path (simulated_data_task) continues using global state.
  • Single-node deployments behave identically (HashMap has one entry).
  • The WebSocket message format (sensing_update) remains the same but the nodes array now contains all active nodes, and estimated_persons reflects the cross-node aggregate.
  • The edge vitals path (#323 fix) also uses per-node state.

Scaling Characteristics

Nodes Per-Node Memory Total Overhead Notes
1 ~50 KB ~50 KB Identical to current
3 ~50 KB ~150 KB Typical home setup
10 ~50 KB ~500 KB Small office
50 ~50 KB ~2.5 MB Building floor
100 ~50 KB ~5 MB Large deployment
256 ~50 KB ~12.8 MB Max (u8 node_id)

Memory is dominated by frame_history (100 frames x ~500 bytes each = ~50 KB per node). This scales linearly and fits comfortably in server memory even at 256 nodes.

QEMU Validation

The existing QEMU swarm infrastructure (ADR-062, scripts/qemu_swarm.py) supports multi-node simulation with configurable topologies:

  • star: Central coordinator + sensor nodes
  • mesh: Fully connected peer network
  • line: Sequential chain
  • ring: Circular topology

Each QEMU instance runs with a unique node_id via NVS provisioning. The swarm health validator (scripts/swarm_health.py) checks per-node UART output.

Validation plan:

  1. QEMU swarm with 3-5 nodes in mesh topology
  2. Verify server produces distinct per-node classifications
  3. Verify aggregate person count reflects multi-node contributions
  4. Verify stale-node eviction after timeout

Consequences

Positive

  • Each node's CSI data is processed independently — no cross-contamination
  • Person count scales with the number of deployed nodes
  • Vital signs are per-node, enabling room-level health monitoring
  • Foundation for spatial localization (per-node positions + triangulation)
  • Scales to 256 nodes with <13 MB memory overhead

Negative

  • Slightly more memory per node (~50 KB each)
  • smooth_and_classify_node function duplicates some logic from global version
  • Per-node VitalSignDetector instances add CPU cost proportional to node count

Risks

  • Node ID collisions (mitigated by NVS persistence since v0.5.0)
  • HashMap growth without cleanup (mitigated by stale-node eviction)
  • ADR-069 (ESP32 CSI → Cognitum Seed RVF Ingest Pipeline) extends this ADR's per-node state architecture with Cognitum Seed integration. Live hardware validation (2026-04-02) confirmed per-node feature vectors flowing through the bridge into the Seed's RVF store with witness chain attestation.

References

  • Issue #249: Detection window same regardless (24 comments)
  • Issue #237: Same display for 0/1/2 people (12 comments)
  • Issue #276: Only one can be detected (8 comments)
  • Issue #282: Detection fail (5 comments)
  • PR #295: Hysteresis smoothing (partial mitigation)
  • PR #300: ESP32 offline detection after 5s
  • ADR-062: QEMU Swarm Configurator