docs/adr/ADR-048-adaptive-csi-classifier.md
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-03-05 |
| Deciders | ruv |
| Depends on | ADR-024 (AETHER Embeddings), ADR-039 (Edge Processing), ADR-045 (AMOLED Display) |
WiFi-based activity classification using ESP32 Channel State Information (CSI) relies on hand-tuned thresholds to distinguish between activity states (absent, present_still, present_moving, active). These static thresholds are brittle — they don't account for:
The existing threshold-based approach produces noisy, unstable classifications that degrade the user experience in the Observatory visualization and the main dashboard.
All CSI-derived metrics pass through a three-stage pipeline before reaching the UI:
adaptive_classifier.rs)A Rust-native environment-tuned classifier that learns from labeled JSONL recordings:
| # | Feature | Source | Discriminative Power |
|---|---|---|---|
| 0 | variance | Server | Medium — temporal CSI spread |
| 1 | motion_band_power | Server | Medium — high-frequency subcarrier energy |
| 2 | breathing_band_power | Server | Low — respiratory band energy |
| 3 | spectral_power | Server | Low — mean squared amplitude |
| 4 | dominant_freq_hz | Server | Low — peak subcarrier index |
| 5 | change_points | Server | Medium — threshold crossing count |
| 6 | mean_rssi | Server | Low — received signal strength |
| 7 | amp_mean | Subcarrier | Medium — mean amplitude across 56 subcarriers |
| 8 | amp_std | Subcarrier | High — amplitude spread (motion increases spread) |
| 9 | amp_skew | Subcarrier | Medium — asymmetry of amplitude distribution |
| 10 | amp_kurt | Subcarrier | High — peakedness (presence creates peaks) |
| 11 | amp_iqr | Subcarrier | Medium — inter-quartile range |
| 12 | amp_entropy | Subcarrier | High — spectral entropy (motion increases disorder) |
| 13 | amp_max | Subcarrier | Medium — peak amplitude value |
| 14 | amp_range | Subcarrier | Medium — amplitude dynamic range |
POST /api/v1/recording/start {"id":"train_<label>"}*empty*→absent, *still*→present_still, *walking*→present_moving, *active*→activePOST /api/v1/adaptive/traindata/adaptive_model.json, auto-loaded on server restart| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/adaptive/train | Train classifier from train_* recordings |
| GET | /api/v1/adaptive/status | Check model status, accuracy, class stats |
| POST | /api/v1/adaptive/unload | Revert to threshold-based classification |
| POST | /api/v1/recording/start | Start recording CSI frames (JSONL) |
| POST | /api/v1/recording/stop | Stop recording |
| GET | /api/v1/recording/list | List available recordings |
| Parameter | Value | Rationale |
|---|---|---|
| Median window | 21 frames | ~2s of history, robust to transients |
| Aggregation | Trimmed mean (middle 50%) | More stable than pure median, less noisy than raw mean |
| EMA alpha | 0.02 | ~5s time constant — readings change very slowly |
| HR dead-band | ±2 BPM | Prevents display creep from micro-fluctuations |
| BR dead-band | ±0.5 BPM | Same for breathing rate |
| HR max jump | 8 BPM/frame | Outlier rejection threshold |
| BR max jump | 2 BPM/frame | Outlier rejection threshold |
| File | Purpose |
|---|---|
crates/wifi-densepose-sensing-server/src/adaptive_classifier.rs | Adaptive classifier module (feature extraction, training, inference) |
crates/wifi-densepose-sensing-server/src/main.rs | Smoothing pipeline, API endpoints, integration |
ui/observatory/js/hud-controller.js | UI-side lerp smoothing (4% per frame) |
data/adaptive_model.json | Trained model (auto-created by training endpoint) |
data/recordings/train_*.jsonl | Labeled training recordings |