Skip to content

feat(sensing-server): per-node CSI separation + dynamic classifier classes#289

Open
taylorjdawson wants to merge 1 commit intoruvnet:mainfrom
taylorjdawson:feat/per-node-csi-upstream
Open

feat(sensing-server): per-node CSI separation + dynamic classifier classes#289
taylorjdawson wants to merge 1 commit intoruvnet:mainfrom
taylorjdawson:feat/per-node-csi-upstream

Conversation

@taylorjdawson
Copy link
Copy Markdown

@taylorjdawson taylorjdawson commented Mar 22, 2026

Summary

  • Track each ESP32 node independently instead of merging all CSI frames into a single buffer
  • Make adaptive classifier classes dynamic — users add classes via filename convention, no code changes needed
  • Add per-node status UI with colored markers and signal features per node
  • Fix RSSI sign bug and XSS vulnerability in sensing UI

Motivation

The sensing server merges CSI frames from all ESP32 nodes into one frame_history buffer, discarding node_id after parsing. This means:

  • Temporal features (variance, motion) compare frames from different physical nodes
  • No spatial information — can't tell which node is seeing activity
  • UI shows "1 ESP32" despite multiple nodes connected
  • Classification accuracy is degraded by mixed-node data

Addresses #237 (multi-node display identical for all states), #276 (only one detected), #51 (amplitude detection fragile).

Implements server-side per-node tracking from the ADR-029 (RuvSense multistatic sensing) architecture.

Changes

Per-node CSI separation (sensing-server/src/main.rs)

  • NodeState struct — per-node frame_history, RSSI history, features, classification, smoothing state
  • smooth_and_classify_node() — per-node motion classification with EMA/debounce
  • compute_fused_features() — weighted aggregation across active nodes; max-boosted for presence-sensitive features (variance, motion_band_power) so single-node strong signals aren't diluted
  • build_per_node_features() — sorted per-node feature list for WebSocket broadcast
  • nodes_endpoint() — new GET /api/v1/nodes endpoint returns per-node health, frame rate, features, classification
  • RSSI sign fix — saturating_neg() for correct negative dBm values
  • Signal field uses fused features instead of single-node
  • Node timeout — stale after 5s, removed after 30s
  • SensingUpdate.node_features — optional field, backward compatible via skip_serializing_if
  • Default impls for FeatureInfo and ClassificationInfo

Dynamic classifier classes (adaptive_classifier.rs)

  • Removed hardcoded CLASSES array and N_CLASSES constant
  • classify_recording_name returns Option<String> — discovers classes from filenames
  • Convention: train_<class>_<description>.jsonl (e.g., train_cooking_kitchen.jsonl)
  • Common patterns still recognized for backward compat: *absent*, *still*, *walking*, *active*
  • Unknown patterns extract class from filename structure as fallback
  • AdaptiveModel.class_names: Vec<String> — dynamic, serialized in model JSON
  • AdaptiveModel.weights: Vec<Vec<f64>> — dynamic class count instead of fixed array
  • Backward compatible: old 4-class models load via #[serde(default)]

UI changes

  • Dynamic node count (was hardcoded "1 ESP32")
  • Per-node status cards with RSSI, variance, classification (DOM createElement, no innerHTML — XSS safe)
  • Color-coded node markers in 3D gaussian splat view (8-color palette)
  • Per-node RSSI history tracking in sensing service

Backward Compatibility

  • SensingUpdate.features still populated with fused aggregate — existing consumers unchanged
  • SensingUpdate.nodes now contains ALL active nodes (was single node per message) — existing code reading nodes[0] still works
  • node_features field is Option with skip_serializing_if — old clients don't receive it
  • Old 4-class adaptive models load correctly via serde defaults
  • Global frame_history still maintained alongside per-node histories
  • Supports any number of nodes (1 to 256) — single-node deployments work identically to before

How to test

# Build
cargo build -p wifi-densepose-sensing-server

# Run with ESP32 nodes
cargo run -p wifi-densepose-sensing-server -- --http-port 3000 --source esp32

# Verify per-node data
curl http://localhost:3000/api/v1/nodes

# Verify backward compat (fused features still present)
curl http://localhost:3000/api/v1/sensing/latest | jq '.features'

# Open UI — should show per-node cards and colored markers
open http://localhost:3000/ui/index.html

# Test dynamic classes — add any class by filename
echo '{"features":{...}}' > data/recordings/train_cooking_kitchen.jsonl
curl -X POST http://localhost:3000/api/v1/adaptive/train
# → model now includes "cooking" class

🤖 Generated with Claude Code

…asses

Track each ESP32 node independently instead of merging all CSI frames
into a single buffer. This enables per-node feature computation,
spatial awareness, and proper multi-node visualization.

Per-node CSI separation:
- Add NodeState struct with per-node frame_history, RSSI history,
  features, classification, and smoothing state
- Compute features per-node using each node's own temporal history
- Add compute_fused_features() for backward-compatible aggregate
- Add smooth_and_classify_node() for per-node motion classification
- Add GET /api/v1/nodes endpoint for per-node health/status
- Add PerNodeFeatureInfo to WebSocket SensingUpdate messages
- Fix RSSI sign (use saturating_neg for correct negative dBm values)
- Node timeout: stale after 5s, removed after 30s

Dynamic classifier classes:
- Remove hardcoded CLASSES array and N_CLASSES constant
- Discover classes automatically from training data filenames
- Convention: train_<class>_<description>.jsonl
- Users can add any class by recording with appropriate filename
- Backward compatible with existing 4-class models via serde default
- AdaptiveModel now stores class_names as Vec<String>

UI changes:
- Dynamic node count display (was hardcoded "1 ESP32")
- Per-node status cards showing RSSI, variance, classification
- Color-coded node markers in 3D gaussian splat view
- Per-node RSSI history tracking in sensing service
- XSS-safe DOM element creation (no innerHTML with server data)

Addresses ruvnet#237, ruvnet#276, ruvnet#51

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@taylorjdawson taylorjdawson force-pushed the feat/per-node-csi-upstream branch from 0e74711 to 11a413d Compare March 25, 2026 22:37
@taylorjdawson taylorjdawson marked this pull request as ready for review March 26, 2026 02:43
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.

1 participant