Skip to content

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

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

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

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

Additionally, the adaptive classifier has hardcoded classes (absent, present_still, present_moving, active), making it impossible for users to add custom activity classes without modifying source code.

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 like variance and motion_band_power)
  • build_per_node_features() — sorted per-node feature list for WebSocket
  • nodes_endpoint()GET /api/v1/nodes returns per-node health
  • 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)
  • 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

Test plan

  • cargo build -p wifi-densepose-sensing-server compiles clean
  • curl /api/v1/nodes returns per-node data for each connected ESP32
  • Sensing UI shows correct node count and per-node status cards
  • RSSI values are negative (-30 to -90 dBm range)
  • Unplug a node → shows "stale" after 5s, disappears after 30s
  • Walk near one node → that node's variance spikes while others stay low
  • Existing WebSocket features field still populated (backward compat)
  • Train with custom class: train_cooking_kitchen.jsonl → model learns "cooking" class
  • Old 4-class model JSON loads without errors

🤖 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>
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