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) <noreply@anthropic.com>
@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

1 participant