docs: add 4 DDD domain models covering all major subsystems
Create complete Domain-Driven Design specifications for: - Signal Processing (3 contexts: CSI Preprocessing, Feature Extraction, Motion Analysis) - Training Pipeline (4 contexts: Dataset Management, Model Architecture, Training Orchestration, Embedding & Transfer) - Hardware Platform (5 contexts: Sensor Node, Edge Processing, WASM Runtime, Aggregation, Provisioning) - Sensing Server (5 contexts: CSI Ingestion, Model Management, CSI Recording, Training Pipeline, Visualization) Update DDD index (3 → 7 models) and README docs table. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
8658cc3de0
commit
2ad510782e
|
|
@ -50,7 +50,7 @@ docker run -p 3000:3000 ruvnet/wifi-densepose:latest
|
||||||
| [User Guide](docs/user-guide.md) | Step-by-step guide: installation, first run, API usage, hardware setup, training |
|
| [User Guide](docs/user-guide.md) | Step-by-step guide: installation, first run, API usage, hardware setup, training |
|
||||||
| [Build Guide](docs/build-guide.md) | Building from source (Rust and Python) |
|
| [Build Guide](docs/build-guide.md) | Building from source (Rust and Python) |
|
||||||
| [Architecture Decisions](docs/adr/README.md) | 44 ADRs — why each technical choice was made, organized by domain (hardware, signal processing, ML, platform, infrastructure) |
|
| [Architecture Decisions](docs/adr/README.md) | 44 ADRs — why each technical choice was made, organized by domain (hardware, signal processing, ML, platform, infrastructure) |
|
||||||
| [Domain Models](docs/ddd/README.md) | 3 DDD models (RuvSense, WiFi-Mat, CHCI) — bounded contexts, aggregates, domain events, and ubiquitous language |
|
| [Domain Models](docs/ddd/README.md) | 7 DDD models (RuvSense, Signal Processing, Training Pipeline, Hardware Platform, Sensing Server, WiFi-Mat, CHCI) — bounded contexts, aggregates, domain events, and ubiquitous language |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,12 @@ DDD organizes the codebase around the problem being solved — not around techni
|
||||||
| Model | What it covers | Bounded Contexts |
|
| Model | What it covers | Bounded Contexts |
|
||||||
|-------|---------------|------------------|
|
|-------|---------------|------------------|
|
||||||
| [RuvSense](ruvsense-domain-model.md) | Multistatic WiFi sensing, pose tracking, vital signs, edge intelligence | 7 contexts: Sensing, Coherence, Tracking, Field Model, Longitudinal, Spatial Identity, Edge Intelligence |
|
| [RuvSense](ruvsense-domain-model.md) | Multistatic WiFi sensing, pose tracking, vital signs, edge intelligence | 7 contexts: Sensing, Coherence, Tracking, Field Model, Longitudinal, Spatial Identity, Edge Intelligence |
|
||||||
| [WiFi-Mat](wifi-mat-domain-model.md) | Disaster response: survivor detection, START triage, mass casualty assessment | Scan Zone, Survivor Tracking, Triage |
|
| [Signal Processing](signal-processing-domain-model.md) | SOTA signal processing: phase cleaning, feature extraction, motion analysis | 3 contexts: CSI Preprocessing, Feature Extraction, Motion Analysis |
|
||||||
| [CHCI](chci-domain-model.md) | Coherent Human Channel Imaging: sub-millimeter body surface reconstruction | Sounding, Channel Estimation, Imaging |
|
| [Training Pipeline](training-pipeline-domain-model.md) | ML training: datasets, model architecture, embeddings, domain generalization | 4 contexts: Dataset Management, Model Architecture, Training Orchestration, Embedding & Transfer |
|
||||||
|
| [Hardware Platform](hardware-platform-domain-model.md) | ESP32 firmware, edge intelligence, WASM runtime, aggregation, provisioning | 5 contexts: Sensor Node, Edge Processing, WASM Runtime, Aggregation, Provisioning |
|
||||||
|
| [Sensing Server](sensing-server-domain-model.md) | Single-binary Axum server: CSI ingestion, model management, recording, training, visualization | 5 contexts: CSI Ingestion, Model Management, CSI Recording, Training Pipeline, Visualization |
|
||||||
|
| [WiFi-Mat](wifi-mat-domain-model.md) | Disaster response: survivor detection, START triage, mass casualty assessment | 3 contexts: Detection, Localization, Alerting |
|
||||||
|
| [CHCI](chci-domain-model.md) | Coherent Human Channel Imaging: sub-millimeter body surface reconstruction | 3 contexts: Sounding, Channel Estimation, Imaging |
|
||||||
|
|
||||||
## How to read these
|
## How to read these
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,842 @@
|
||||||
|
# Sensing Server Domain Model
|
||||||
|
|
||||||
|
The Sensing Server is the single-binary deployment surface of WiFi-DensePose. It receives raw CSI frames from ESP32 nodes, processes them into sensing features, streams live data to a web UI, and provides a self-contained workflow for recording data, training models, and running inference -- all without external dependencies.
|
||||||
|
|
||||||
|
This document defines the system using [Domain-Driven Design](https://martinfowler.com/bliki/DomainDrivenDesign.html) (DDD): bounded contexts that own their data and rules, aggregate roots that enforce invariants, value objects that carry meaning, and domain events that connect everything. The server is implemented as a single Axum binary (`wifi-densepose-sensing-server`) with all state managed through `Arc<RwLock<AppStateInner>>`.
|
||||||
|
|
||||||
|
**Bounded Contexts:**
|
||||||
|
|
||||||
|
| # | Context | Responsibility | Key ADRs | Code |
|
||||||
|
|---|---------|----------------|----------|------|
|
||||||
|
| 1 | [CSI Ingestion](#1-csi-ingestion-context) | Receive, decode, and feature-extract CSI frames from ESP32 UDP | [ADR-019](../adr/ADR-019-sensing-only-ui-mode.md), [ADR-035](../adr/ADR-035-live-sensing-ui-accuracy.md) | `sensing-server/src/main.rs` |
|
||||||
|
| 2 | [Model Management](#2-model-management-context) | Load, unload, list RVF models; LoRA profile activation | [ADR-043](../adr/ADR-043-sensing-server-ui-api-completion.md) | `sensing-server/src/model_manager.rs` |
|
||||||
|
| 3 | [CSI Recording](#3-csi-recording-context) | Record CSI frames to .jsonl files, manage recording sessions | [ADR-043](../adr/ADR-043-sensing-server-ui-api-completion.md) | `sensing-server/src/recording.rs` |
|
||||||
|
| 4 | [Training Pipeline](#4-training-pipeline-context) | Background training runs, progress streaming, contrastive pretraining | [ADR-043](../adr/ADR-043-sensing-server-ui-api-completion.md) | `sensing-server/src/training_api.rs` |
|
||||||
|
| 5 | [Visualization](#5-visualization-context) | WebSocket streaming to web UI, Gaussian splat rendering, data transparency | [ADR-019](../adr/ADR-019-sensing-only-ui-mode.md), [ADR-035](../adr/ADR-035-live-sensing-ui-accuracy.md) | `ui/` |
|
||||||
|
|
||||||
|
All code paths shown are relative to `rust-port/wifi-densepose-rs/crates/wifi-densepose-` unless otherwise noted.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Domain-Driven Design Specification
|
||||||
|
|
||||||
|
### Ubiquitous Language
|
||||||
|
|
||||||
|
| Term | Definition |
|
||||||
|
|------|------------|
|
||||||
|
| **Sensing Update** | A complete JSON message broadcast to WebSocket clients each tick, containing node data, features, classification, signal field, and optional vital signs |
|
||||||
|
| **Tick** | One processing cycle of the sensing loop (default 100ms = 10 fps, configurable via `--tick-ms`) |
|
||||||
|
| **Data Source** | Origin of CSI data: `esp32` (UDP port 5005), `wifi` (Windows RSSI), `simulated` (synthetic), or `auto` (try ESP32 then fall back) |
|
||||||
|
| **RVF Model** | A `.rvf` container file holding trained weights, manifest metadata, optional LoRA adapters, and vital sign configuration |
|
||||||
|
| **LoRA Profile** | A lightweight adapter applied on top of a base RVF model for environment-specific fine-tuning without retraining the full model |
|
||||||
|
| **Recording Session** | A period during which CSI frames are appended to a `.csi.jsonl` file, identified by a session ID and optional activity label |
|
||||||
|
| **Training Run** | A background task that loads recorded CSI data, extracts features, trains a regularised linear model, and exports a `.rvf` container |
|
||||||
|
| **Frame History** | A circular buffer of the last 100 CSI amplitude vectors used for temporal analysis (sliding-window variance, Goertzel breathing estimation) |
|
||||||
|
| **Goertzel Filter** | A frequency-domain estimator applied to the frame history to detect breathing rate (0.1--0.5 Hz) via a 9-candidate filter bank |
|
||||||
|
| **Signal Field** | A 20x1x20 grid of interpolated signal intensity values rendered as Gaussian splats in the UI |
|
||||||
|
| **Pose Source** | Whether pose keypoints are `signal_derived` (analytical from CSI features) or `model_inference` (from a loaded RVF model) |
|
||||||
|
| **Progressive Loader** | A two-layer model loading strategy: Layer A loads instantly for basic inference, Layer B loads in background for full accuracy |
|
||||||
|
| **Sensing-Only Mode** | UI mode when the DensePose backend is unavailable; suppresses DensePose tabs, shows only sensing and signal visualization |
|
||||||
|
| **AppStateInner** | The single shared state struct holding all server state, accessed via `Arc<RwLock<AppStateInner>>` |
|
||||||
|
| **PCK Score** | Percentage of Correct Keypoints -- the primary accuracy metric for pose estimation models |
|
||||||
|
| **Contrastive Pretraining** | Self-supervised training on unlabeled CSI data that learns signal representations before supervised fine-tuning (ADR-024) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bounded Contexts
|
||||||
|
|
||||||
|
### 1. CSI Ingestion Context
|
||||||
|
|
||||||
|
**Responsibility:** Receive raw CSI frames from ESP32 nodes via UDP (port 5005), decode the binary protocol, extract temporal and frequency-domain features, and produce a `SensingUpdate` each tick.
|
||||||
|
|
||||||
|
```
|
||||||
|
+------------------------------------------------------------+
|
||||||
|
| CSI Ingestion Context |
|
||||||
|
+------------------------------------------------------------+
|
||||||
|
| |
|
||||||
|
| +----------------+ +----------------+ |
|
||||||
|
| | UDP Listener | | Data Source | |
|
||||||
|
| | (port 5005) | | Selector | |
|
||||||
|
| | Esp32Frame | | (auto/esp32/ | |
|
||||||
|
| | parser | | wifi/sim) | |
|
||||||
|
| +-------+--------+ +-------+--------+ |
|
||||||
|
| | | |
|
||||||
|
| +----------+----------+ |
|
||||||
|
| v |
|
||||||
|
| +-------------------+ |
|
||||||
|
| | Frame History | |
|
||||||
|
| | Buffer | |
|
||||||
|
| | (VecDeque<Vec>, | |
|
||||||
|
| | 100 frames) | |
|
||||||
|
| +--------+----------+ |
|
||||||
|
| v |
|
||||||
|
| +-------------------+ |
|
||||||
|
| | Feature | |
|
||||||
|
| | Extractor | |
|
||||||
|
| | (Welford stats, | |
|
||||||
|
| | Goertzel FFT, | |
|
||||||
|
| | L2 motion) | |
|
||||||
|
| +--------+----------+ |
|
||||||
|
| v |
|
||||||
|
| +-------------------+ |
|
||||||
|
| | Vital Sign | |
|
||||||
|
| | Detector |---> SensingUpdate |
|
||||||
|
| | (HR, RR, | |
|
||||||
|
| | breathing) | |
|
||||||
|
| +-------------------+ |
|
||||||
|
| |
|
||||||
|
+------------------------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
**Aggregates:**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Aggregate Root: The central shared state of the sensing server.
|
||||||
|
/// All mutations go through RwLock. All handler functions receive
|
||||||
|
/// State<Arc<RwLock<AppStateInner>>>.
|
||||||
|
pub struct AppStateInner {
|
||||||
|
/// Most recent sensing update broadcast to clients.
|
||||||
|
latest_update: Option<SensingUpdate>,
|
||||||
|
/// RSSI history for sparkline display.
|
||||||
|
rssi_history: VecDeque<f64>,
|
||||||
|
/// Circular buffer of recent CSI amplitude vectors (100 frames).
|
||||||
|
frame_history: VecDeque<Vec<f64>>,
|
||||||
|
/// Monotonic tick counter.
|
||||||
|
tick: u64,
|
||||||
|
/// Active data source identifier ("esp32", "wifi", "simulated").
|
||||||
|
source: String,
|
||||||
|
/// Broadcast channel for WebSocket fan-out.
|
||||||
|
tx: broadcast::Sender<String>,
|
||||||
|
/// Vital sign detector instance.
|
||||||
|
vital_detector: VitalSignDetector,
|
||||||
|
/// Most recent vital signs reading.
|
||||||
|
latest_vitals: VitalSigns,
|
||||||
|
/// Smoothed person count (EMA) for hysteresis.
|
||||||
|
smoothed_person_score: f64,
|
||||||
|
// ... model, recording, training fields (see other contexts)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Value Objects:**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// A complete sensing update broadcast to WebSocket clients each tick.
|
||||||
|
pub struct SensingUpdate {
|
||||||
|
pub msg_type: String, // always "sensing_update"
|
||||||
|
pub timestamp: f64, // Unix timestamp with ms precision
|
||||||
|
pub source: String, // "esp32" | "wifi" | "simulated"
|
||||||
|
pub tick: u64, // monotonic tick counter
|
||||||
|
pub nodes: Vec<NodeInfo>, // per-node CSI data
|
||||||
|
pub features: FeatureInfo, // extracted signal features
|
||||||
|
pub classification: ClassificationInfo,
|
||||||
|
pub signal_field: SignalField,
|
||||||
|
pub vital_signs: Option<VitalSigns>,
|
||||||
|
pub persons: Option<Vec<PersonDetection>>,
|
||||||
|
pub estimated_persons: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-node CSI data received from one ESP32.
|
||||||
|
pub struct NodeInfo {
|
||||||
|
pub node_id: u8,
|
||||||
|
pub rssi_dbm: f64,
|
||||||
|
pub position: [f64; 3],
|
||||||
|
pub amplitude: Vec<f64>,
|
||||||
|
pub subcarrier_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracted signal features from the frame history buffer.
|
||||||
|
pub struct FeatureInfo {
|
||||||
|
pub mean_rssi: f64,
|
||||||
|
pub variance: f64,
|
||||||
|
pub motion_band_power: f64,
|
||||||
|
pub breathing_band_power: f64,
|
||||||
|
pub dominant_freq_hz: f64,
|
||||||
|
pub change_points: usize,
|
||||||
|
pub spectral_power: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Motion classification derived from features.
|
||||||
|
pub struct ClassificationInfo {
|
||||||
|
pub motion_level: String, // "empty" | "static" | "active"
|
||||||
|
pub presence: bool,
|
||||||
|
pub confidence: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interpolated signal field for Gaussian splat visualization.
|
||||||
|
pub struct SignalField {
|
||||||
|
pub grid_size: [usize; 3], // [20, 1, 20]
|
||||||
|
pub values: Vec<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ESP32 binary CSI frame (ADR-018 protocol, 20-byte header).
|
||||||
|
pub struct Esp32Frame {
|
||||||
|
pub magic: u32, // 0xC5100001
|
||||||
|
pub node_id: u8,
|
||||||
|
pub n_antennas: u8,
|
||||||
|
pub n_subcarriers: u8,
|
||||||
|
pub freq_mhz: u16,
|
||||||
|
pub sequence: u32,
|
||||||
|
pub rssi: i8,
|
||||||
|
pub noise_floor: i8,
|
||||||
|
pub amplitudes: Vec<f64>,
|
||||||
|
pub phases: Vec<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Data source selection enum.
|
||||||
|
pub enum DataSource {
|
||||||
|
Esp32Udp, // Real ESP32 CSI via UDP port 5005
|
||||||
|
WindowsRssi, // Windows WiFi RSSI via netsh
|
||||||
|
Simulated, // Synthetic sine-wave data
|
||||||
|
Auto, // Try ESP32, fall back to Windows, then simulated
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Domain Services:**
|
||||||
|
- `FeatureExtractionService` -- Computes temporal variance (Welford), Goertzel breathing estimation (9-band filter bank), L2 frame-to-frame motion score, SNR-based signal quality
|
||||||
|
- `VitalSignDetectionService` -- Estimates breathing rate, heart rate, and confidence from CSI phase history
|
||||||
|
- `DataSourceSelectionService` -- Probes UDP port 5005 for ESP32 frames; falls back through Windows RSSI then simulation
|
||||||
|
|
||||||
|
**Invariants:**
|
||||||
|
- Frame history buffer never exceeds 100 entries (oldest dropped on push)
|
||||||
|
- Goertzel breathing estimate requires 3x SNR above noise to be reported
|
||||||
|
- Source type is determined once at startup and does not change during runtime
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Model Management Context
|
||||||
|
|
||||||
|
**Responsibility:** Discover `.rvf` model files from `data/models/`, load weights into memory for inference, manage the active model lifecycle, and support LoRA profile activation.
|
||||||
|
|
||||||
|
```
|
||||||
|
+------------------------------------------------------------+
|
||||||
|
| Model Management Context |
|
||||||
|
+------------------------------------------------------------+
|
||||||
|
| |
|
||||||
|
| +----------------+ +----------------+ |
|
||||||
|
| | Model Scanner | | RVF Reader | |
|
||||||
|
| | (data/models/ | | (parse .rvf | |
|
||||||
|
| | *.rvf enum) | | manifest) | |
|
||||||
|
| +-------+--------+ +-------+--------+ |
|
||||||
|
| | | |
|
||||||
|
| +----------+----------+ |
|
||||||
|
| v |
|
||||||
|
| +-------------------+ |
|
||||||
|
| | Model Registry | |
|
||||||
|
| | (Vec<ModelInfo>) | |
|
||||||
|
| +--------+----------+ |
|
||||||
|
| v |
|
||||||
|
| +-------------------+ |
|
||||||
|
| | Model Loader | |
|
||||||
|
| | (RvfReader -> |---> LoadedModelState |
|
||||||
|
| | weights, | |
|
||||||
|
| | LoRA profiles) | |
|
||||||
|
| +--------+----------+ |
|
||||||
|
| v |
|
||||||
|
| +-------------------+ |
|
||||||
|
| | LoRA Activator | |
|
||||||
|
| | (profile switch) | |
|
||||||
|
| +-------------------+ |
|
||||||
|
| |
|
||||||
|
+------------------------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
**Aggregates:**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Aggregate Root: Runtime state for a loaded RVF model.
|
||||||
|
/// At most one LoadedModelState exists at any time.
|
||||||
|
pub struct LoadedModelState {
|
||||||
|
/// Model identifier (derived from filename without .rvf extension).
|
||||||
|
pub model_id: String,
|
||||||
|
/// Original filename on disk.
|
||||||
|
pub filename: String,
|
||||||
|
/// Version string from the RVF manifest.
|
||||||
|
pub version: String,
|
||||||
|
/// Description from the RVF manifest.
|
||||||
|
pub description: String,
|
||||||
|
/// LoRA profiles available in this model.
|
||||||
|
pub lora_profiles: Vec<String>,
|
||||||
|
/// Currently active LoRA profile (if any).
|
||||||
|
pub active_lora_profile: Option<String>,
|
||||||
|
/// Model weights (f32 parameters).
|
||||||
|
pub weights: Vec<f32>,
|
||||||
|
/// Number of frames processed since load.
|
||||||
|
pub frames_processed: u64,
|
||||||
|
/// Cumulative inference time for avg calculation.
|
||||||
|
pub total_inference_ms: f64,
|
||||||
|
/// When the model was loaded.
|
||||||
|
pub loaded_at: Instant,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Value Objects:**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Summary information for a model discovered on disk.
|
||||||
|
pub struct ModelInfo {
|
||||||
|
pub id: String,
|
||||||
|
pub filename: String,
|
||||||
|
pub version: String,
|
||||||
|
pub description: String,
|
||||||
|
pub size_bytes: u64,
|
||||||
|
pub created_at: String,
|
||||||
|
pub pck_score: Option<f64>,
|
||||||
|
pub has_quantization: bool,
|
||||||
|
pub lora_profiles: Vec<String>,
|
||||||
|
pub segment_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about the currently loaded model with runtime stats.
|
||||||
|
pub struct ActiveModelInfo {
|
||||||
|
pub model_id: String,
|
||||||
|
pub filename: String,
|
||||||
|
pub version: String,
|
||||||
|
pub description: String,
|
||||||
|
pub avg_inference_ms: f64,
|
||||||
|
pub frames_processed: u64,
|
||||||
|
pub pose_source: String, // "model_inference"
|
||||||
|
pub lora_profiles: Vec<String>,
|
||||||
|
pub active_lora_profile: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request to load a model by ID.
|
||||||
|
pub struct LoadModelRequest {
|
||||||
|
pub model_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request to activate a LoRA profile.
|
||||||
|
pub struct ActivateLoraRequest {
|
||||||
|
pub model_id: String,
|
||||||
|
pub profile_name: String,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Domain Services:**
|
||||||
|
- `ModelScanService` -- Scans `data/models/` at startup for `.rvf` files, parses each with `RvfReader` to extract manifest metadata
|
||||||
|
- `ModelLoadService` -- Reads model weights from an RVF container into memory, sets `model_loaded = true`
|
||||||
|
- `LoraActivationService` -- Switches the active LoRA adapter on a loaded model without full reload
|
||||||
|
|
||||||
|
**Invariants:**
|
||||||
|
- Only one model can be loaded at a time; loading a new model implicitly unloads the previous one
|
||||||
|
- A model must be loaded before a LoRA profile can be activated
|
||||||
|
- The `active_lora_profile` must be one of the model's declared `lora_profiles`
|
||||||
|
- Model deletion is refused if the model is currently loaded (must unload first)
|
||||||
|
- `data/models/` directory is created at startup if it does not exist
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. CSI Recording Context
|
||||||
|
|
||||||
|
**Responsibility:** Capture CSI frames to `.csi.jsonl` files during active recording sessions, manage session lifecycle, and provide download/delete operations on stored recordings.
|
||||||
|
|
||||||
|
```
|
||||||
|
+------------------------------------------------------------+
|
||||||
|
| CSI Recording Context |
|
||||||
|
+------------------------------------------------------------+
|
||||||
|
| |
|
||||||
|
| +----------------+ +----------------+ |
|
||||||
|
| | Start/Stop | | Auto-Stop | |
|
||||||
|
| | Controller | | Timer | |
|
||||||
|
| | (REST API) | | (duration_ | |
|
||||||
|
| | | | secs check) | |
|
||||||
|
| +-------+--------+ +-------+--------+ |
|
||||||
|
| | | |
|
||||||
|
| +----------+----------+ |
|
||||||
|
| v |
|
||||||
|
| +-------------------+ |
|
||||||
|
| | Recording State | |
|
||||||
|
| | (session_id, | |
|
||||||
|
| | frame_count, | |
|
||||||
|
| | file_path) | |
|
||||||
|
| +--------+----------+ |
|
||||||
|
| v |
|
||||||
|
| +-------------------+ |
|
||||||
|
| | Frame Writer | |
|
||||||
|
| | (maybe_record_ |---> .csi.jsonl file |
|
||||||
|
| | frame on each | |
|
||||||
|
| | tick) | |
|
||||||
|
| +--------+----------+ |
|
||||||
|
| v |
|
||||||
|
| +-------------------+ |
|
||||||
|
| | Metadata Writer | |
|
||||||
|
| | (.meta.json on | |
|
||||||
|
| | stop) | |
|
||||||
|
| +-------------------+ |
|
||||||
|
| |
|
||||||
|
+------------------------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
**Aggregates:**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Aggregate Root: Runtime state for the active CSI recording session.
|
||||||
|
/// At most one RecordingState can be active at any time.
|
||||||
|
pub struct RecordingState {
|
||||||
|
/// Whether a recording is currently active.
|
||||||
|
pub active: bool,
|
||||||
|
/// Session ID of the active recording.
|
||||||
|
pub session_id: String,
|
||||||
|
/// Session display name.
|
||||||
|
pub session_name: String,
|
||||||
|
/// Optional label / activity tag (e.g., "walking", "standing").
|
||||||
|
pub label: Option<String>,
|
||||||
|
/// Path to the JSONL file being written.
|
||||||
|
pub file_path: PathBuf,
|
||||||
|
/// Number of frames written so far.
|
||||||
|
pub frame_count: u64,
|
||||||
|
/// When the recording started (monotonic clock).
|
||||||
|
pub start_time: Instant,
|
||||||
|
/// ISO-8601 start timestamp for metadata.
|
||||||
|
pub started_at: String,
|
||||||
|
/// Optional auto-stop duration in seconds.
|
||||||
|
pub duration_secs: Option<u64>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Value Objects:**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Metadata for a completed or active recording session.
|
||||||
|
pub struct RecordingSession {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub label: Option<String>,
|
||||||
|
pub started_at: String,
|
||||||
|
pub ended_at: Option<String>,
|
||||||
|
pub frame_count: u64,
|
||||||
|
pub file_size_bytes: u64,
|
||||||
|
pub file_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single recorded CSI frame line (JSONL format).
|
||||||
|
pub struct RecordedFrame {
|
||||||
|
pub timestamp: f64,
|
||||||
|
pub subcarriers: Vec<f64>,
|
||||||
|
pub rssi: f64,
|
||||||
|
pub noise_floor: f64,
|
||||||
|
pub features: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request to start a new recording session.
|
||||||
|
pub struct StartRecordingRequest {
|
||||||
|
pub session_name: String,
|
||||||
|
pub label: Option<String>,
|
||||||
|
pub duration_secs: Option<u64>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Domain Services:**
|
||||||
|
- `RecordingLifecycleService` -- Creates a new `.csi.jsonl` file, generates session ID, manages start/stop transitions
|
||||||
|
- `FrameWriterService` -- Called on each tick via `maybe_record_frame()`, appends a `RecordedFrame` JSON line to the active file
|
||||||
|
- `AutoStopService` -- Checks elapsed time against `duration_secs` on each tick; triggers stop when exceeded
|
||||||
|
- `RecordingScanService` -- Enumerates `data/recordings/` for `.csi.jsonl` files and reads companion `.meta.json` for session metadata
|
||||||
|
|
||||||
|
**Invariants:**
|
||||||
|
- Only one recording session can be active at a time; starting a new recording while one is active returns HTTP 409 Conflict
|
||||||
|
- Recording with `duration_secs` set auto-stops after the specified elapsed time
|
||||||
|
- A `.meta.json` companion file is written when a recording stops, capturing final frame count and duration
|
||||||
|
- `data/recordings/` directory is created at startup if it does not exist
|
||||||
|
- Frame writer acquires a read lock on `AppStateInner` per tick; stop acquires a write lock
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Training Pipeline Context
|
||||||
|
|
||||||
|
**Responsibility:** Run background training against recorded CSI data, stream epoch-level progress via WebSocket, and export trained models as `.rvf` containers. Supports supervised training, contrastive pretraining (ADR-024), and LoRA fine-tuning.
|
||||||
|
|
||||||
|
```
|
||||||
|
+------------------------------------------------------------+
|
||||||
|
| Training Pipeline Context |
|
||||||
|
+------------------------------------------------------------+
|
||||||
|
| |
|
||||||
|
| +----------------+ +----------------+ |
|
||||||
|
| | Training API | | WebSocket | |
|
||||||
|
| | (start/stop/ | | Progress | |
|
||||||
|
| | status) | | Streamer | |
|
||||||
|
| +-------+--------+ +-------+--------+ |
|
||||||
|
| | ^ |
|
||||||
|
| v | |
|
||||||
|
| +-------------------+ | |
|
||||||
|
| | Training | | |
|
||||||
|
| | Orchestrator +--------+ |
|
||||||
|
| | (tokio::spawn) | broadcast::Sender |
|
||||||
|
| +--------+----------+ |
|
||||||
|
| v |
|
||||||
|
| +-------------------+ |
|
||||||
|
| | Feature | |
|
||||||
|
| | Extractor | |
|
||||||
|
| | (subcarrier var, | |
|
||||||
|
| | Goertzel power, | |
|
||||||
|
| | temporal grad) | |
|
||||||
|
| +--------+----------+ |
|
||||||
|
| v |
|
||||||
|
| +-------------------+ |
|
||||||
|
| | Gradient Descent | |
|
||||||
|
| | Trainer | |
|
||||||
|
| | (batch SGD, |---> TrainingProgress |
|
||||||
|
| | early stopping, | |
|
||||||
|
| | warmup) | |
|
||||||
|
| +--------+----------+ |
|
||||||
|
| v |
|
||||||
|
| +-------------------+ |
|
||||||
|
| | RVF Exporter | |
|
||||||
|
| | (RvfBuilder -> |---> data/models/*.rvf |
|
||||||
|
| | .rvf container) | |
|
||||||
|
| +-------------------+ |
|
||||||
|
| |
|
||||||
|
+------------------------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
**Aggregates:**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Aggregate Root: Runtime training state stored in AppStateInner.
|
||||||
|
/// At most one training run can be active at any time.
|
||||||
|
pub struct TrainingState {
|
||||||
|
/// Current status snapshot.
|
||||||
|
pub status: TrainingStatus,
|
||||||
|
/// Handle to the background training task (for cancellation).
|
||||||
|
pub task_handle: Option<tokio::task::JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Value Objects:**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Current training status (returned by GET /api/v1/train/status).
|
||||||
|
pub struct TrainingStatus {
|
||||||
|
pub active: bool,
|
||||||
|
pub epoch: u32,
|
||||||
|
pub total_epochs: u32,
|
||||||
|
pub train_loss: f64,
|
||||||
|
pub val_pck: f64, // Percentage of Correct Keypoints
|
||||||
|
pub val_oks: f64, // Object Keypoint Similarity
|
||||||
|
pub lr: f64, // current learning rate
|
||||||
|
pub best_pck: f64,
|
||||||
|
pub best_epoch: u32,
|
||||||
|
pub patience_remaining: u32,
|
||||||
|
pub eta_secs: Option<u64>,
|
||||||
|
pub phase: String, // "idle" | "training" | "complete" | "failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Progress update sent over WebSocket to connected UI clients.
|
||||||
|
pub struct TrainingProgress {
|
||||||
|
pub epoch: u32,
|
||||||
|
pub batch: u32,
|
||||||
|
pub total_batches: u32,
|
||||||
|
pub train_loss: f64,
|
||||||
|
pub val_pck: f64,
|
||||||
|
pub val_oks: f64,
|
||||||
|
pub lr: f64,
|
||||||
|
pub phase: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Training configuration submitted with a start request.
|
||||||
|
pub struct TrainingConfig {
|
||||||
|
pub epochs: u32, // default: 100
|
||||||
|
pub batch_size: u32, // default: 8
|
||||||
|
pub learning_rate: f64, // default: 0.001
|
||||||
|
pub weight_decay: f64, // default: 1e-4
|
||||||
|
pub early_stopping_patience: u32, // default: 20
|
||||||
|
pub warmup_epochs: u32, // default: 5
|
||||||
|
pub pretrained_rvf: Option<String>,
|
||||||
|
pub lora_profile: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request to start supervised training.
|
||||||
|
pub struct StartTrainingRequest {
|
||||||
|
pub dataset_ids: Vec<String>, // recording session IDs
|
||||||
|
pub config: TrainingConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request to start contrastive pretraining (ADR-024).
|
||||||
|
pub struct PretrainRequest {
|
||||||
|
pub dataset_ids: Vec<String>,
|
||||||
|
pub epochs: u32, // default: 50
|
||||||
|
pub lr: f64, // default: 0.001
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request to start LoRA fine-tuning.
|
||||||
|
pub struct LoraTrainRequest {
|
||||||
|
pub base_model_id: String,
|
||||||
|
pub dataset_ids: Vec<String>,
|
||||||
|
pub profile_name: String,
|
||||||
|
pub rank: u8, // default: 8
|
||||||
|
pub epochs: u32, // default: 30
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Domain Services:**
|
||||||
|
- `TrainingOrchestrationService` -- Spawns a background `tokio::task`, loads recorded frames, runs feature extraction, executes gradient descent with early stopping and warmup
|
||||||
|
- `FeatureExtractionService` -- Computes per-subcarrier sliding-window variance, temporal gradients, Goertzel frequency-domain power across 9 bands, and 3 global scalar features (mean amplitude, std, motion score)
|
||||||
|
- `ProgressBroadcastService` -- Sends `TrainingProgress` messages through a `broadcast::Sender` channel that WebSocket handlers subscribe to
|
||||||
|
- `RvfExportService` -- Uses `RvfBuilder` to write the best checkpoint as a `.rvf` container to `data/models/`
|
||||||
|
|
||||||
|
**Invariants:**
|
||||||
|
- Only one training run can be active at a time; starting training while one is running returns HTTP 409 Conflict
|
||||||
|
- Training requires at least one recording with a minimum frame count before starting
|
||||||
|
- Early stopping halts training after `patience` epochs with no improvement in `val_pck`
|
||||||
|
- Learning rate warmup ramps linearly from 0 to `learning_rate` over `warmup_epochs`
|
||||||
|
- On completion, the best model (by `val_pck`) is automatically exported as `.rvf`
|
||||||
|
- Training status phase transitions: `idle` -> `training` -> `complete` | `failed` -> `idle`
|
||||||
|
- Stopping an active training run aborts the background task via `JoinHandle::abort()` and resets phase to `idle`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Visualization Context
|
||||||
|
|
||||||
|
**Responsibility:** Stream sensing data to web UI clients via WebSocket, render Gaussian splat visualizations, display data source transparency indicators, and manage UI mode (full vs. sensing-only).
|
||||||
|
|
||||||
|
```
|
||||||
|
+------------------------------------------------------------+
|
||||||
|
| Visualization Context |
|
||||||
|
+------------------------------------------------------------+
|
||||||
|
| |
|
||||||
|
| +----------------+ +----------------+ |
|
||||||
|
| | WebSocket | | Sensing | |
|
||||||
|
| | Hub | | Service (JS) | |
|
||||||
|
| | (/ws/sensing) | | (client-side | |
|
||||||
|
| | broadcast:: | | reconnect + | |
|
||||||
|
| | Receiver | | sim fallback)| |
|
||||||
|
| +-------+--------+ +-------+--------+ |
|
||||||
|
| | | |
|
||||||
|
| +----------+----------+ |
|
||||||
|
| v |
|
||||||
|
| +----------------------------------------------+ |
|
||||||
|
| | UI Components | |
|
||||||
|
| | | |
|
||||||
|
| | +----------+ +----------+ +----------+ | |
|
||||||
|
| | | Sensing | | Live | | Models | | |
|
||||||
|
| | | Tab | | Demo Tab | | Tab | | |
|
||||||
|
| | | (splats) | | (pose) | | (manage) | | |
|
||||||
|
| | +----------+ +----------+ +----------+ | |
|
||||||
|
| | +----------+ +----------+ | |
|
||||||
|
| | | Recording| | Training | | |
|
||||||
|
| | | Tab | | Tab | | |
|
||||||
|
| | | (capture)| | (train) | | |
|
||||||
|
| | +----------+ +----------+ | |
|
||||||
|
| +----------------------------------------------+ |
|
||||||
|
| |
|
||||||
|
+------------------------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
**Value Objects:**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Data source indicator shown in the UI (ADR-035).
|
||||||
|
pub enum DataSourceIndicator {
|
||||||
|
LiveEsp32, // Green banner: "LIVE - ESP32"
|
||||||
|
Reconnecting, // Yellow banner: "RECONNECTING..."
|
||||||
|
Simulated, // Red banner: "SIMULATED DATA"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pose estimation mode badge (ADR-035).
|
||||||
|
pub enum EstimationMode {
|
||||||
|
SignalDerived, // Green badge: analytical pose from CSI features
|
||||||
|
ModelInference, // Blue badge: neural network inference from loaded RVF
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render mode for pose visualization (ADR-035).
|
||||||
|
pub enum RenderMode {
|
||||||
|
Skeleton, // Green lines connecting joints + red keypoint dots
|
||||||
|
Keypoints, // Large colored dots with glow and labels
|
||||||
|
Heatmap, // Gaussian radial blobs per keypoint, faint skeleton overlay
|
||||||
|
Dense, // Body region segmentation with colored filled polygons
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Domain Services:**
|
||||||
|
- `WebSocketBroadcastService` -- Subscribes to `broadcast::Sender<String>`, forwards each `SensingUpdate` JSON to all connected WebSocket clients
|
||||||
|
- `SensingServiceJS` -- Client-side JavaScript that manages WebSocket connection, tracks `dataSource` state, falls back to simulation after 5 failed reconnect attempts (~30s delay)
|
||||||
|
- `GaussianSplatRenderer` -- Custom GLSL `ShaderMaterial` rendering point-cloud splats on a 20x20 floor grid, colored by signal intensity
|
||||||
|
- `PoseRenderer` -- Renders skeleton, keypoints, heatmap, or dense body segmentation modes
|
||||||
|
- `BackendDetector` -- Auto-detects whether the full DensePose backend is available; sets `sensingOnlyMode = true` if unreachable
|
||||||
|
|
||||||
|
**Invariants:**
|
||||||
|
- WebSocket sensing service is started on application init, not lazily on tab visit (ADR-043 fix)
|
||||||
|
- Simulation fallback is delayed to 5 failed reconnect attempts (~30 seconds) to avoid premature synthetic data
|
||||||
|
- `pose_source` field is passed through data conversion so the Estimation Mode badge displays correctly
|
||||||
|
- Dashboard and Live Demo tabs read `sensingService.dataSource` at load time -- the service must already be connected
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Domain Events
|
||||||
|
|
||||||
|
| Event | Published By | Consumed By | Payload |
|
||||||
|
|-------|-------------|-------------|---------|
|
||||||
|
| `ServerStarted` | CSI Ingestion | Visualization | `{ http_port, udp_port, source_type }` |
|
||||||
|
| `CsiFrameIngested` | CSI Ingestion | Recording, Visualization | `{ source, node_id, subcarrier_count, tick }` |
|
||||||
|
| `SensingUpdateBroadcast` | CSI Ingestion | Visualization (WebSocket) | Full `SensingUpdate` JSON |
|
||||||
|
| `ModelLoaded` | Model Management | CSI Ingestion (inference path) | `{ model_id, weight_count, version }` |
|
||||||
|
| `ModelUnloaded` | Model Management | CSI Ingestion | `{ model_id }` |
|
||||||
|
| `LoraProfileActivated` | Model Management | CSI Ingestion | `{ model_id, profile_name }` |
|
||||||
|
| `RecordingStarted` | Recording | Visualization | `{ session_id, session_name, file_path }` |
|
||||||
|
| `RecordingStopped` | Recording | Visualization | `{ session_id, frame_count, duration_secs }` |
|
||||||
|
| `TrainingStarted` | Training Pipeline | Visualization | `{ run_id, config, recording_ids }` |
|
||||||
|
| `TrainingEpochComplete` | Training Pipeline | Visualization (WebSocket) | `{ epoch, total_epochs, train_loss, val_pck, lr }` |
|
||||||
|
| `TrainingComplete` | Training Pipeline | Model Management, Visualization | `{ run_id, final_pck, model_path }` |
|
||||||
|
| `TrainingFailed` | Training Pipeline | Visualization | `{ run_id, error_message }` |
|
||||||
|
| `WebSocketClientConnected` | Visualization | -- | `{ endpoint, client_addr }` |
|
||||||
|
| `WebSocketClientDisconnected` | Visualization | -- | `{ endpoint, client_addr }` |
|
||||||
|
|
||||||
|
In the current implementation, events are realized through two mechanisms:
|
||||||
|
1. **`broadcast::Sender<String>`** for WebSocket fan-out of sensing updates
|
||||||
|
2. **`broadcast::Sender<TrainingProgress>`** for training progress streaming
|
||||||
|
3. **State mutations via RwLock** where other contexts read state changes on their next tick
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context Map
|
||||||
|
|
||||||
|
```
|
||||||
|
+-------------------+ +---------------------+
|
||||||
|
| CSI Ingestion |--------->| Visualization |
|
||||||
|
| (produces | publish | (WebSocket |
|
||||||
|
| SensingUpdate) | -------> | consumers) |
|
||||||
|
+--------+----------+ +----------+----------+
|
||||||
|
| |
|
||||||
|
| maybe_record_frame() | reads dataSource
|
||||||
|
v |
|
||||||
|
+-------------------+ |
|
||||||
|
| CSI Recording | |
|
||||||
|
| (hooks into | |
|
||||||
|
| tick loop) | |
|
||||||
|
+--------+----------+ |
|
||||||
|
| |
|
||||||
|
| provides dataset_ids |
|
||||||
|
v |
|
||||||
|
+-------------------+ +----------+----------+
|
||||||
|
| Training Pipeline |--------->| Model Management |
|
||||||
|
| (reads .jsonl, | exports | (loads .rvf for |
|
||||||
|
| trains model) | .rvf --> | inference) |
|
||||||
|
+-------------------+ +----------+----------+
|
||||||
|
|
|
||||||
|
| model weights
|
||||||
|
v
|
||||||
|
+----------+----------+
|
||||||
|
| CSI Ingestion |
|
||||||
|
| (inference path |
|
||||||
|
| uses loaded model)|
|
||||||
|
+----------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
**Relationships:**
|
||||||
|
|
||||||
|
| Upstream | Downstream | Relationship | Mechanism |
|
||||||
|
|----------|-----------|--------------|-----------|
|
||||||
|
| CSI Ingestion | Visualization | Published Language | `broadcast::Sender<String>` with `SensingUpdate` JSON schema |
|
||||||
|
| CSI Ingestion | CSI Recording | Shared Kernel | `maybe_record_frame()` called from the ingestion tick loop |
|
||||||
|
| CSI Recording | Training Pipeline | Conformist | Training reads `.csi.jsonl` files produced by recording; no negotiation on format |
|
||||||
|
| Training Pipeline | Model Management | Supplier-Consumer | Training exports `.rvf` to `data/models/`; Model Management scans and loads |
|
||||||
|
| Model Management | CSI Ingestion | Shared Kernel | Loaded weights stored in `AppStateInner`; ingestion reads them for inference |
|
||||||
|
| Training Pipeline | Visualization | Published Language | `broadcast::Sender<TrainingProgress>` with progress JSON schema |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anti-Corruption Layers
|
||||||
|
|
||||||
|
### ESP32 Binary Protocol ACL
|
||||||
|
|
||||||
|
The ESP32 sends CSI frames using a compact binary protocol (ADR-018): 20-byte header with magic `0xC5100001`, followed by amplitude and phase arrays. The `Esp32Frame` parser in the ingestion context decodes this binary format into domain value objects (`NodeInfo`, amplitude/phase vectors) before any downstream processing. No other context handles raw UDP bytes.
|
||||||
|
|
||||||
|
### RVF Container ACL
|
||||||
|
|
||||||
|
The `.rvf` container format encapsulates model weights, manifest metadata, vital sign configuration, and optional LoRA adapters. The `RvfReader` and `RvfBuilder` types in the `rvf_container` module provide the anti-corruption layer between the on-disk binary format and the domain types (`ModelInfo`, `LoadedModelState`). The training pipeline writes through `RvfBuilder`; the model management context reads through `RvfReader`.
|
||||||
|
|
||||||
|
### Sensing-Only Mode ACL (Client-Side)
|
||||||
|
|
||||||
|
When the DensePose backend (port 8000) is unreachable, the client-side `BackendDetector` sets `sensingOnlyMode = true`. The `ApiService.request()` method short-circuits all requests to the DensePose backend, returning empty responses instead of `ERR_CONNECTION_REFUSED`. This prevents DensePose-specific concerns from leaking into the sensing UI.
|
||||||
|
|
||||||
|
### JSONL Recording Format ACL
|
||||||
|
|
||||||
|
CSI frames are recorded as newline-delimited JSON (`.csi.jsonl`). The `RecordedFrame` struct defines the schema: `{timestamp, subcarriers, rssi, noise_floor, features}`. The training pipeline reads through this schema, extracting subcarrier arrays for feature computation. If the internal sensing representation changes, only the `maybe_record_frame()` serializer needs updating -- the training pipeline depends only on the `RecordedFrame` contract.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## REST API Surface
|
||||||
|
|
||||||
|
All endpoints share `AppStateInner` via `Arc<RwLock<AppStateInner>>`.
|
||||||
|
|
||||||
|
### CSI Ingestion & Sensing
|
||||||
|
|
||||||
|
| Method | Path | Context | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| GET | `/api/v1/sensing/latest` | Ingestion | Latest sensing update |
|
||||||
|
| WS | `/ws/sensing` | Visualization | Streaming sensing updates |
|
||||||
|
|
||||||
|
### Model Management
|
||||||
|
|
||||||
|
| Method | Path | Context | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| GET | `/api/v1/models` | Model Mgmt | List all discovered `.rvf` models |
|
||||||
|
| GET | `/api/v1/models/:id` | Model Mgmt | Detailed info for a specific model |
|
||||||
|
| GET | `/api/v1/models/active` | Model Mgmt | Active model with runtime stats |
|
||||||
|
| POST | `/api/v1/models/load` | Model Mgmt | Load model weights into memory |
|
||||||
|
| POST | `/api/v1/models/unload` | Model Mgmt | Unload the active model |
|
||||||
|
| DELETE | `/api/v1/models/:id` | Model Mgmt | Delete a model file from disk |
|
||||||
|
| GET | `/api/v1/models/lora/profiles` | Model Mgmt | List LoRA profiles for active model |
|
||||||
|
| POST | `/api/v1/models/lora/activate` | Model Mgmt | Activate a LoRA adapter |
|
||||||
|
|
||||||
|
### CSI Recording
|
||||||
|
|
||||||
|
| Method | Path | Context | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| POST | `/api/v1/recording/start` | Recording | Start a new recording session |
|
||||||
|
| POST | `/api/v1/recording/stop` | Recording | Stop the active recording |
|
||||||
|
| GET | `/api/v1/recording/list` | Recording | List all recording sessions |
|
||||||
|
| GET | `/api/v1/recording/download/:id` | Recording | Download a `.csi.jsonl` file |
|
||||||
|
| DELETE | `/api/v1/recording/:id` | Recording | Delete a recording |
|
||||||
|
|
||||||
|
### Training Pipeline
|
||||||
|
|
||||||
|
| Method | Path | Context | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| POST | `/api/v1/train/start` | Training | Start supervised training |
|
||||||
|
| POST | `/api/v1/train/stop` | Training | Stop the active training run |
|
||||||
|
| GET | `/api/v1/train/status` | Training | Current training phase and metrics |
|
||||||
|
| POST | `/api/v1/train/pretrain` | Training | Start contrastive pretraining |
|
||||||
|
| POST | `/api/v1/train/lora` | Training | Start LoRA fine-tuning |
|
||||||
|
| WS | `/ws/train/progress` | Training | Streaming training progress |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
data/
|
||||||
|
+-- models/ # RVF model files
|
||||||
|
| +-- wifi-densepose-v1.rvf # Trained model container
|
||||||
|
| +-- wifi-densepose-field-v2.rvf # Environment-calibrated model
|
||||||
|
+-- recordings/ # CSI recording sessions
|
||||||
|
+-- walking-20260303_140000.csi.jsonl # Raw CSI frames (JSONL)
|
||||||
|
+-- walking-20260303_140000.csi.meta.json # Session metadata
|
||||||
|
+-- standing-20260303_141500.csi.jsonl
|
||||||
|
+-- standing-20260303_141500.csi.meta.json
|
||||||
|
|
||||||
|
crates/wifi-densepose-sensing-server/
|
||||||
|
+-- src/
|
||||||
|
+-- main.rs # Server entry, CLI args, AppStateInner, sensing loop
|
||||||
|
+-- model_manager.rs # Model Management bounded context
|
||||||
|
+-- recording.rs # CSI Recording bounded context
|
||||||
|
+-- training_api.rs # Training Pipeline bounded context
|
||||||
|
+-- rvf_container.rs # RVF format ACL (RvfReader, RvfBuilder)
|
||||||
|
+-- rvf_pipeline.rs # Progressive loader for model inference
|
||||||
|
+-- vital_signs.rs # Vital sign detection from CSI phase
|
||||||
|
+-- dataset.rs # Dataset loading for training
|
||||||
|
+-- trainer.rs # Core training loop implementation
|
||||||
|
+-- embedding.rs # Contrastive embedding extraction
|
||||||
|
+-- graph_transformer.rs # Graph transformer architecture
|
||||||
|
+-- sona.rs # SONA self-optimizing profile
|
||||||
|
+-- sparse_inference.rs # Sparse inference engine
|
||||||
|
+-- lib.rs # Public module re-exports
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [ADR-019: Sensing-Only UI Mode](../adr/ADR-019-sensing-only-ui-mode.md) -- Decoupled sensing UI, Gaussian splats, Python WebSocket bridge
|
||||||
|
- [ADR-035: Live Sensing UI Accuracy](../adr/ADR-035-live-sensing-ui-accuracy.md) -- Data transparency, Goertzel breathing estimation, signal-responsive pose
|
||||||
|
- [ADR-043: Sensing Server UI API Completion](../adr/ADR-043-sensing-server-ui-api-completion.md) -- Model, recording, training endpoints; single-binary deployment
|
||||||
|
- [RuvSense Domain Model](ruvsense-domain-model.md) -- Upstream signal processing domain (multistatic sensing, coherence, tracking)
|
||||||
|
- [WiFi-Mat Domain Model](wifi-mat-domain-model.md) -- Downstream disaster response domain
|
||||||
|
|
@ -0,0 +1,663 @@
|
||||||
|
# Signal Processing Domain Model
|
||||||
|
|
||||||
|
## Domain-Driven Design Specification
|
||||||
|
|
||||||
|
Based on ADR-014 (SOTA Signal Processing) and the `wifi-densepose-signal` crate.
|
||||||
|
|
||||||
|
### Ubiquitous Language
|
||||||
|
|
||||||
|
| Term | Definition |
|
||||||
|
|------|------------|
|
||||||
|
| **CsiFrame** | A single CSI measurement: amplitude + phase per antenna per subcarrier at one timestamp |
|
||||||
|
| **Conjugate Multiplication** | `H_ref[k] * conj(H_target[k])` — cancels CFO/SFO/PDD, isolating environment-induced phase |
|
||||||
|
| **CSI Ratio** | The complex result of conjugate multiplication between two antenna streams |
|
||||||
|
| **Hampel Filter** | Running median +/- scaled MAD outlier detector; resists up to 50% contamination |
|
||||||
|
| **Phase Sanitization** | Pipeline of unwrapping, outlier removal, smoothing, and noise filtering on raw CSI phase |
|
||||||
|
| **Spectrogram** | 2D time-frequency matrix from STFT, standard CNN input for WiFi activity recognition |
|
||||||
|
| **Subcarrier Sensitivity** | Variance ratio (motion var / static var) ranking how responsive a subcarrier is to motion |
|
||||||
|
| **Body Velocity Profile (BVP)** | Doppler-derived velocity x time 2D matrix; domain-independent motion representation |
|
||||||
|
| **Fresnel Zone** | Ellipsoidal region between TX and RX where signal reflection/diffraction occurs |
|
||||||
|
| **Breathing Estimate** | BPM + amplitude + confidence derived from Fresnel zone boundary crossings |
|
||||||
|
| **Motion Score** | Composite (0.0-1.0) from variance, correlation, phase, and optional Doppler components |
|
||||||
|
| **Presence State** | Binary detection result: human present/absent with smoothed confidence |
|
||||||
|
| **Calibration** | Recording baseline variance during a known-empty period for adaptive detection |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bounded Contexts
|
||||||
|
|
||||||
|
### 1. CSI Preprocessing Context
|
||||||
|
|
||||||
|
**Responsibility**: Produce clean, hardware-artifact-free CSI data from raw measurements.
|
||||||
|
|
||||||
|
```
|
||||||
|
+-----------------------------------------------------------+
|
||||||
|
| CSI Preprocessing Context |
|
||||||
|
+-----------------------------------------------------------+
|
||||||
|
| |
|
||||||
|
| +--------------+ +--------------+ +------------+ |
|
||||||
|
| | Conjugate | | Hampel | | Phase | |
|
||||||
|
| | Multiplication| | Filter | | Sanitizer | |
|
||||||
|
| +------+-------+ +------+-------+ +-----+------+ |
|
||||||
|
| | | | |
|
||||||
|
| v v v |
|
||||||
|
| +------+-------+ +------+-------+ +-----+------+ |
|
||||||
|
| | CsiRatio | | HampelResult | | Sanitized | |
|
||||||
|
| | (clean phase)| |(outlier-free)| | Phase | |
|
||||||
|
| +--------------+ +--------------+ +------------+ |
|
||||||
|
| | | | |
|
||||||
|
| +-------------------+------------------+ |
|
||||||
|
| | |
|
||||||
|
| v |
|
||||||
|
| +-------+--------+ |
|
||||||
|
| | CsiProcessor |--> CleanedCsiData |
|
||||||
|
| +----------------+ |
|
||||||
|
| |
|
||||||
|
+-----------------------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
**Aggregates**: `CsiProcessor` (Aggregate Root)
|
||||||
|
|
||||||
|
**Value Objects**: `CsiData`, `CsiRatio`, `HampelResult`, `HampelConfig`, `PhaseSanitizerConfig`
|
||||||
|
|
||||||
|
**Domain Services**: `CsiPreprocessor`, `PhaseSanitizer`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Feature Extraction Context
|
||||||
|
|
||||||
|
**Responsibility**: Transform clean CSI data into ML-ready feature representations.
|
||||||
|
|
||||||
|
```
|
||||||
|
+-----------------------------------------------------------+
|
||||||
|
| Feature Extraction Context |
|
||||||
|
+-----------------------------------------------------------+
|
||||||
|
| |
|
||||||
|
| +--------------+ +--------------+ +------------+ |
|
||||||
|
| | STFT | | Subcarrier | | Doppler | |
|
||||||
|
| | Spectrogram | | Selection | | BVP Engine | |
|
||||||
|
| +------+-------+ +------+-------+ +-----+------+ |
|
||||||
|
| | | | |
|
||||||
|
| v v v |
|
||||||
|
| +------+-------+ +------+-------+ +-----+------+ |
|
||||||
|
| | Spectrogram | | Subcarrier | | BodyVel | |
|
||||||
|
| | (2D TF) | | Selection | | Profile | |
|
||||||
|
| +--------------+ +--------------+ +------------+ |
|
||||||
|
| | | | |
|
||||||
|
| +-------------------+------------------+ |
|
||||||
|
| | |
|
||||||
|
| v |
|
||||||
|
| +----------+----------+ |
|
||||||
|
| | FeatureExtractor |--> CsiFeatures |
|
||||||
|
| +---------------------+ |
|
||||||
|
| |
|
||||||
|
+-----------------------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
**Aggregates**: `FeatureExtractor` (Aggregate Root)
|
||||||
|
|
||||||
|
**Value Objects**: `Spectrogram`, `SubcarrierSelection`, `BodyVelocityProfile`, `CsiFeatures`
|
||||||
|
|
||||||
|
**Domain Services**: `SpectrogramConfig`, `SubcarrierSelectionConfig`, `BvpConfig`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Motion Analysis Context
|
||||||
|
|
||||||
|
**Responsibility**: Detect and classify human motion and vital signs from CSI features.
|
||||||
|
|
||||||
|
```
|
||||||
|
+-----------------------------------------------------------+
|
||||||
|
| Motion Analysis Context |
|
||||||
|
+-----------------------------------------------------------+
|
||||||
|
| |
|
||||||
|
| +--------------+ +--------------+ |
|
||||||
|
| | Motion | | Fresnel | |
|
||||||
|
| | Detector | | Breathing | |
|
||||||
|
| +------+-------+ +------+-------+ |
|
||||||
|
| | | |
|
||||||
|
| v v |
|
||||||
|
| +------+-------+ +------+-------+ |
|
||||||
|
| | MotionScore | | Breathing | |
|
||||||
|
| |+ Detection | | Estimate | |
|
||||||
|
| +--------------+ +--------------+ |
|
||||||
|
| | | |
|
||||||
|
| +-------------------+ |
|
||||||
|
| | |
|
||||||
|
| v |
|
||||||
|
| +--------+--------+ |
|
||||||
|
| | HumanDetection |--> PresenceState |
|
||||||
|
| | Result | |
|
||||||
|
| +-----------------+ |
|
||||||
|
| |
|
||||||
|
+-----------------------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
**Aggregates**: `MotionDetector` (Aggregate Root)
|
||||||
|
|
||||||
|
**Value Objects**: `MotionScore`, `MotionAnalysis`, `HumanDetectionResult`, `BreathingEstimate`, `FresnelGeometry`
|
||||||
|
|
||||||
|
**Domain Services**: `FresnelBreathingEstimator`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Aggregates
|
||||||
|
|
||||||
|
### CsiProcessor (CSI Preprocessing Root)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct CsiProcessor {
|
||||||
|
config: CsiProcessorConfig,
|
||||||
|
preprocessor: CsiPreprocessor,
|
||||||
|
history: VecDeque<CsiData>,
|
||||||
|
previous_detection_confidence: f64,
|
||||||
|
statistics: ProcessingStatistics,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CsiProcessor {
|
||||||
|
/// Create with validated configuration
|
||||||
|
pub fn new(config: CsiProcessorConfig) -> Result<Self, CsiProcessorError>;
|
||||||
|
|
||||||
|
/// Full preprocessing pipeline: noise removal -> windowing -> normalization
|
||||||
|
pub fn preprocess(&self, csi_data: &CsiData) -> Result<CsiData, CsiProcessorError>;
|
||||||
|
|
||||||
|
/// Maintain temporal history for downstream feature extraction
|
||||||
|
pub fn add_to_history(&mut self, csi_data: CsiData);
|
||||||
|
|
||||||
|
/// Apply exponential moving average to detection confidence
|
||||||
|
pub fn apply_temporal_smoothing(&mut self, raw_confidence: f64) -> f64;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### FeatureExtractor (Feature Extraction Root)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct FeatureExtractor {
|
||||||
|
config: FeatureExtractorConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeatureExtractor {
|
||||||
|
/// Extract all feature types from a single CsiData snapshot
|
||||||
|
pub fn extract(&self, csi_data: &CsiData) -> CsiFeatures;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### MotionDetector (Motion Analysis Root)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct MotionDetector {
|
||||||
|
config: MotionDetectorConfig,
|
||||||
|
previous_confidence: f64,
|
||||||
|
motion_history: VecDeque<MotionScore>,
|
||||||
|
baseline_variance: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MotionDetector {
|
||||||
|
/// Analyze motion from extracted features
|
||||||
|
pub fn analyze_motion(&self, features: &CsiFeatures) -> MotionAnalysis;
|
||||||
|
|
||||||
|
/// Full detection pipeline: analyze -> score -> smooth -> threshold
|
||||||
|
pub fn detect_human(&mut self, features: &CsiFeatures) -> HumanDetectionResult;
|
||||||
|
|
||||||
|
/// Record baseline variance for adaptive detection
|
||||||
|
pub fn calibrate(&mut self, features: &CsiFeatures);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Value Objects
|
||||||
|
|
||||||
|
### CsiData
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct CsiData {
|
||||||
|
pub timestamp: DateTime<Utc>,
|
||||||
|
pub amplitude: Array2<f64>, // (num_antennas x num_subcarriers)
|
||||||
|
pub phase: Array2<f64>, // (num_antennas x num_subcarriers), radians
|
||||||
|
pub frequency: f64, // center frequency in Hz
|
||||||
|
pub bandwidth: f64, // bandwidth in Hz
|
||||||
|
pub num_subcarriers: usize,
|
||||||
|
pub num_antennas: usize,
|
||||||
|
pub snr: f64, // signal-to-noise ratio in dB
|
||||||
|
pub metadata: CsiMetadata,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Spectrogram
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct Spectrogram {
|
||||||
|
pub data: Array2<f64>, // (n_freq x n_time) power/magnitude
|
||||||
|
pub n_freq: usize, // frequency bins (window_size/2 + 1)
|
||||||
|
pub n_time: usize, // time frames
|
||||||
|
pub freq_resolution: f64, // Hz per bin
|
||||||
|
pub time_resolution: f64, // seconds per frame
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SubcarrierSelection
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct SubcarrierSelection {
|
||||||
|
pub selected_indices: Vec<usize>, // ranked by sensitivity, descending
|
||||||
|
pub sensitivity_scores: Vec<f64>, // variance ratio for ALL subcarriers
|
||||||
|
pub selected_data: Option<Array2<f64>>, // filtered matrix (optional)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### BodyVelocityProfile
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct BodyVelocityProfile {
|
||||||
|
pub data: Array2<f64>, // (n_velocity_bins x n_time_frames)
|
||||||
|
pub velocity_bins: Vec<f64>, // velocity value for each row (m/s)
|
||||||
|
pub n_time: usize,
|
||||||
|
pub time_resolution: f64, // seconds per frame
|
||||||
|
pub velocity_resolution: f64, // m/s per bin
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### BreathingEstimate
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct BreathingEstimate {
|
||||||
|
pub rate_bpm: f64, // breaths per minute
|
||||||
|
pub confidence: f64, // combined confidence (0.0-1.0)
|
||||||
|
pub period_seconds: f64, // estimated breathing period
|
||||||
|
pub autocorrelation_peak: f64, // periodicity quality
|
||||||
|
pub fresnel_confidence: f64, // Fresnel model match
|
||||||
|
pub amplitude_variation: f64, // observed amplitude variation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### MotionScore
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct MotionScore {
|
||||||
|
pub total: f64, // weighted composite (0.0-1.0)
|
||||||
|
pub variance_component: f64,
|
||||||
|
pub correlation_component: f64,
|
||||||
|
pub phase_component: f64,
|
||||||
|
pub doppler_component: Option<f64>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### HampelResult
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct HampelResult {
|
||||||
|
pub filtered: Vec<f64>, // outliers replaced with local median
|
||||||
|
pub outlier_indices: Vec<usize>,
|
||||||
|
pub medians: Vec<f64>, // local median at each sample
|
||||||
|
pub sigma_estimates: Vec<f64>, // estimated local sigma at each sample
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### FresnelGeometry
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct FresnelGeometry {
|
||||||
|
pub d_tx_body: f64, // TX to body distance (meters)
|
||||||
|
pub d_body_rx: f64, // body to RX distance (meters)
|
||||||
|
pub frequency: f64, // carrier frequency (Hz)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FresnelGeometry {
|
||||||
|
pub fn wavelength(&self) -> f64;
|
||||||
|
pub fn fresnel_radius(&self, n: u32) -> f64;
|
||||||
|
pub fn phase_change(&self, displacement_m: f64) -> f64;
|
||||||
|
pub fn expected_amplitude_variation(&self, displacement_m: f64) -> f64;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Domain Events
|
||||||
|
|
||||||
|
### Preprocessing Events
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum PreprocessingEvent {
|
||||||
|
/// Raw CSI frame cleaned through the full pipeline
|
||||||
|
FrameCleaned {
|
||||||
|
timestamp: DateTime<Utc>,
|
||||||
|
num_antennas: usize,
|
||||||
|
num_subcarriers: usize,
|
||||||
|
noise_filtered: bool,
|
||||||
|
windowed: bool,
|
||||||
|
normalized: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Outliers detected and replaced by Hampel filter
|
||||||
|
OutliersDetected {
|
||||||
|
subcarrier_indices: Vec<usize>,
|
||||||
|
replacement_values: Vec<f64>,
|
||||||
|
contamination_ratio: f64,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Phase sanitization completed
|
||||||
|
PhaseSanitized {
|
||||||
|
method: UnwrappingMethod,
|
||||||
|
outliers_removed: usize,
|
||||||
|
smoothing_applied: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Feature Extraction Events
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum FeatureExtractionEvent {
|
||||||
|
/// Spectrogram computed from temporal CSI stream
|
||||||
|
SpectrogramGenerated {
|
||||||
|
n_time: usize,
|
||||||
|
n_freq: usize,
|
||||||
|
window_size: usize,
|
||||||
|
window_fn: WindowFunction,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Top-K sensitive subcarriers selected
|
||||||
|
SubcarriersSelected {
|
||||||
|
top_k_indices: Vec<usize>,
|
||||||
|
sensitivity_scores: Vec<f64>,
|
||||||
|
min_sensitivity_threshold: f64,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Body Velocity Profile extracted
|
||||||
|
BvpExtracted {
|
||||||
|
n_velocity_bins: usize,
|
||||||
|
n_time_frames: usize,
|
||||||
|
max_velocity: f64,
|
||||||
|
carrier_frequency: f64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Motion Analysis Events
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum MotionAnalysisEvent {
|
||||||
|
/// Human motion detected above threshold
|
||||||
|
MotionDetected {
|
||||||
|
score: MotionScore,
|
||||||
|
confidence: f64,
|
||||||
|
threshold: f64,
|
||||||
|
timestamp: DateTime<Utc>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Breathing detected via Fresnel zone model
|
||||||
|
BreathingDetected {
|
||||||
|
rate_bpm: f64,
|
||||||
|
amplitude_variation: f64,
|
||||||
|
fresnel_confidence: f64,
|
||||||
|
autocorrelation_peak: f64,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Presence state changed (entered or left)
|
||||||
|
PresenceChanged {
|
||||||
|
previous: bool,
|
||||||
|
current: bool,
|
||||||
|
smoothed_confidence: f64,
|
||||||
|
timestamp: DateTime<Utc>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Detector calibrated with baseline variance
|
||||||
|
BaselineCalibrated {
|
||||||
|
baseline_variance: f64,
|
||||||
|
timestamp: DateTime<Utc>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Invariants
|
||||||
|
|
||||||
|
### CSI Preprocessing Invariants
|
||||||
|
|
||||||
|
1. **Conjugate multiplication requires >= 2 antenna elements.** `compute_ratio_matrix` returns `CsiRatioError::InsufficientAntennas` if `n_ant < 2`. Without two antennas, there is no pair to cancel common-mode offsets.
|
||||||
|
|
||||||
|
2. **Hampel filter window must be >= 1 (half_window > 0).** A zero-width window cannot compute a local median. Enforced by `HampelError::InvalidWindow`.
|
||||||
|
|
||||||
|
3. **Phase data must be within configured range before sanitization.** Default range is `[-pi, pi]`. Enforced by `PhaseSanitizer::validate_phase_data`.
|
||||||
|
|
||||||
|
4. **Antenna stream lengths must match for conjugate multiplication.** `conjugate_multiply` returns `CsiRatioError::LengthMismatch` if `h_ref.len() != h_target.len()`.
|
||||||
|
|
||||||
|
### Feature Extraction Invariants
|
||||||
|
|
||||||
|
5. **Spectrogram window size must be > 0 and signal must be >= window_size samples.** Enforced by `SpectrogramError::SignalTooShort` and `SpectrogramError::InvalidWindowSize`.
|
||||||
|
|
||||||
|
6. **Subcarrier selection must receive matching subcarrier counts.** Motion and static data must have the same number of columns. Enforced by `SelectionError::SubcarrierCountMismatch`.
|
||||||
|
|
||||||
|
7. **BVP requires >= window_size temporal samples.** Insufficient history prevents STFT computation. Enforced by `BvpError::InsufficientSamples`.
|
||||||
|
|
||||||
|
8. **BVP carrier frequency must be > 0 for wavelength calculation.** Zero frequency would produce a division-by-zero in the Doppler-to-velocity mapping.
|
||||||
|
|
||||||
|
### Motion Analysis Invariants
|
||||||
|
|
||||||
|
9. **Fresnel geometry requires positive distances (d_tx_body > 0, d_body_rx > 0).** Zero or negative distances are physically impossible. Enforced by `FresnelError::InvalidDistance`.
|
||||||
|
|
||||||
|
10. **Fresnel frequency must be positive.** Required for wavelength computation. Enforced by `FresnelError::InvalidFrequency`.
|
||||||
|
|
||||||
|
11. **Breathing estimation requires >= 10 amplitude samples.** Fewer samples cannot support autocorrelation analysis. Enforced by `FresnelError::InsufficientData`.
|
||||||
|
|
||||||
|
12. **Motion detector history does not exceed configured max size.** Oldest entries are evicted via `VecDeque::pop_front` when capacity is reached.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Domain Services
|
||||||
|
|
||||||
|
### CsiPreprocessor
|
||||||
|
|
||||||
|
Orchestrates the cleaning pipeline for a single CSI frame.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct CsiPreprocessor {
|
||||||
|
noise_threshold: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CsiPreprocessor {
|
||||||
|
/// Remove subcarriers below noise floor (amplitude in dB < threshold)
|
||||||
|
pub fn remove_noise(&self, csi_data: &CsiData) -> Result<CsiData, CsiProcessorError>;
|
||||||
|
|
||||||
|
/// Apply Hamming window to reduce spectral leakage
|
||||||
|
pub fn apply_windowing(&self, csi_data: &CsiData) -> Result<CsiData, CsiProcessorError>;
|
||||||
|
|
||||||
|
/// Normalize amplitude to unit variance
|
||||||
|
pub fn normalize_amplitude(&self, csi_data: &CsiData) -> Result<CsiData, CsiProcessorError>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PhaseSanitizer
|
||||||
|
|
||||||
|
Full phase cleaning pipeline: unwrap -> outlier removal -> smoothing -> noise filtering.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct PhaseSanitizer {
|
||||||
|
config: PhaseSanitizerConfig,
|
||||||
|
statistics: SanitizationStatistics,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PhaseSanitizer {
|
||||||
|
/// Complete sanitization pipeline (all four stages)
|
||||||
|
pub fn sanitize_phase(
|
||||||
|
&mut self,
|
||||||
|
phase_data: &Array2<f64>,
|
||||||
|
) -> Result<Array2<f64>, PhaseSanitizationError>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### FresnelBreathingEstimator
|
||||||
|
|
||||||
|
Physics-based breathing detection using Fresnel zone geometry.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct FresnelBreathingEstimator {
|
||||||
|
geometry: FresnelGeometry,
|
||||||
|
min_displacement: f64, // 3mm default
|
||||||
|
max_displacement: f64, // 15mm default
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FresnelBreathingEstimator {
|
||||||
|
/// Check if amplitude variation matches Fresnel breathing model
|
||||||
|
pub fn breathing_confidence(&self, observed_amplitude_variation: f64) -> f64;
|
||||||
|
|
||||||
|
/// Estimate breathing rate via autocorrelation + Fresnel validation
|
||||||
|
pub fn estimate_breathing_rate(
|
||||||
|
&self,
|
||||||
|
amplitude_signal: &[f64],
|
||||||
|
sample_rate: f64,
|
||||||
|
) -> Result<BreathingEstimate, FresnelError>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context Map
|
||||||
|
|
||||||
|
```
|
||||||
|
+--------------------------------------------------------------+
|
||||||
|
| Signal Processing System |
|
||||||
|
+--------------------------------------------------------------+
|
||||||
|
| |
|
||||||
|
| +----------------+ Published +------------------+ |
|
||||||
|
| | CSI | Language | Feature | |
|
||||||
|
| | Preprocessing |------------>| Extraction | |
|
||||||
|
| | Context | CsiData | Context | |
|
||||||
|
| +-------+--------+ +--------+---------+ |
|
||||||
|
| | | |
|
||||||
|
| | Publishes | Publishes |
|
||||||
|
| | CleanedCsiData | CsiFeatures |
|
||||||
|
| v v |
|
||||||
|
| +-------+-------------------------------+---------+ |
|
||||||
|
| | Event Bus (Domain Events) | |
|
||||||
|
| +---------------------------+---------------------+ |
|
||||||
|
| | |
|
||||||
|
| | Subscribes |
|
||||||
|
| v |
|
||||||
|
| +---------+---------+ |
|
||||||
|
| | Motion | |
|
||||||
|
| | Analysis | |
|
||||||
|
| | Context | |
|
||||||
|
| +-------------------+ |
|
||||||
|
| |
|
||||||
|
+---------------------------------------------------------------+
|
||||||
|
| DOWNSTREAM (Customer/Supplier) |
|
||||||
|
| +-----------------+ +------------------+ +--------------+ |
|
||||||
|
| | wifi-densepose | | wifi-densepose | |wifi-densepose| |
|
||||||
|
| | -nn | | -mat | | -train | |
|
||||||
|
| | (consumes | | (consumes | |(consumes | |
|
||||||
|
| | CsiFeatures, | | BreathingEst, | | CsiFeatures) | |
|
||||||
|
| | Spectrogram) | | MotionScore) | | | |
|
||||||
|
| +-----------------+ +------------------+ +--------------+ |
|
||||||
|
+---------------------------------------------------------------+
|
||||||
|
| UPSTREAM (Conformist) |
|
||||||
|
| +-----------------+ +------------------+ |
|
||||||
|
| | wifi-densepose | | wifi-densepose | |
|
||||||
|
| | -core | | -hardware | |
|
||||||
|
| | (CsiFrame | | (ESP32 raw CSI | |
|
||||||
|
| | primitives) | | data ingestion) | |
|
||||||
|
| +-----------------+ +------------------+ |
|
||||||
|
+---------------------------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
**Relationship Types**:
|
||||||
|
- Preprocessing -> Feature Extraction: **Published Language** (CsiData is the shared contract)
|
||||||
|
- Preprocessing -> Motion Analysis: **Customer/Supplier** (Preprocessing supplies cleaned data)
|
||||||
|
- Feature Extraction -> Motion Analysis: **Customer/Supplier** (Features supplies CsiFeatures)
|
||||||
|
- Signal -> wifi-densepose-nn: **Customer/Supplier** (Signal publishes Spectrogram, BVP)
|
||||||
|
- Signal -> wifi-densepose-mat: **Customer/Supplier** (Signal publishes BreathingEstimate, MotionScore)
|
||||||
|
- Signal <- wifi-densepose-core: **Conformist** (Signal adapts to core CsiFrame types)
|
||||||
|
- Signal <- wifi-densepose-hardware: **Conformist** (Signal adapts to raw ESP32 CSI format)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anti-Corruption Layers
|
||||||
|
|
||||||
|
### Hardware ACL (Upstream)
|
||||||
|
|
||||||
|
Translates raw ESP32 CSI packets into the signal crate's `CsiData` value object, normalizing hardware-specific quirks (LLTF/HT-LTF format differences, antenna mapping, null subcarrier handling).
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Normalizes vendor-specific CSI frames to canonical CsiData
|
||||||
|
pub struct HardwareNormalizer {
|
||||||
|
hardware_type: HardwareType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HardwareNormalizer {
|
||||||
|
/// Convert raw hardware bytes to canonical CsiData
|
||||||
|
pub fn normalize(
|
||||||
|
&self,
|
||||||
|
raw_csi: &[u8],
|
||||||
|
hardware_type: HardwareType,
|
||||||
|
) -> Result<CanonicalCsiFrame, HardwareNormError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum HardwareType {
|
||||||
|
Esp32S3,
|
||||||
|
Intel5300,
|
||||||
|
AtherosAr9580,
|
||||||
|
Simulation,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Neural Network ACL (Downstream)
|
||||||
|
|
||||||
|
Adapts signal processing outputs (Spectrogram, BVP, CsiFeatures) into tensor formats expected by the `wifi-densepose-nn` crate. This boundary prevents neural network model details from leaking into the signal processing domain.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Adapts signal crate types to neural network tensor format
|
||||||
|
pub struct SignalToTensorAdapter;
|
||||||
|
|
||||||
|
impl SignalToTensorAdapter {
|
||||||
|
/// Convert Spectrogram to CNN-ready 2D tensor
|
||||||
|
pub fn spectrogram_to_tensor(spec: &Spectrogram) -> Array2<f32> {
|
||||||
|
spec.data.mapv(|v| v as f32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert BVP to domain-independent velocity tensor
|
||||||
|
pub fn bvp_to_tensor(bvp: &BodyVelocityProfile) -> Array2<f32> {
|
||||||
|
bvp.data.mapv(|v| v as f32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert selected subcarrier data to reduced-dimension input
|
||||||
|
pub fn selected_csi_to_tensor(
|
||||||
|
selection: &SubcarrierSelection,
|
||||||
|
data: &Array2<f64>,
|
||||||
|
) -> Result<Array2<f32>, SelectionError> {
|
||||||
|
let extracted = extract_selected(data, selection)?;
|
||||||
|
Ok(extracted.mapv(|v| v as f32))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### MAT ACL (Downstream)
|
||||||
|
|
||||||
|
Adapts motion analysis outputs for the Mass Casualty Assessment Tool, translating domain-generic motion scores and breathing estimates into disaster-context vital signs.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Adapts signal processing outputs for disaster assessment
|
||||||
|
pub struct SignalToMatAdapter;
|
||||||
|
|
||||||
|
impl SignalToMatAdapter {
|
||||||
|
/// Convert BreathingEstimate to MAT-domain BreathingPattern
|
||||||
|
pub fn to_breathing_pattern(est: &BreathingEstimate) -> BreathingPattern {
|
||||||
|
BreathingPattern {
|
||||||
|
rate_bpm: est.rate_bpm as f32,
|
||||||
|
amplitude: est.amplitude_variation as f32,
|
||||||
|
regularity: est.autocorrelation_peak as f32,
|
||||||
|
pattern_type: classify_breathing_type(est.rate_bpm),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert MotionScore to MAT-domain presence indicator
|
||||||
|
pub fn to_presence_indicator(score: &MotionScore) -> PresenceIndicator {
|
||||||
|
PresenceIndicator {
|
||||||
|
detected: score.total > 0.3,
|
||||||
|
confidence: score.total,
|
||||||
|
motion_level: classify_motion_level(score),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue