This commit is contained in:
ruvnet
2026-06-02 15:46:25 +00:00
parent e3c245b45b
commit 62f74a1ea3
340 changed files with 119915 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
# Domain Models
This folder contains Domain-Driven Design (DDD) specifications for each major subsystem in RuView.
DDD organizes the codebase around the problem being solved — not around technical layers. Each *bounded context* owns its own data, rules, and language. Contexts communicate through domain events, not by sharing mutable state. This makes the system easier to reason about, test, and extend — whether you're a person or an AI agent.
## Models
| 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 |
| [Signal Processing](signal-processing-domain-model.md) | SOTA signal processing: phase cleaning, feature extraction, motion analysis | 3 contexts: CSI Preprocessing, Feature Extraction, Motion Analysis |
| [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 |
| [rvCSI](rvcsi-domain-model.md) | Edge RF sensing runtime: multi-source CSI ingestion, validation, normalization, event extraction, RuVector RF memory, agent/MCP integration | 7 contexts: Capture, Validation, Signal, Calibration, Event, Memory, Agent |
## How to read these
Each model defines:
- **Ubiquitous Language** — Terms with precise meanings used in both code and conversation
- **Bounded Contexts** — Independent subsystems with clear responsibilities and boundaries
- **Aggregates** — Clusters of objects that enforce business rules (e.g., a PoseTrack owns its keypoints)
- **Value Objects** — Immutable data with meaning (e.g., a CoherenceScore is not just a float)
- **Domain Events** — Things that happened that other contexts may care about
- **Invariants** — Rules that must always be true (e.g., "drift alert requires >2sigma for >3 days")
- **Anti-Corruption Layers** — Adapters that translate between contexts without leaking internals
## Related
- [Architecture Decision Records](../adr/README.md) — Why each technical choice was made
- [User Guide](../user-guide.md) — Setup and API reference
+926
View File
@@ -0,0 +1,926 @@
# Coherent Human Channel Imaging (CHCI) Domain Model
## Domain-Driven Design Specification
### Ubiquitous Language
| Term | Definition |
|------|------------|
| **Coherent Human Channel Imaging (CHCI)** | A purpose-built RF sensing protocol that uses phase-locked sounding, multi-band fusion, and cognitive waveform adaptation to reconstruct human body surfaces and physiological motion at sub-millimeter resolution |
| **Sounding Frame** | A deterministic OFDM transmission (NDP or custom burst) with known pilot structure, transmitted at fixed cadence for channel measurement — as opposed to passive CSI extracted from data traffic |
| **Phase Coherence** | The property of multiple radio nodes sharing a common phase reference, enabling complex-valued channel measurements without per-node LO drift correction |
| **Reference Clock** | A shared oscillator (TCXO + PLL) distributed to all CHCI nodes via coaxial cable, providing both 40 MHz timing reference and in-band phase reference signal |
| **Cognitive Waveform** | A sounding waveform whose parameters (cadence, bandwidth, band selection, power, subcarrier subset) adapt in real-time based on the current scene state inferred from the body model |
| **Diffraction Tomography** | Coherent reconstruction of body surface geometry from complex-valued channel responses across multiple node pairs and frequency bands — produces surface contours rather than volumetric opacity |
| **Sensing Mode** | One of six operational states (IDLE, ALERT, ACTIVE, VITAL, GESTURE, SLEEP) that determine waveform parameters and processing pipeline configuration |
| **Micro-Burst** | A very short (420 μs) deterministic OFDM symbol transmitted at high cadence (15 kHz) for maximizing Doppler resolution without full 802.11 frame overhead |
| **Multi-Band Fusion** | Simultaneous sounding at 2.4 GHz and 5 GHz (optionally 6 GHz), fused as projections of the same latent motion field using body model priors as constraints |
| **Displacement Floor** | The minimum detectable surface displacement at a given range, determined by phase noise, coherent averaging depth, and antenna count: δ_min = λ/(4π) × σ_φ/√(N_ant × N_avg) |
| **Channel Contrast** | The ratio of complex channel response with human present to the empty-room reference response — the input to diffraction tomography |
| **Coherence Delta** | The change in phase coherence metric between consecutive observation windows — the trigger signal for cognitive waveform transitions |
| **NDP** | Null Data PPDU — an 802.11bf-standard sounding frame containing only preamble and training fields, no data payload |
| **Sensing Availability Window (SAW)** | An 802.11bf-defined time interval during which NDP sounding exchanges are permitted between sensing initiator and responder |
| **Body Model Prior** | Geometric constraints derived from known human body dimensions (segment lengths, joint angle limits) used to regularize cross-band fusion and tomographic reconstruction |
| **Phase Reference Signal** | A continuous-wave tone at the operating band center frequency, distributed alongside the 40 MHz clock, enabling all nodes to measure and compensate residual phase offset |
---
## Bounded Contexts
### 1. Waveform Generation Context
**Responsibility**: Generating, scheduling, and transmitting deterministic sounding waveforms across all CHCI nodes.
```
┌──────────────────────────────────────────────────────────────┐
│ Waveform Generation Context │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌──────────────┐ │
│ │ NDP Sounding │ │ Micro-Burst │ │ Chirp │ │
│ │ Generator │ │ Generator │ │ Generator │ │
│ │ (802.11bf) │ │ (Custom OFDM) │ │ (Multi-BW) │ │
│ └───────┬───────┘ └───────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └────────────┬───────┴────────────────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Sounding │ │
│ │ Scheduler │ ← Cadence, band, power from │
│ │ (Aggregate Root) │ Cognitive Engine │
│ └────────┬─────────┘ │
│ │ │
│ ┌──────────┴──────────┐ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ TX Chain │ │ TX Chain │ │
│ │ (2.4 GHz) │ │ (5 GHz) │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ Events emitted: │
│ SoundingFrameTransmitted { band, timestamp, seq_id } │
│ BurstSequenceCompleted { burst_count, duration } │
│ WaveformConfigChanged { old_mode, new_mode } │
│ │
└──────────────────────────────────────────────────────────────┘
```
**Aggregates:**
- `SoundingScheduler` (Aggregate Root) — Orchestrates sounding frame transmission across nodes and bands according to the current waveform configuration
**Entities:**
- `SoundingFrame` — A single NDP or micro-burst transmission with sequence ID, band, timestamp, and pilot structure
- `BurstSequence` — An ordered set of micro-bursts within one observation window, used for coherent Doppler integration
- `WaveformConfig` — The current waveform parameter set (cadence, bandwidth, band selection, power level, subcarrier mask)
**Value Objects:**
- `SoundingCadence` — Transmission rate in Hz (15000), constrained by regulatory duty cycle limits
- `BandSelection` — Set of active bands {2.4 GHz, 5 GHz, 6 GHz} for current mode
- `SubcarrierMask` — Bit vector selecting active subcarriers for focused sensing (vital mode uses optimal subset)
- `BurstDuration` — Single burst length in microseconds (420 μs)
- `DutyCycle` — Computed duty cycle percentage, must not exceed regulatory limit (ETSI: 10 ms max burst)
**Domain Services:**
- `RegulatoryComplianceChecker` — Validates that any waveform configuration satisfies FCC Part 15.247 and ETSI EN 300 328 constraints before applying
- `BandCoordinator` — Manages time-division or simultaneous multi-band sounding to avoid self-interference
---
### 2. Clock Synchronization Context
**Responsibility**: Distributing and maintaining phase-coherent timing across all CHCI nodes in the sensing mesh.
```
┌──────────────────────────────────────────────────────────────┐
│ Clock Synchronization Context │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ │
│ │ Reference │ │
│ │ Clock Module │ ← TCXO (40 MHz, ±0.5 ppm) │
│ │ (Aggregate │ │
│ │ Root) │ │
│ └───────┬────────┘ │
│ │ │
│ ┌───────┴────────┐ │
│ │ PLL Synthesizer│ ← SI5351A: generates 40 MHz clock │
│ │ │ + 2.4/5 GHz CW phase reference │
│ └───────┬────────┘ │
│ │ │
│ ┌─────┼─────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │Node1│ │Node2│ ... │NodeN│ │
│ │Phase│ │Phase│ │Phase│ │
│ │Lock │ │Lock │ │Lock │ │
│ └──┬──┘ └──┬──┘ └──┬──┘ │
│ │ │ │ │
│ └───────┼──────────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Phase Calibration │ ← Measures residual offset │
│ │ Service │ per node at startup │
│ └──────────────────┘ │
│ │
│ Events emitted: │
│ ClockLockAcquired { node_id, offset_ppm } │
│ PhaseDriftDetected { node_id, drift_deg_per_min } │
│ CalibrationCompleted { residual_offsets: Vec<f64> } │
│ │
└──────────────────────────────────────────────────────────────┘
```
**Aggregates:**
- `ReferenceClockModule` (Aggregate Root) — The single source of timing truth for the entire CHCI mesh
**Entities:**
- `NodePhaseLock` — Per-node state tracking lock status, residual offset, and drift rate
- `CalibrationSession` — A timed procedure that measures and records per-node phase offsets under static conditions
**Value Objects:**
- `PhaseOffset` — Residual phase offset in degrees after clock distribution, per node per subcarrier
- `DriftRate` — Phase drift in degrees per minute, must remain below threshold (0.05°/min for heartbeat sensing)
- `LockStatus` — Enum {Acquiring, Locked, Drifting, Lost} indicating current synchronization state
**Domain Services:**
- `PhaseCalibrationService` — Runs startup and periodic calibration routines; replaces statistical LO estimation in current `phase_align.rs`
- `DriftMonitor` — Continuous background service that detects when any node exceeds drift threshold and triggers recalibration
**Invariants:**
- All nodes must achieve `Locked` status before CHCI sensing begins
- Phase variance per subcarrier must remain ≤ 0.5° RMS over any 10-minute window
- If any node transitions to `Lost`, system falls back to statistical phase correction (legacy mode)
---
### 3. Coherent Signal Processing Context
**Responsibility**: Processing raw coherent CSI into body-surface representations using diffraction tomography and multi-band fusion.
```
┌──────────────────────────────────────────────────────────────────┐
│ Coherent Signal Processing Context │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌──────────────────┐ │
│ │ Coherent CSI │ │ Reference │ │ Calibration │ │
│ │ Stream │ │ Channel │ │ Store │ │
│ │ (per node │ │ (empty room) │ │ (per deployment) │ │
│ │ per band) │ │ │ │ │ │
│ └───────┬───────┘ └───────┬───────┘ └────────┬─────────┘ │
│ │ │ │ │
│ └────────────┬───────┴─────────────────────┘ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ Channel Contrast │ │
│ │ Computer │ │
│ │ H_c = H_meas / H_ref │ │
│ └───────────┬───────────┘ │
│ │ │
│ ┌──────────┴──────────┐ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Diffraction │ │ Multi-Band │ │
│ │ Tomography │ │ Coherent Fusion │ │
│ │ Engine │ │ │ │
│ │ (Aggregate Root) │ │ Body model priors │ │
│ │ │ │ as soft │ │
│ │ Complex │ │ constraints │ │
│ │ permittivity │ │ │ │
│ │ contrast per │ │ Cross-band phase │ │
│ │ voxel │ │ alignment │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
│ └──────────┬──────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Body Surface │──▶ DensePose UV Mapping │
│ │ Reconstruction │ │
│ └──────────────────┘ │
│ │
│ Events emitted: │
│ VoxelGridUpdated { grid_dims, resolution_cm, timestamp } │
│ BodySurfaceReconstructed { n_vertices, confidence } │
│ CoherenceDegradation { node_id, band, severity } │
│ │
└──────────────────────────────────────────────────────────────────┘
```
**Aggregates:**
- `DiffractionTomographyEngine` (Aggregate Root) — Reconstructs 3D body surface geometry from coherent channel contrast measurements across all node pairs and frequency bands
**Entities:**
- `CoherentCsiFrame` — A single coherent channel measurement: complex-valued H(f) per subcarrier, with phase-lock metadata, node ID, band, sequence ID, and timestamp
- `ReferenceChannel` — The empty-room complex channel response per link per band, used as the denominator in channel contrast computation
- `VoxelGrid` — 3D grid of complex permittivity contrast values, the output of diffraction tomography
- `BodySurface` — Extracted iso-surface from voxel grid, represented as triangulated mesh or point cloud
**Value Objects:**
- `ChannelContrast` — Complex ratio H_measured/H_reference per subcarrier per link — the fundamental input to tomography
- `SubcarrierResponse` — Complex-valued (amplitude + phase) channel response at a single subcarrier frequency
- `VoxelCoordinate` — (x, y, z) position in room coordinate frame with associated complex permittivity value
- `SurfaceNormal` — Orientation vector at each surface vertex, derived from permittivity gradient
- `CoherenceMetric` — Complex-valued coherence score (magnitude + phase) replacing the current real-valued Z-score
**Domain Services:**
- `ChannelContrastComputer` — Divides measured channel by reference to isolate human-induced perturbation
- `MultiBandFuser` — Aligns phase across bands using body model priors and combines into unified spectral response
- `SurfaceExtractor` — Applies marching cubes or similar iso-surface algorithm to permittivity contrast grid
**RuVector Integration:**
- `ruvector-attention` → Cross-band attention weights for frequency fusion (extends `CrossViewpointAttention`)
- `ruvector-solver` → Sparse reconstruction for under-determined tomographic inversions
- `ruvector-temporal-tensor` → Temporal coherence of surface reconstructions across frames
---
### 4. Cognitive Waveform Context
**Responsibility**: Adapting the sensing waveform in real-time based on scene state, optimizing the tradeoff between sensing fidelity and power consumption.
```
┌──────────────────────────────────────────────────────────────┐
│ Cognitive Waveform Context │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Scene State Observer │ │
│ │ │ │
│ │ Body Model ──▶ ┌──────────────┐ │ │
│ │ │ Coherence │ │ │
│ │ Coherence ──▶│ Delta │──▶ Mode Transition │ │
│ │ Metrics │ Analyzer │ Signal │ │
│ │ └──────────────┘ │ │
│ │ Motion ──▶ │ │
│ │ Classifier │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ Sensing Mode │ │
│ │ State Machine │ │
│ │ (Aggregate Root) │ │
│ │ │ │
│ │ IDLE ──▶ ALERT ──▶ ACTIVE │
│ │ ╱ │ ╲ │
│ │ VITAL GESTURE SLEEP │
│ │ │
│ └───────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ Waveform Parameter │ │
│ │ Computer │ │
│ │ │──▶ WaveformConfig │
│ │ Mode → {cadence, │ (to Waveform │
│ │ bandwidth, bands, │ Generation Context) │
│ │ power, subcarriers} │ │
│ └───────────────────────┘ │
│ │
│ Events emitted: │
│ SensingModeChanged { from, to, trigger_reason } │
│ PowerBudgetAdjusted { new_budget_mw, mode } │
│ SubcarrierSubsetOptimized { selected: Vec<u16>, criterion }│
│ │
└──────────────────────────────────────────────────────────────┘
```
**Aggregates:**
- `SensingModeStateMachine` (Aggregate Root) — Manages transitions between six sensing modes based on coherence delta, motion classification, and body model state
**Entities:**
- `SensingMode` — One of {IDLE, ALERT, ACTIVE, VITAL, GESTURE, SLEEP} with associated waveform parameter set
- `ModeTransition` — A state change event with trigger reason, timestamp, and hysteresis counter
- `PowerBudget` — Per-mode power allocation constraining cadence and TX power
**Value Objects:**
- `CoherenceDelta` — Magnitude of coherence change between consecutive observation windows — the primary mode transition trigger
- `MotionClassification` — Enum {Static, Breathing, Walking, Gesturing, Falling} derived from micro-Doppler signature
- `ModeHysteresis` — Counter preventing rapid mode oscillation: requires N consecutive trigger events before transition (default N=3)
- `OptimalSubcarrierSet` — The subset of subcarriers with highest SNR for vital sign extraction, computed from recent channel statistics
**Domain Services:**
- `SceneStateObserver` — Fuses body model output, coherence metrics, and motion classifier into a unified scene state descriptor
- `ModeTransitionEvaluator` — Applies hysteresis and priority rules to determine if a mode change should occur
- `SubcarrierSelector` — Identifies optimal subcarrier subset for vital mode using Fisher information criterion or SNR ranking
- `PowerManager` — Computes TX power and duty cycle to stay within regulatory and battery constraints per mode
**Invariants:**
- IDLE mode must be entered after 30 seconds of no detection (configurable)
- Mode transitions must satisfy hysteresis: ≥3 consecutive trigger events
- Power budget must never exceed regulatory limit (20 dBm EIRP at 2.4 GHz)
- Subcarrier subset in VITAL mode must include ≥16 subcarriers for statistical reliability
---
### 5. Displacement Measurement Context
**Responsibility**: Extracting sub-millimeter physiological displacement (breathing, heartbeat, tremor) from coherent phase time series.
```
┌──────────────────────────────────────────────────────────────┐
│ Displacement Measurement Context │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ │
│ │ Phase Time │ ← Coherent CSI phase per subcarrier │
│ │ Series Buffer │ per link, at sounding cadence │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Phase-to- │ │
│ │ Displacement │ │
│ │ Converter │ │
│ │ δ = λΔφ / (4π) │ │
│ └──────┬────────────┘ │
│ │ │
│ ┌──────┴──────────────────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Respiratory │ │ Cardiac │ │
│ │ Analyzer │ │ Analyzer │ │
│ │ (Aggregate Root) │ │ │ │
│ │ │ │ Bandpass: │ │
│ │ Bandpass: │ │ 0.83.0 Hz │ │
│ │ 0.10.6 Hz │ │ (48180 BPM) │ │
│ │ (636 BPM) │ │ │ │
│ │ │ │ Harmonic cancel │ │
│ │ Amplitude: 412mm │ │ (remove respir. │ │
│ │ │ │ harmonics) │ │
│ └────────┬──────────┘ │ │ │
│ │ │ Amplitude: │ │
│ │ │ 0.20.5 mm │ │
│ │ └────────┬─────────┘ │
│ │ │ │
│ └──────────┬───────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Vital Signs │ │
│ │ Fusion │──▶ VitalSignReport │
│ │ (multi-link, │ │
│ │ multi-band) │ │
│ └──────────────────┘ │
│ │
│ Events emitted: │
│ BreathingRateEstimated { bpm, confidence, method } │
│ HeartRateEstimated { bpm, confidence, hrv_ms } │
│ ApneaEventDetected { duration_s, severity } │
│ DisplacementAnomaly { max_displacement_mm, location } │
│ │
└──────────────────────────────────────────────────────────────┘
```
**Aggregates:**
- `RespiratoryAnalyzer` (Aggregate Root) — Extracts breathing rate and pattern from 0.10.6 Hz displacement band
**Entities:**
- `PhaseTimeSeries` — Windowed buffer of unwrapped phase values per subcarrier per link, at sounding cadence
- `DisplacementTimeSeries` — Converted from phase: δ(t) = λΔφ(t) / (4π), represents physical surface displacement in mm
- `VitalSignReport` — Fused output containing breathing rate, heart rate, HRV, confidence scores, and anomaly flags
**Value Objects:**
- `PhaseUnwrapped` — Continuous (unwrapped) phase in radians, free from 2π ambiguity
- `DisplacementSample` — Single displacement value in mm with timestamp and confidence
- `BreathingRate` — BPM value (636 range) with confidence score
- `HeartRate` — BPM value (48180 range) with confidence score and HRV interval
- `ApneaEvent` — Duration, severity, and confidence of detected breathing cessation
**Domain Services:**
- `PhaseUnwrapper` — Continuous phase unwrapping with outlier rejection; critical for displacement conversion
- `RespiratoryHarmonicCanceller` — Removes breathing harmonics from cardiac band to isolate heartbeat signal
- `MultilinkFuser` — Combines displacement estimates across node pairs using SNR-weighted averaging
- `AnomalyDetector` — Flags displacement patterns inconsistent with normal physiology (fall, seizure, cardiac arrest)
**Invariants:**
- Phase unwrapping must maintain continuity: |Δφ| < π between consecutive samples
- Displacement floor must be validated against acceptance metric (AT-2: ≤ 0.1 mm at 2 m)
- Heart rate estimation requires minimum 10 seconds of stable data (cardiac analyzer warmup)
- Multi-link fusion must use ≥2 independent links for confidence scoring
---
### 6. Regulatory Compliance Context
**Responsibility**: Ensuring all CHCI transmissions comply with applicable ISM band regulations across deployment jurisdictions.
```
┌──────────────────────────────────────────────────────────────┐
│ Regulatory Compliance Context │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌──────────────┐ │
│ │ FCC Part 15 │ │ ETSI EN │ │ 802.11bf │ │
│ │ Rules │ │ 300 328 │ │ Compliance │ │
│ │ │ │ │ │ │ │
│ │ - 30 dBm max │ │ - 20 dBm EIRP│ │ - NDP format │ │
│ │ - Digital mod │ │ - LBT or 10ms │ │ - SAW window │ │
│ │ - Spread │ │ burst max │ │ - SMS setup │ │
│ │ spectrum │ │ - Duty cycle │ │ │ │
│ └───────┬───────┘ └───────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └────────────┬───────┴────────────────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Compliance │ │
│ │ Validator │ │
│ │ (Aggregate Root) │ │
│ │ │ │
│ │ Validates every │ │
│ │ WaveformConfig │ │
│ │ before TX │ │
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Jurisdiction │ │
│ │ Registry │ │
│ │ │ │
│ │ US → FCC │ │
│ │ EU → ETSI │ │
│ │ JP → ARIB │ │
│ │ ... │ │
│ └──────────────────┘ │
│ │
│ Events emitted: │
│ ComplianceCheckPassed { jurisdiction, config_hash } │
│ ComplianceViolation { rule, parameter, value, limit } │
│ JurisdictionChanged { from, to } │
│ │
└──────────────────────────────────────────────────────────────┘
```
**Aggregates:**
- `ComplianceValidator` (Aggregate Root) — Gate that must approve every waveform configuration before transmission is permitted
**Entities:**
- `JurisdictionProfile` — Complete set of regulatory constraints for a given region (FCC, ETSI, ARIB, etc.)
- `ComplianceRecord` — Audit trail of compliance checks with timestamps and configuration hashes
**Value Objects:**
- `MaxEIRP` — Maximum effective isotropic radiated power in dBm, per band per jurisdiction
- `MaxBurstDuration` — Maximum continuous transmission time (ETSI: 10 ms)
- `MinIdleTime` — Minimum idle period between bursts
- `ModulationType` — Must be digital modulation (OFDM qualifies) or spread spectrum for FCC
- `DutyCycleLimit` — Maximum percentage of time occupied by transmissions
**Invariants:**
- No transmission shall occur without a passing `ComplianceCheckPassed` event
- Duty cycle must be recalculated and validated on every cadence change
- Jurisdiction must be set during deployment configuration; default is most restrictive (ETSI)
---
## Core Domain Entities
### CoherentCsiFrame (Entity)
```rust
pub struct CoherentCsiFrame {
/// Unique sequence identifier for this sounding frame
seq_id: u64,
/// Node that received this frame
rx_node_id: NodeId,
/// Node that transmitted this frame (known from sounding schedule)
tx_node_id: NodeId,
/// Frequency band: Band2_4GHz, Band5GHz, Band6GHz
band: FrequencyBand,
/// UTC timestamp with microsecond precision
timestamp_us: u64,
/// Complex channel response per subcarrier: (amplitude, phase) pairs
subcarrier_responses: Vec<Complex64>,
/// Phase lock status at time of capture
phase_lock: LockStatus,
/// Residual phase offset from calibration (degrees)
residual_offset_deg: f64,
/// Signal-to-noise ratio estimate (dB)
snr_db: f32,
/// Sounding mode that produced this frame
source_mode: SoundingMode,
}
```
**Invariants:**
- `phase_lock` must be `Locked` for frame to be used in coherent processing
- `subcarrier_responses.len()` must match expected count for `band` and bandwidth (56 for 20 MHz)
- `snr_db` must be ≥ 10 dB for frame to contribute to displacement estimation
- `timestamp_us` must be monotonically increasing per `rx_node_id`
### WaveformConfig (Value Object)
```rust
pub struct WaveformConfig {
/// Active sensing mode
mode: SensingMode,
/// Sounding cadence in Hz
cadence_hz: f64,
/// Active frequency bands
bands: BandSet,
/// Bandwidth per band
bandwidth_mhz: u8,
/// Transmit power in dBm
tx_power_dbm: f32,
/// Subcarrier mask (None = all subcarriers active)
subcarrier_mask: Option<BitVec>,
/// Burst duration in microseconds
burst_duration_us: u16,
/// Number of symbols per burst
symbols_per_burst: u8,
/// Computed duty cycle (must pass compliance check)
duty_cycle_pct: f64,
}
```
**Invariants:**
- `cadence_hz` must be ≥ 1.0 and ≤ 5000.0
- `duty_cycle_pct` must not exceed jurisdiction limit (ETSI: derived from 10 ms burst max)
- `tx_power_dbm` must not exceed jurisdiction max EIRP
- `bandwidth_mhz` must be one of {20, 40, 80}
- `burst_duration_us` must be ≥ 4 (single OFDM symbol + CP)
### SensingMode (Value Object)
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SensingMode {
/// 1 Hz, single band, presence detection only
Idle,
/// 10 Hz, dual band, coarse tracking
Alert,
/// 50-200 Hz, all bands, full DensePose + vitals
Active,
/// 100 Hz, optimal subcarrier subset, breathing + HR + HRV
Vital,
/// 200 Hz, full band, DTW gesture classification
Gesture,
/// 20 Hz, single band, apnea/movement/stage detection
Sleep,
}
impl SensingMode {
pub fn default_config(&self) -> WaveformConfig {
match self {
Self::Idle => WaveformConfig {
mode: *self,
cadence_hz: 1.0,
bands: BandSet::single(Band::Band2_4GHz),
bandwidth_mhz: 20,
tx_power_dbm: 10.0,
subcarrier_mask: None,
burst_duration_us: 4,
symbols_per_burst: 1,
duty_cycle_pct: 0.0004,
},
Self::Alert => WaveformConfig {
mode: *self,
cadence_hz: 10.0,
bands: BandSet::dual(Band::Band2_4GHz, Band::Band5GHz),
bandwidth_mhz: 20,
tx_power_dbm: 15.0,
subcarrier_mask: None,
burst_duration_us: 8,
symbols_per_burst: 2,
duty_cycle_pct: 0.008,
},
Self::Active => WaveformConfig {
mode: *self,
cadence_hz: 100.0,
bands: BandSet::all(),
bandwidth_mhz: 40,
tx_power_dbm: 20.0,
subcarrier_mask: None,
burst_duration_us: 16,
symbols_per_burst: 4,
duty_cycle_pct: 0.16,
},
Self::Vital => WaveformConfig {
mode: *self,
cadence_hz: 100.0,
bands: BandSet::dual(Band::Band2_4GHz, Band::Band5GHz),
bandwidth_mhz: 20,
tx_power_dbm: 18.0,
subcarrier_mask: Some(optimal_vital_subcarriers()),
burst_duration_us: 8,
symbols_per_burst: 2,
duty_cycle_pct: 0.08,
},
Self::Gesture => WaveformConfig {
mode: *self,
cadence_hz: 200.0,
bands: BandSet::all(),
bandwidth_mhz: 40,
tx_power_dbm: 20.0,
subcarrier_mask: None,
burst_duration_us: 16,
symbols_per_burst: 4,
duty_cycle_pct: 0.32,
},
Self::Sleep => WaveformConfig {
mode: *self,
cadence_hz: 20.0,
bands: BandSet::single(Band::Band2_4GHz),
bandwidth_mhz: 20,
tx_power_dbm: 12.0,
subcarrier_mask: None,
burst_duration_us: 4,
symbols_per_burst: 1,
duty_cycle_pct: 0.008,
},
}
}
}
```
### VitalSignReport (Value Object)
```rust
pub struct VitalSignReport {
/// Timestamp of this report
timestamp_us: u64,
/// Breathing rate in BPM (None if not measurable)
breathing_bpm: Option<f64>,
/// Breathing confidence [0.0, 1.0]
breathing_confidence: f64,
/// Heart rate in BPM (None if not measurable — requires CHCI coherent mode)
heart_rate_bpm: Option<f64>,
/// Heart rate confidence [0.0, 1.0]
heart_rate_confidence: f64,
/// Heart rate variability: RMSSD in milliseconds
hrv_rmssd_ms: Option<f64>,
/// Detected anomalies
anomalies: Vec<VitalAnomaly>,
/// Number of independent links contributing to this estimate
contributing_links: u16,
/// Sensing mode that produced this report
source_mode: SensingMode,
}
pub enum VitalAnomaly {
Apnea { duration_s: f64, severity: Severity },
Tachycardia { bpm: f64 },
Bradycardia { bpm: f64 },
IrregularRhythm { irregularity_score: f64 },
FallDetected { impact_g: f64 },
NoMotion { duration_s: f64 },
}
```
### NodeId and FrequencyBand (Value Objects)
```rust
/// Unique identifier for a CHCI node in the sensing mesh
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NodeId(pub u8);
/// Operating frequency band
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrequencyBand {
/// 2.4 GHz ISM band (2400-2483.5 MHz), λ = 12.5 cm
Band2_4GHz,
/// 5 GHz UNII band (5150-5850 MHz), λ = 6.0 cm
Band5GHz,
/// 6 GHz band (5925-7125 MHz), λ = 5.0 cm, WiFi 6E
Band6GHz,
}
impl FrequencyBand {
pub fn wavelength_m(&self) -> f64 {
match self {
Self::Band2_4GHz => 0.125,
Self::Band5GHz => 0.060,
Self::Band6GHz => 0.050,
}
}
/// Displacement per radian of phase change: λ/(4π)
pub fn displacement_per_radian_mm(&self) -> f64 {
self.wavelength_m() * 1000.0 / (4.0 * std::f64::consts::PI)
}
}
```
---
## Domain Events
### Waveform Events
```rust
pub enum WaveformEvent {
/// A sounding frame was transmitted
SoundingFrameTransmitted {
seq_id: u64,
tx_node: NodeId,
band: FrequencyBand,
timestamp_us: u64,
},
/// A burst sequence completed (micro-burst mode)
BurstSequenceCompleted {
burst_count: u32,
total_duration_us: u64,
},
/// Waveform configuration changed (mode transition)
WaveformConfigChanged {
old_mode: SensingMode,
new_mode: SensingMode,
trigger: ModeTransitionTrigger,
},
}
pub enum ModeTransitionTrigger {
CoherenceDeltaThreshold { delta: f64 },
PersonDetected { confidence: f64 },
PersonLost { absence_duration_s: f64 },
PoseClassification { pose: PoseClass },
MotionSpike { magnitude: f64 },
Manual,
}
```
### Clock Events
```rust
pub enum ClockEvent {
/// A node achieved phase lock
ClockLockAcquired {
node_id: NodeId,
residual_offset_deg: f64,
},
/// Phase drift detected on a node
PhaseDriftDetected {
node_id: NodeId,
drift_deg_per_min: f64,
},
/// Phase lock lost on a node — triggers fallback to statistical correction
ClockLockLost {
node_id: NodeId,
reason: LockLossReason,
},
/// Calibration procedure completed
CalibrationCompleted {
residual_offsets: Vec<(NodeId, f64)>,
max_residual_deg: f64,
},
}
```
### Measurement Events
```rust
pub enum MeasurementEvent {
/// Body surface reconstructed from diffraction tomography
BodySurfaceReconstructed {
n_vertices: u32,
resolution_cm: f64,
confidence: f64,
timestamp_us: u64,
},
/// Vital signs estimated
VitalSignsUpdated {
report: VitalSignReport,
},
/// Displacement anomaly detected
DisplacementAnomaly {
max_displacement_mm: f64,
anomaly_type: VitalAnomaly,
},
/// Coherence degradation on a link (may trigger recalibration)
CoherenceDegradation {
tx_node: NodeId,
rx_node: NodeId,
band: FrequencyBand,
severity: Severity,
},
}
```
---
## Context Map
```
┌─────────────────────────────────────────────────────────────────────────┐
│ CHCI Context Map │
│ │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ Waveform │ ◀───── │ Cognitive │ │
│ │ Generation │ config │ Waveform │ │
│ │ Context │ │ Context │ │
│ └───────┬────────┘ └───────▲────────┘ │
│ │ │ │
│ │ sounding │ scene state │
│ │ frames │ feedback │
│ ▼ │ │
│ ┌────────────────┐ ┌───────┴────────┐ │
│ │ Clock │ phase │ Coherent │ │
│ │ Synchro- │ lock ──▶│ Signal │ │
│ │ nization │ status │ Processing │ │
│ │ Context │ │ Context │ │
│ └────────────────┘ └───────┬────────┘ │
│ │ │
│ body surface, │
│ coherence metrics │
│ │ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Displacement │ │
│ │ Measurement │ │
│ │ Context │ │
│ └────────────────┘ │
│ │
│ ┌────────────────┐ │
│ │ Regulatory │ ◀── validates all WaveformConfig before TX │
│ │ Compliance │ │
│ │ Context │ │
│ └────────────────┘ │
│ │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ Integration with existing WiFi-DensePose bounded contexts: │
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ RuvSense │ │ RuVector │ │ DensePose │ │
│ │ Multistatic │ │ Cross-View │ │ Body Model │ │
│ │ (ADR-029) │ │ Fusion │ │ (Core) │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
│ │
│ CHCI Signal Processing feeds directly into existing │
│ RuvSense/RuVector/DensePose pipeline — coherent CSI │
│ replaces incoherent CSI as input, same output interface │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### Anti-Corruption Layers
| Boundary | Direction | Mechanism |
|----------|-----------|-----------|
| CHCI Signal Processing → RuvSense | Downstream | `CoherentCsiFrame` adapts to existing `CsiFrame` trait via `IntoLegacyCsi` adapter — existing pipeline works unmodified |
| Cognitive Waveform → ADR-039 Edge Tiers | Bidirectional | Sensing modes map to edge tiers: IDLE→Tier0, ACTIVE→Tier1, VITAL→Tier2. Shared `EdgeConfig` value object |
| Clock Synchronization → Hardware | Downstream | `ClockDriver` trait abstracts SI5351A hardware specifics; mock implementation for testing |
| Regulatory Compliance → All TX Contexts | Upstream | Compliance Validator acts as a policy gateway — no transmission without passing check |
---
## Integration with Existing Codebase
### Modified Modules
| File | Current | CHCI Change |
|------|---------|-------------|
| `signal/src/ruvsense/phase_align.rs` | Statistical LO offset estimation via circular mean | Add `SharedClockAligner` path: when `phase_lock == Locked`, skip statistical estimation, apply only residual calibration offset |
| `signal/src/ruvsense/multiband.rs` | Independent per-channel fusion | Add `CoherentCrossBandFuser`: phase-aligns across bands using body model priors before fusion |
| `signal/src/ruvsense/coherence.rs` | Z-score coherence scoring (real-valued) | Add `ComplexCoherenceMetric`: phasor-domain coherence using both magnitude and phase information |
| `signal/src/ruvsense/tomography.rs` | Amplitude-only ISTA L1 solver | Add `DiffractionTomographyEngine`: complex-valued reconstruction using channel contrast |
| `signal/src/ruvsense/coherence_gate.rs` | Accept/Reject gate decisions | Add cognitive waveform feedback: gate decisions emit `CoherenceDelta` events to mode state machine |
| `signal/src/ruvsense/multistatic.rs` | Attention-weighted fusion | Add clock synchronization status as fusion weight modifier |
| `hardware/src/esp32/` | TDM protocol, channel hopping | Add NDP sounding mode, reference clock driver, phase reference input |
| `ruvector/src/viewpoint/attention.rs` | CrossViewpointAttention | Extend to cross-band attention with frequency-dependent geometric bias |
### New Crate: `wifi-densepose-chci`
```
wifi-densepose-chci/
├── src/
│ ├── lib.rs # Crate root, re-exports
│ ├── waveform/
│ │ ├── mod.rs
│ │ ├── ndp_generator.rs # 802.11bf NDP sounding frame generation
│ │ ├── burst_generator.rs # Micro-burst OFDM symbol generation
│ │ ├── scheduler.rs # Sounding schedule orchestration
│ │ └── compliance.rs # Regulatory compliance validation
│ ├── clock/
│ │ ├── mod.rs
│ │ ├── reference.rs # Reference clock module abstraction
│ │ ├── pll_driver.rs # SI5351A PLL synthesizer driver
│ │ ├── calibration.rs # Phase calibration procedures
│ │ └── drift_monitor.rs # Continuous drift detection
│ ├── cognitive/
│ │ ├── mod.rs
│ │ ├── mode.rs # SensingMode enum and transitions
│ │ ├── state_machine.rs # Mode state machine with hysteresis
│ │ ├── scene_observer.rs # Scene state fusion from body model + coherence
│ │ ├── subcarrier_select.rs # Optimal subcarrier subset for vital mode
│ │ └── power_manager.rs # Power budget per mode
│ ├── tomography/
│ │ ├── mod.rs
│ │ ├── contrast.rs # Channel contrast computation
│ │ ├── diffraction.rs # Coherent diffraction tomography engine
│ │ └── surface.rs # Iso-surface extraction (marching cubes)
│ ├── displacement/
│ │ ├── mod.rs
│ │ ├── phase_to_disp.rs # Phase-to-displacement conversion
│ │ ├── respiratory.rs # Breathing rate analyzer
│ │ ├── cardiac.rs # Heart rate + HRV analyzer
│ │ └── anomaly.rs # Vital sign anomaly detection
│ └── types.rs # Shared types (NodeId, FrequencyBand, etc.)
├── Cargo.toml
└── tests/
├── integration/
│ ├── acceptance_tests.rs # AT-1 through AT-8
│ └── mode_transitions.rs # Cognitive state machine tests
└── unit/
├── compliance_tests.rs
├── displacement_tests.rs
└── tomography_tests.rs
```
@@ -0,0 +1,648 @@
# Deployment Platform Domain Model
The Deployment Platform domain covers everything from cross-compiling the sensing server for ARM targets to managing TV box appliances running Armbian: provisioning devices, deploying binaries, configuring kiosk displays, and coordinating multi-room installations. It bridges the gap between the Sensing Server domain (which produces the binary) and the physical hardware it runs on.
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.
**Bounded Contexts:**
| # | Context | Responsibility | Key ADRs | Code |
|---|---------|----------------|----------|------|
| 1 | [Appliance Management](#1-appliance-management-context) | Device inventory, provisioning, health monitoring, OTA updates for TV box deployments | [ADR-046](../adr/ADR-046-android-tv-box-armbian-deployment.md) | `scripts/deploy/`, `config/armbian/` |
| 2 | [Cross-Compilation](#2-cross-compilation-context) | Build pipeline for aarch64, binary packaging, CI/CD release artifacts | [ADR-046](../adr/ADR-046-android-tv-box-armbian-deployment.md) | `.github/workflows/`, `Cross.toml` |
| 3 | [Display Kiosk](#3-display-kiosk-context) | HDMI output management, Chromium kiosk mode, screen rotation, auto-start | [ADR-046](../adr/ADR-046-android-tv-box-armbian-deployment.md) | `config/armbian/kiosk/` |
| 4 | [WiFi CSI Bridge](#4-wifi-csi-bridge-context) | Custom WiFi driver CSI extraction, protocol translation to ESP32 binary format | [ADR-046](../adr/ADR-046-android-tv-box-armbian-deployment.md) | `tools/csi-bridge/` |
| 5 | [Network Topology](#5-network-topology-context) | ESP32 mesh ↔ TV box connectivity, dedicated AP mode, multi-room routing | [ADR-046](../adr/ADR-046-android-tv-box-armbian-deployment.md), [ADR-012](../adr/ADR-012-esp32-csi-sensor-mesh.md) | `config/armbian/network/` |
---
## Domain-Driven Design Specification
### Ubiquitous Language
| Term | Definition |
|------|------------|
| **Appliance** | A TV box running Armbian with the sensing server deployed, treated as a managed device in the fleet |
| **Fleet** | The set of all appliances across a multi-room or multi-site installation |
| **Deployment Package** | A self-contained archive containing the sensing-server binary, systemd unit, configuration, and setup script for a target architecture |
| **Kiosk Mode** | Chromium running in full-screen, no-UI mode pointing at `localhost:3000`, auto-started by systemd on HDMI-connected appliances |
| **CSI Bridge** | A userspace daemon that reads CSI data from a patched WiFi driver and re-encodes it as ESP32-compatible UDP frames for the sensing server |
| **Dedicated AP** | An optional `hostapd`-managed WiFi access point on the TV box that creates an isolated network for ESP32 nodes |
| **OTA Update** | Over-the-air binary replacement: download new sensing-server binary, validate checksum, swap via atomic rename, restart service |
| **Reference Device** | A TV box model that has been tested and validated for Armbian + sensing-server deployment (e.g., T95 Max+ / S905X3) |
| **Provisioning** | First-time setup of an appliance: flash Armbian to SD, deploy package, configure WiFi, start services |
| **Health Beacon** | Periodic JSON payload sent by each appliance to a central coordinator (if multi-room) containing uptime, CPU temp, memory usage, inference latency, connected ESP32 count |
---
## Bounded Contexts
### 1. Appliance Management Context
**Responsibility:** Track deployed TV box appliances, provision new devices, monitor health, and coordinate OTA updates across the fleet.
```
+------------------------------------------------------------+
| Appliance Management Context |
+------------------------------------------------------------+
| |
| +----------------+ +----------------+ |
| | Device | | Provisioning | |
| | Registry | | Service | |
| | (fleet state) | | (first-time | |
| | | | setup) | |
| +-------+--------+ +-------+--------+ |
| | | |
| +----------+----------+ |
| v |
| +-------------------+ |
| | Health Monitor | |
| | (beacon receiver,| |
| | thermal alerts, | |
| | connectivity) | |
| +--------+----------+ |
| v |
| +-------------------+ |
| | OTA Updater | |
| | (binary swap, | |
| | rollback, | |
| | checksum verify)| |
| +-------------------+ |
| |
+------------------------------------------------------------+
```
**Aggregates:**
```rust
/// Aggregate Root: A managed TV box appliance in the fleet.
/// Identified by MAC address of the primary Ethernet interface.
pub struct Appliance {
/// Unique device identifier (Ethernet MAC address).
pub device_id: DeviceId,
/// Human-readable name (e.g., "living-room", "bedroom-1").
pub name: String,
/// Hardware model (e.g., "T95 Max+ S905X3").
pub hardware_model: HardwareModel,
/// Current deployment state.
pub state: ApplianceState,
/// Installed sensing-server version.
pub server_version: SemanticVersion,
/// Network configuration.
pub network: NetworkConfig,
/// Last received health beacon.
pub last_health: Option<HealthBeacon>,
/// Provisioning timestamp.
pub provisioned_at: DateTime<Utc>,
/// Connected ESP32 node IDs (from last beacon).
pub connected_nodes: Vec<u8>,
}
/// Lifecycle states for an appliance.
pub enum ApplianceState {
/// SD card prepared, not yet booted.
Provisioned,
/// Booted and running, health beacons received.
Online,
/// No health beacon for >5 minutes.
Unreachable,
/// OTA update in progress.
Updating,
/// Manual maintenance / stopped.
Offline,
/// Thermal throttling or hardware issue detected.
Degraded,
}
```
**Value Objects:**
```rust
/// Hardware model specification for a TV box.
pub struct HardwareModel {
/// Marketing name (e.g., "T95 Max+").
pub name: String,
/// SoC identifier (e.g., "Amlogic S905X3").
pub soc: String,
/// WiFi chipset (e.g., "RTL8822CS").
pub wifi_chipset: String,
/// Total RAM in MB.
pub ram_mb: u32,
/// eMMC storage in GB.
pub emmc_gb: u32,
/// Whether CSI bridge is supported for this WiFi chipset.
pub csi_bridge_supported: bool,
/// Armbian device tree name (e.g., "meson-sm1-sei610").
pub armbian_dtb: String,
}
/// Periodic health report from an appliance.
pub struct HealthBeacon {
pub device_id: DeviceId,
pub timestamp: DateTime<Utc>,
pub uptime_secs: u64,
pub cpu_temp_celsius: f32,
pub cpu_usage_percent: f32,
pub memory_used_mb: u32,
pub memory_total_mb: u32,
pub disk_used_percent: f32,
pub inference_latency_ms: f32,
pub connected_esp32_nodes: Vec<u8>,
pub server_version: SemanticVersion,
pub csi_frames_per_sec: f32,
pub websocket_clients: u32,
}
/// Network configuration for an appliance.
pub struct NetworkConfig {
/// Primary IP address (Ethernet or WiFi client).
pub ip_address: IpAddr,
/// Whether the appliance runs a dedicated AP for ESP32 nodes.
pub dedicated_ap: Option<DedicatedApConfig>,
/// UDP port for ESP32 CSI reception.
pub csi_udp_port: u16, // default: 5005
/// HTTP port for sensing server.
pub http_port: u16, // default: 3000
}
/// Configuration for a dedicated WiFi AP hosted by the appliance.
pub struct DedicatedApConfig {
/// SSID for the ESP32 mesh network.
pub ssid: String,
/// WPA2 passphrase.
pub passphrase: String,
/// Channel (1-11 for 2.4 GHz).
pub channel: u8,
/// DHCP range for connected ESP32 nodes.
pub dhcp_range: (IpAddr, IpAddr),
}
/// Unique device identifier (Ethernet MAC).
pub struct DeviceId(pub [u8; 6]);
/// Semantic version for tracking installed software.
pub struct SemanticVersion {
pub major: u16,
pub minor: u16,
pub patch: u16,
pub pre: Option<String>,
}
```
**Domain Services:**
- `ProvisioningService` — Generates Armbian SD card image with pre-configured deployment package, WiFi credentials, and systemd units
- `HealthMonitorService` — Listens for UDP health beacons from fleet appliances, triggers alerts on thermal throttling (>80°C), unreachable (>5 min), or high memory usage (>90%)
- `OtaUpdateService` — Downloads new binary from release URL, verifies SHA-256 checksum, performs atomic swap (`rename(new, current)`), restarts systemd service, rolls back if health beacon fails within 60s
**Invariants:**
- Device ID (MAC address) is immutable after provisioning
- OTA update refuses to proceed if current CPU temperature >75°C (thermal headroom)
- Rollback is automatic if no healthy beacon is received within 60 seconds of restart
- Dedicated AP SSID must not match the upstream WiFi SSID
---
### 2. Cross-Compilation Context
**Responsibility:** Build the sensing-server binary for ARM64 targets, package deployment archives, and manage CI/CD release artifacts.
```
+------------------------------------------------------------+
| Cross-Compilation Context |
+------------------------------------------------------------+
| |
| +----------------+ +----------------+ |
| | Cross.toml | | GitHub Actions| |
| | (target cfg) | | CI Matrix | |
| +-------+--------+ +-------+--------+ |
| | | |
| +----------+----------+ |
| v |
| +-------------------+ |
| | Build Pipeline | |
| | (cross build | |
| | --target | |
| | aarch64-unknown-| |
| | linux-gnu) | |
| +--------+----------+ |
| v |
| +-------------------+ |
| | Binary Packager | |
| | (strip, compress,|---> .tar.gz artifact |
| | bundle assets, | |
| | systemd units) | |
| +-------------------+ |
| |
+------------------------------------------------------------+
```
**Value Objects:**
```rust
/// A packaged deployment archive for a target platform.
pub struct DeploymentPackage {
/// Target triple (e.g., "aarch64-unknown-linux-gnu").
pub target: String,
/// Sensing server binary (stripped).
pub binary: PathBuf,
/// Binary size in bytes.
pub binary_size: u64,
/// SHA-256 checksum of the binary.
pub checksum: String,
/// Systemd service unit file.
pub service_unit: String,
/// Static web UI assets directory.
pub ui_assets: PathBuf,
/// Armbian configuration files (kiosk, network, etc.).
pub config_files: Vec<PathBuf>,
/// Setup script (runs on first boot).
pub setup_script: PathBuf,
/// Version being packaged.
pub version: SemanticVersion,
}
/// Build target specification.
pub struct BuildTarget {
/// Rust target triple.
pub triple: String,
/// CPU architecture description.
pub arch: String,
/// Whether NEON SIMD is available.
pub has_neon: bool,
/// Cross-compilation Docker image.
pub cross_image: String,
/// Binary size limit in bytes.
pub size_limit: u64,
}
```
**Supported Targets:**
| Target Triple | Architecture | Use Case | Size Limit |
|---------------|-------------|----------|------------|
| `x86_64-unknown-linux-gnu` | x86-64 | PC/laptop (existing) | 30 MB |
| `aarch64-unknown-linux-gnu` | ARM64 | TV box (Armbian) | 15 MB |
| `armv7-unknown-linux-gnueabihf` | ARMv7 | Older TV boxes (32-bit) | 12 MB |
| `x86_64-pc-windows-msvc` | x86-64 | Windows (existing) | 30 MB |
**Invariants:**
- Stripped binary must be under size limit for target
- SHA-256 checksum is computed and included in every deployment package
- UI assets are embedded in binary via `include_dir!` or bundled alongside
- No native GPU dependencies — CPU-only inference (candle or ONNX Runtime)
---
### 3. Display Kiosk Context
**Responsibility:** Manage HDMI output on TV box appliances, running Chromium in kiosk mode to display the sensing dashboard full-screen on boot.
```
+------------------------------------------------------------+
| Display Kiosk Context |
+------------------------------------------------------------+
| |
| +----------------+ +----------------+ |
| | systemd | | Chromium | |
| | autologin + | | Kiosk Launch | |
| | X11/Wayland | | (full-screen, | |
| | session | | no-UI bars) | |
| +-------+--------+ +-------+--------+ |
| | | |
| +----------+----------+ |
| v |
| +-------------------+ |
| | Display Manager | |
| | (resolution, | |
| | rotation, | |
| | overscan, | |
| | sleep/wake) | |
| +-------------------+ |
| |
+------------------------------------------------------------+
```
**Value Objects:**
```rust
/// Display configuration for kiosk mode.
pub struct KioskConfig {
/// URL to display (default: "http://localhost:3000").
pub url: String,
/// Screen rotation in degrees (0, 90, 180, 270).
pub rotation: u16,
/// Whether to hide the mouse cursor.
pub hide_cursor: bool,
/// Auto-refresh interval in seconds (0 = disabled).
pub auto_refresh_secs: u32,
/// Display sleep schedule (e.g., off 23:00-06:00).
pub sleep_schedule: Option<SleepSchedule>,
/// Overscan compensation percentage (0-10).
pub overscan_percent: u8,
}
/// Sleep schedule for display power management.
pub struct SleepSchedule {
/// Time to turn display off (HH:MM local time).
pub sleep_time: String,
/// Time to turn display on (HH:MM local time).
pub wake_time: String,
}
```
**Invariants:**
- Chromium kiosk starts only after sensing-server systemd unit is `active`
- If Chromium crashes, systemd restarts it within 5 seconds (`Restart=always`)
- Display sleep/wake uses CEC commands (HDMI-CEC) to control TV power when available
- No browser UI elements are visible (address bar, scrollbars, etc.)
---
### 4. WiFi CSI Bridge Context
**Responsibility:** Extract CSI data from patched WiFi drivers on the TV box and translate it into ESP32-compatible binary frames for the sensing server. This is the Phase 2 custom firmware path.
```
+------------------------------------------------------------+
| WiFi CSI Bridge Context |
+------------------------------------------------------------+
| |
| +----------------+ +----------------+ |
| | Patched WiFi | | CSI Reader | |
| | Driver | | (Netlink / | |
| | (kernel space)| | procfs / | |
| | CSI hooks | | UDP socket) | |
| +-------+--------+ +-------+--------+ |
| | | |
| +----------+----------+ |
| v |
| +-------------------+ |
| | Protocol | |
| | Translator | |
| | (chipset CSI → | |
| | ESP32 binary | |
| | 0xC5100001) | |
| +--------+----------+ |
| v |
| +-------------------+ |
| | UDP Sender | |
| | (localhost:5005) |---> sensing-server |
| +-------------------+ |
| |
+------------------------------------------------------------+
```
**Value Objects:**
```rust
/// Raw CSI extraction from a WiFi chipset.
pub struct ChipsetCsiFrame {
/// Source chipset type.
pub chipset: WifiChipset,
/// Timestamp of extraction (kernel monotonic clock).
pub timestamp_us: u64,
/// Number of subcarriers (varies by chipset and bandwidth).
pub n_subcarriers: u16,
/// Number of spatial streams / antennas.
pub n_streams: u8,
/// Channel frequency in MHz.
pub freq_mhz: u16,
/// Bandwidth (20/40/80/160 MHz).
pub bandwidth_mhz: u16,
/// RSSI in dBm.
pub rssi_dbm: i8,
/// Noise floor estimate in dBm.
pub noise_floor_dbm: i8,
/// Complex CSI values (I/Q pairs) per subcarrier per stream.
pub csi_matrix: Vec<Complex<f32>>,
/// Source MAC address (BSSID of the AP being measured).
pub source_mac: [u8; 6],
}
/// Supported WiFi chipsets for CSI extraction.
pub enum WifiChipset {
/// Broadcom BCM43455 via Nexmon CSI patches.
BroadcomBcm43455,
/// Realtek RTL8822CS via modified rtw88 driver.
RealtekRtl8822cs,
/// MediaTek MT7661 via mt76 driver modification.
MediatekMt7661,
}
/// Translated frame in ESP32 binary protocol (ADR-018).
pub struct Esp32CompatFrame {
/// Magic: 0xC5100001
pub magic: u32,
/// Virtual node ID assigned to this WiFi interface.
pub node_id: u8,
/// Number of antennas / spatial streams.
pub n_antennas: u8,
/// Number of subcarriers (resampled to match ESP32 format).
pub n_subcarriers: u8,
/// Frequency in MHz.
pub freq_mhz: u16,
/// Sequence number (monotonic counter).
pub sequence: u32,
/// RSSI in dBm.
pub rssi: i8,
/// Noise floor in dBm.
pub noise_floor: i8,
/// Amplitude values (extracted from complex CSI).
pub amplitudes: Vec<f32>,
/// Phase values (extracted from complex CSI).
pub phases: Vec<f32>,
}
```
**Domain Services:**
- `CsiExtractionService` — Reads raw CSI from patched driver via Netlink socket (BCM43455), procfs (RTL8822CS), or UDP (MT7661)
- `SubcarrierResamplerService` — Resamples chipset-specific subcarrier counts to match ESP32 format (e.g., 256 → 128 via decimation or interpolation)
- `ProtocolTranslatorService` — Converts `ChipsetCsiFrame` to `Esp32CompatFrame` with ADR-018 binary encoding
- `CalibrationService` — Compensates for chipset-specific phase offsets, antenna spacing, and gain differences relative to ESP32 CSI
**Invariants:**
- Bridge assigns virtual `node_id` in range 200-254 (reserved for non-ESP32 sources) to avoid collision with physical ESP32 node IDs (1-199)
- Subcarrier resampling preserves frequency ordering (lowest to highest)
- Phase values are unwrapped before encoding (continuous, not wrapped to ±π)
- Bridge daemon starts only if a compatible patched driver is detected at boot
---
### 5. Network Topology Context
**Responsibility:** Manage network connectivity between ESP32 sensor nodes and TV box appliances, including optional dedicated AP mode and multi-room routing.
```
+------------------------------------------------------------+
| Network Topology Context |
+------------------------------------------------------------+
| |
| +----------------+ +----------------+ |
| | hostapd | | DHCP Server | |
| | (dedicated AP | | (dnsmasq for | |
| | for ESP32 | | ESP32 nodes) | |
| | mesh) | | | |
| +-------+--------+ +-------+--------+ |
| | | |
| +----------+----------+ |
| v |
| +-------------------+ |
| | Topology Manager | |
| | (node discovery, | |
| | IP assignment, | |
| | route config) | |
| +--------+----------+ |
| v |
| +-------------------+ |
| | Firewall Rules | |
| | (iptables/nft: | |
| | allow UDP 5005, | |
| | block external | |
| | access to ESP32 | |
| | subnet) | |
| +-------------------+ |
| |
+------------------------------------------------------------+
```
**Value Objects:**
```rust
/// Network topology for a single-room deployment.
pub struct RoomTopology {
/// Appliance acting as the aggregator.
pub appliance: DeviceId,
/// Whether the appliance runs a dedicated AP.
pub dedicated_ap: bool,
/// Connected ESP32 nodes with their assigned IPs.
pub nodes: Vec<EspNodeConnection>,
/// Upstream network interface (Ethernet or WiFi client).
pub uplink_interface: String,
/// Sensing network interface (dedicated AP or same as uplink).
pub sensing_interface: String,
}
/// An ESP32 node's network connection to the appliance.
pub struct EspNodeConnection {
/// ESP32 node ID (from firmware NVS).
pub node_id: u8,
/// MAC address of the ESP32.
pub mac: [u8; 6],
/// Assigned IP address (via DHCP or static).
pub ip: IpAddr,
/// Last CSI frame received timestamp.
pub last_seen: DateTime<Utc>,
/// Average CSI frames per second from this node.
pub fps: f32,
}
```
**Domain Services:**
- `DedicatedApService` — Configures `hostapd` to create a WPA2 AP on the TV box's WiFi interface, assigns DHCP range via `dnsmasq`, sets up IP forwarding
- `NodeDiscoveryService` — Monitors UDP port 5005 for new ESP32 node IDs, registers them in the topology, alerts on node departure (no frames for >30s)
- `FirewallService` — Configures `nftables`/`iptables` to isolate the ESP32 subnet from the upstream LAN, allowing only UDP 5005 inbound and HTTP 3000 outbound
**Invariants:**
- Dedicated AP uses a separate WiFi interface or virtual interface (not the uplink)
- ESP32 subnet is isolated from upstream LAN by default (firewall rules)
- If dedicated AP is disabled, ESP32 nodes must be on the same LAN subnet as the appliance
- Node discovery does not require mDNS or any discovery protocol — ESP32 nodes are configured with the appliance's IP via NVS provisioning (ADR-044)
---
## Domain Events
| Event | Published By | Consumed By | Payload |
|-------|-------------|-------------|---------|
| `ApplianceProvisioned` | Appliance Mgmt | Fleet Dashboard | `{ device_id, name, hardware_model, ip }` |
| `ApplianceOnline` | Appliance Mgmt | Fleet Dashboard | `{ device_id, server_version, uptime }` |
| `ApplianceUnreachable` | Appliance Mgmt | Fleet Dashboard, Alerting | `{ device_id, last_seen, reason }` |
| `ApplianceDegraded` | Appliance Mgmt | Fleet Dashboard, Alerting | `{ device_id, cpu_temp, reason }` |
| `OtaUpdateStarted` | Appliance Mgmt | Fleet Dashboard | `{ device_id, from_version, to_version }` |
| `OtaUpdateCompleted` | Appliance Mgmt | Fleet Dashboard | `{ device_id, new_version, duration_secs }` |
| `OtaUpdateRolledBack` | Appliance Mgmt | Fleet Dashboard, Alerting | `{ device_id, attempted_version, rollback_version, reason }` |
| `BinaryBuilt` | Cross-Compilation | Release Pipeline | `{ target, version, binary_size, checksum }` |
| `DeploymentPackageCreated` | Cross-Compilation | Appliance Mgmt | `{ target, version, package_url }` |
| `KioskStarted` | Display Kiosk | Appliance Mgmt | `{ device_id, url, resolution }` |
| `KioskCrashed` | Display Kiosk | Appliance Mgmt | `{ device_id, exit_code, restart_count }` |
| `CsiBridgeStarted` | WiFi CSI Bridge | Appliance Mgmt, Sensing Server | `{ device_id, chipset, virtual_node_id }` |
| `CsiBridgeFailed` | WiFi CSI Bridge | Appliance Mgmt | `{ device_id, chipset, error }` |
| `EspNodeDiscovered` | Network Topology | Appliance Mgmt | `{ appliance_id, node_id, mac, ip }` |
| `EspNodeLost` | Network Topology | Appliance Mgmt, Alerting | `{ appliance_id, node_id, last_seen }` |
| `DedicatedApStarted` | Network Topology | Appliance Mgmt | `{ appliance_id, ssid, channel }` |
---
## Context Map
```
+-------------------+ +---------------------+
| Appliance |--------->| Fleet Dashboard |
| Management | events | (external UI for |
| (fleet state) | -------> | multi-room mgmt) |
+--------+----------+ +---------------------+
|
| provisions, monitors
v
+-------------------+ +---------------------+
| Cross-Compilation |--------->| GitHub Releases |
| (build pipeline) | uploads | (binary artifacts) |
+-------------------+ +---------------------+
|
| provides binary
v
+-------------------+ +---------------------+
| Display Kiosk |--------->| Sensing Server |
| (Chromium on | loads | (upstream domain, |
| HDMI output) | UI from | produces web UI) |
+-------------------+ +----------+----------+
^
+-------------------+ |
| WiFi CSI Bridge |-----UDP 5005------>|
| (patched driver) | ESP32 compat |
+-------------------+ frames |
|
+-------------------+ |
| Network Topology |-----UDP 5005------>|
| (ESP32 mesh | ESP32 frames |
| connectivity) | |
+-------------------+ |
```
**Relationships:**
| Upstream | Downstream | Relationship | Mechanism |
|----------|-----------|--------------|-----------|
| Cross-Compilation | Appliance Mgmt | Supplier-Consumer | Build produces binary; Appliance Mgmt deploys it |
| Appliance Mgmt | Display Kiosk | Customer-Supplier | Appliance Mgmt starts kiosk after server is healthy |
| WiFi CSI Bridge | Sensing Server (external) | Conformist | Bridge adapts its output to match ESP32 binary protocol (ADR-018) |
| Network Topology | Sensing Server (external) | Shared Kernel | Both depend on UDP port 5005 and ESP32 node ID scheme |
| Appliance Mgmt | Network Topology | Customer-Supplier | Appliance config determines whether dedicated AP is enabled |
---
## Anti-Corruption Layers
### ESP32 Protocol ACL (CSI Bridge)
The WiFi CSI Bridge translates chipset-specific CSI formats (Nexmon, rtw88, mt76) into the ESP32 binary protocol (ADR-018). The sensing server never knows whether frames came from a real ESP32 or a TV box WiFi chipset. Virtual node IDs (200-254) prevent collision with physical ESP32 IDs but are otherwise treated identically by the ingestion context.
### Armbian Platform ACL
Appliance Management abstracts over Armbian specifics (device tree names, boot configuration, dtb overlays) through the `HardwareModel` value object. Higher-level contexts (Cross-Compilation, Display Kiosk) depend only on the target triple (`aarch64-unknown-linux-gnu`) and systemd service interface, not on Amlogic/Allwinner/Rockchip kernel specifics.
### Fleet Coordination ACL
For multi-room deployments, each appliance is self-contained (runs its own sensing server, display, and network). The fleet dashboard reads health beacons but never controls individual appliances directly. OTA updates are pulled by each appliance (not pushed), maintaining the appliance as the authority over its own state.
---
## Related
- [ADR-046: Android TV Box / Armbian Deployment](../adr/ADR-046-android-tv-box-armbian-deployment.md) — Primary architectural decision
- [ADR-012: ESP32 CSI Sensor Mesh](../adr/ADR-012-esp32-csi-sensor-mesh.md) — ESP32 mesh network design
- [ADR-018: Dev Implementation](../adr/ADR-018-dev-implementation.md) — ESP32 binary CSI protocol
- [ADR-039: Edge Intelligence](../adr/ADR-039-esp32-edge-intelligence.md) — On-device processing tiers
- [ADR-044: Provisioning Tool](../adr/ADR-044-provisioning-tool-enhancements.md) — NVS provisioning for ESP32 nodes
- [Hardware Platform Domain Model](hardware-platform-domain-model.md) — Upstream domain (ESP32 hardware)
- [Sensing Server Domain Model](sensing-server-domain-model.md) — Upstream domain (server software)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+395
View File
@@ -0,0 +1,395 @@
# rvCSI — Edge RF Sensing Runtime Domain Model
## Domain-Driven Design Specification
> Companion documents: [rvCSI Platform PRD](../prd/rvcsi-platform-prd.md) · [ADR-095 — rvCSI Edge RF Sensing Platform](../adr/ADR-095-rvcsi-edge-rf-sensing-platform.md)
### Domain
Camera-free RF spatial sensing from WiFi Channel State Information (CSI).
### Core domain
**RF field interpretation.** rvCSI converts noisy radio channel measurements into validated events and temporal embeddings that represent changes in physical space. CSI is treated as a *temporal delta stream* against learned baselines — not as exact vision.
### Supporting subdomains
Hardware adapter management · packet parsing · signal processing · calibration · event extraction · temporal memory · agent integration · replay and audit.
### Generic subdomains
Logging · configuration · CLI parsing · WebSocket streaming · package publishing · dashboard visualization.
---
## Ubiquitous Language
| Term | Definition |
|------|------------|
| **CSI** | Channel State Information — per-subcarrier complex channel response measured by a WiFi receiver |
| **Source** | A physical or replayed producer of CSI frames (a NIC, an ESP32 node, a PCAP file, a recorded capture) |
| **Adapter** | A software module that knows how to receive and decode source-specific CSI and normalize it into a `CsiFrame` |
| **Frame** | One CSI observation at a timestamp — the unit of ingestion |
| **Window** | A bounded sequence of frames from one source/session, used for analysis |
| **Baseline** | The learned normal RF-field state for a space |
| **Delta** | The measured difference of the current field from baseline |
| **Event** | A semantic interpretation of one or more windows (presence started, motion detected, anomaly, …) |
| **Quality score** | Confidence, in [0, 1], that a signal/frame/window is usable |
| **Calibration** | The process of learning a stable baseline for a space |
| **Room signature** | A vector representation of a space under normal conditions |
| **Drift** | Slow movement of the field away from baseline |
| **Anomaly** | A significant, unexplained deviation from baseline |
| **RF memory** | Persisted temporal vectors and events for a physical space (stored in RuVector) |
| **Coherence** | Consistency among sources, windows, and learned baselines |
| **Quarantine** | A holding store for rejected/corrupt frames, kept for audit rather than discarded |
| **Adapter profile** | A capability descriptor for a source (chip, firmware/driver versions, supported channels/bandwidths, expected subcarrier counts, capture/injection/monitor-mode support) |
| **Calibration version** | An immutable identifier for a particular learned baseline; every event references the calibration version it was detected against |
| **Evidence window set** | The set of `WindowId`s an event references as its justification — an event with no evidence is invalid |
---
## Bounded Contexts
```
┌─────────────┐ ┌──────────────┐ ┌────────────┐ ┌──────────────┐
│ Capture │──▶│ Validation │──▶│ Signal │──▶│ Calibration │
│ context │ │ context │ │ context │ │ context │
└─────────────┘ └──────────────┘ └─────┬──────┘ └──────┬───────┘
│ │
▼ │
┌────────────┐ │
│ Event │◀──────────┘
│ context │
└─────┬──────┘
┌─────────────┴─────────────┐
▼ ▼
┌────────────┐ ┌────────────┐
│ Memory │ │ Agent │
│ context │ │ context │
└────────────┘ └────────────┘
```
- **Capture** upstreams raw input from sources.
- **Validation** protects every downstream context — nothing crosses into SDK/DSP/memory/agents unvalidated.
- **Signal** turns frames into windows.
- **Calibration** gives windows a room-specific baseline.
- **Event** converts deltas into meaning.
- **Memory** stores time, similarity, drift, and coherence (RuVector).
- **Agent** exposes safe actions and queries (MCP / TypeScript).
---
### 1. Capture context
**Responsibility:** connect to CSI sources and produce raw frames.
```
┌──────────────────────────────────────────────────────────────┐
│ Capture Context │
├──────────────────────────────────────────────────────────────┤
│ ┌────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Source │ │ CaptureSession │ │ AdapterProfile │ │
│ │ (adapter │ │ (aggregate root)│ │ (capability │ │
│ │ plugin) │ │ │ │ descriptor) │ │
│ └────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
│ CsiSource trait: open · start · next_frame · stop · health │
└──────────────────────────────────────────────────────────────┘
```
| Element | Kind | Notes |
|---------|------|-------|
| `Source` | Entity | A configured adapter instance bound to a device or file |
| `CaptureSession` | Entity / **aggregate root** | Owns exactly one `AdapterProfile` and one runtime configuration |
| `AdapterProfile` | Entity | Chip, firmware/driver versions, supported channels/bandwidths, expected subcarrier counts, capability flags |
| `Channel`, `Bandwidth`, `FirmwareVersion`, `DriverVersion` | Value objects | Immutable |
**Commands:** `StartCapture` · `StopCapture` · `RestartCapture` · `InspectSource`
**Domain events:** `CaptureStarted` · `CaptureStopped` · `SourceDisconnected` · `AdapterUnsupported`
---
### 2. Validation context
**Responsibility:** make frames safe and trustworthy before any language-boundary crossing.
| Element | Kind | Notes |
|---------|------|-------|
| `ValidationPolicy` | Entity | Bounds, monotonicity rules, finiteness checks, quarantine on/off |
| `QuarantineStore` | Entity | Holds rejected/corrupt frames for audit |
| `ValidatedFrame` | **Aggregate root** | The frame once it has passed (or been degraded by) validation |
| `ValidationError`, `QualityScore`, `FrameBounds` | Value objects | `QualityScore` ∈ [0, 1] |
**Commands:** `ValidateFrame` · `QuarantineFrame`
**Domain events:** `FrameAccepted` · `FrameRejected` · `QualityDropped`
---
### 3. Signal context
**Responsibility:** DSP and window features.
```
Frame stream ─▶ SignalPipeline ─▶ WindowBuffer ─▶ CsiWindow
(DC removal, phase unwrap, (mean amplitude,
smoothing, Hampel filter, phase variance,
variance, baseline subtraction, motion energy,
motion energy, presence score) presence/quality scores)
```
| Element | Kind | Notes |
|---------|------|-------|
| `SignalPipeline` | Entity | Ordered DSP stages; reuses `wifi-densepose-signal` primitives |
| `WindowBuffer` | Entity | Accumulates frames into bounded windows |
| `CsiWindow` | **Aggregate root** | Frames from exactly one source/session |
| `AmplitudeVector`, `PhaseVector`, `MotionEnergy`, `PresenceScore` | Value objects | |
**Commands:** `ProcessFrame` · `BuildWindow` · `EstimateBaselineDelta`
**Domain events:** `WindowReady` · `BaselineDeltaMeasured`
---
### 4. Calibration context
**Responsibility:** learn and version the normal RF state and room signatures.
| Element | Kind | Notes |
|---------|------|-------|
| `CalibrationProfile` | **Aggregate root** | Linked to source, room, adapter profile, configuration |
| `RoomSignature` | Entity | Vector representation of a space under normal conditions |
| `BaselineModel` | Entity | Statistical model of the baseline field; carries version history |
| `CalibrationVersion`, `StabilityScore`, `RoomId` | Value objects | Calibration cannot complete if `StabilityScore` < threshold |
**Commands:** `StartCalibration` · `CompleteCalibration` · `UpdateBaseline` · `RejectUnstableCalibration`
**Domain events:** `CalibrationStarted` · `CalibrationCompleted` · `CalibrationFailed` · `BaselineUpdated`
---
### 5. Event context
**Responsibility:** semantic event extraction with confidence and evidence.
| Element | Kind | Notes |
|---------|------|-------|
| `EventDetector` | Entity | One per event family (presence, motion, breathing, anomaly, …) |
| `EventStateMachine` | Entity | Holds the per-source detection state; emits transitions |
| `CsiEvent` | **Aggregate root** | Must reference ≥ 1 evidence window; confidence ∈ [0, 1]; references calibration version |
| `Confidence`, `EvidenceWindowSet`, `EventKind` | Value objects | |
**Commands:** `DetectEvents` · `PublishEvent` · `SuppressEvent`
**Domain events (the `CsiEventKind` enum):** `PresenceStarted` · `PresenceEnded` · `MotionDetected` · `MotionSettled` · `BaselineChanged` · `SignalQualityDropped` · `DeviceDisconnected` · `BreathingCandidate` · `AnomalyDetected` · `CalibrationRequired`
---
### 6. Memory context
**Responsibility:** RuVector storage and retrieval — RF memory.
| Element | Kind | Notes |
|---------|------|-------|
| `RfMemoryCollection` | Entity | A RuVector collection scoped to a deployment |
| `TemporalEmbedding` | Entity | Frame / window / event embedding with timestamp |
| `SensorGraph` | Entity | Graph of sources and their topological relationships |
| `RoomMemory` | **Aggregate root** | Stored embeddings must be traceable to frame windows or event windows |
| `EmbeddingVector`, `DriftScore`, `CoherenceScore` | Value objects | `DriftScore` must include the baseline version |
**Commands:** `StoreWindowEmbedding` · `StoreEventEmbedding` · `QuerySimilarWindows` · `ComputeDrift`
**Domain events:** `EmbeddingStored` · `DriftDetected` · `SimilarPatternFound`
Data stored: frame embeddings · window embeddings · room baseline vectors · event vectors · drift snapshots · sensor-topology graph edges · source health records. Retention policy applies at collection level. No orphan embeddings.
---
### 7. Agent context
**Responsibility:** MCP and TypeScript agent interaction — safe actions and queries.
| Element | Kind | Notes |
|---------|------|-------|
| `AgentSubscription` | Entity | An agent's filtered stream of events |
| `McpToolSession` | Entity | A tool invocation context with permissions |
| `AgentSession` | **Aggregate root** | |
| `ToolPermission`, `EventFilter`, `AgentIntent` | Value objects | `ToolPermission` distinguishes read vs. write-gated |
**Commands:** `SubscribeToEvents` · `RequestStatus` · `RequestCalibration` · `QueryMemory`
**Domain events:** `AgentSubscribed` · `ToolExecuted` · `PermissionDenied`
**MCP tools** (read by default; write-gated marked `*`): `rvcsi_status` · `rvcsi_list_sources` · `rvcsi_start_capture *` · `rvcsi_stop_capture *` · `rvcsi_get_presence` · `rvcsi_get_recent_events` · `rvcsi_calibrate_room *` · `rvcsi_export_window *` · `rvcsi_query_ruvector` · `rvcsi_health_report`.
---
## Context Map
| Upstream → Downstream | Relationship | ACL / contract |
|-----------------------|--------------|----------------|
| Capture → Validation | Customer/Supplier | Raw frames pass through `ValidationPolicy`; only `Accepted`/`Degraded` continue |
| Validation → Signal | Conformist (Signal accepts `ValidatedFrame` as-is) | `CsiFrame` schema is the published language |
| Signal → Calibration | Customer/Supplier | Windows + baseline-delta measurements feed baseline modeling |
| Calibration → Event | Customer/Supplier | Detectors declare which `CalibrationVersion` they used |
| Signal/Event → Memory | Published Language (`EmbeddingVector`, event metadata) | `rvcsi-ruvector` ACL translates to RuVector's API |
| Event → Agent | Open Host Service (event stream + MCP tools) | `EventFilter` + `ToolPermission` enforced at the boundary |
| Capture → Agent | Conformist (health/status only, via MCP read tools) | No raw frames cross to agents |
The **`CsiFrame` schema is the shared kernel** between Capture, Validation, Signal, and the language-boundary (napi-rs) layer. It is the FFI-safe object; nothing device-specific leaks past it.
---
## Aggregates and Invariants
### `CaptureSession` aggregate
**Invariant:** a capture session has exactly one source profile and one runtime configuration.
1. A session cannot emit frames before it is started.
2. A session cannot change channel without restart unless the adapter supports dynamic retune.
3. A session must emit `SourceDisconnected` before stopping due to device loss.
### `ValidatedFrame` aggregate
**Invariant:** no frame crosses into SDK, DSP, memory, or agents unless its validation status is `Accepted` or `Degraded`.
1. Rejected frames go to quarantine when quarantine is enabled.
2. Degraded frames must carry quality-reason metadata.
3. Missing *optional* hardware metadata must not invalidate a frame.
### `CsiWindow` aggregate
**Invariant:** a window contains frames from exactly one source and one session.
1. Mixed-source windows are not allowed.
2. Window start time must be strictly less than end time.
3. Window quality is bounded in [0, 1].
### `CalibrationProfile` aggregate
**Invariant:** a calibration profile is linked to source, room, adapter profile, and configuration.
1. Calibration cannot complete if `StabilityScore` is below threshold.
2. Baseline updates must preserve version history.
3. Event detectors must declare which calibration version they used.
### `CsiEvent` aggregate
**Invariant:** an event must have evidence.
1. Every event references at least one evidence window.
2. Confidence is bounded in [0, 1].
3. Event suppression must be explainable by policy.
### `RoomMemory` aggregate
**Invariant:** stored embeddings are traceable to frame windows or event windows.
1. No orphan embeddings.
2. Retention policy applies at collection level.
3. Drift scores must include the baseline version.
---
## Data Model
```rust
pub struct CsiFrame {
pub frame_id: FrameId,
pub session_id: SessionId,
pub source_id: SourceId,
pub adapter_kind: AdapterKind,
pub timestamp_ns: u64,
pub channel: u16,
pub bandwidth_mhz: u16,
pub rssi_dbm: Option<i16>,
pub noise_floor_dbm: Option<i16>,
pub antenna_index: Option<u8>,
pub tx_chain: Option<u8>,
pub rx_chain: Option<u8>,
pub subcarrier_count: u16,
pub i_values: Vec<f32>,
pub q_values: Vec<f32>,
pub amplitude: Vec<f32>,
pub phase: Vec<f32>,
pub validation: ValidationStatus,
pub quality_score: f32,
pub calibration_version: Option<String>,
}
pub struct CsiWindow {
pub window_id: WindowId,
pub session_id: SessionId,
pub source_id: SourceId,
pub start_ns: u64,
pub end_ns: u64,
pub frame_count: u32,
pub mean_amplitude: Vec<f32>,
pub phase_variance: Vec<f32>,
pub motion_energy: f32,
pub presence_score: f32,
pub quality_score: f32,
}
pub enum CsiEventKind {
PresenceStarted,
PresenceEnded,
MotionDetected,
MotionSettled,
BaselineChanged,
SignalQualityDropped,
DeviceDisconnected,
BreathingCandidate,
AnomalyDetected,
CalibrationRequired,
}
pub struct CsiEvent {
pub event_id: EventId,
pub kind: CsiEventKind,
pub session_id: SessionId,
pub source_id: SourceId,
pub timestamp_ns: u64,
pub confidence: f32,
pub evidence_window_ids: Vec<WindowId>,
pub metadata_json: String,
}
pub struct AdapterProfile {
pub adapter_kind: AdapterKind,
pub chip: Option<String>,
pub firmware_version: Option<String>,
pub driver_version: Option<String>,
pub supported_channels: Vec<u16>,
pub supported_bandwidths_mhz: Vec<u16>,
pub expected_subcarrier_counts: Vec<u16>,
pub supports_live_capture: bool,
pub supports_injection: bool,
pub supports_monitor_mode: bool,
}
pub enum ValidationStatus { Accepted, Degraded, Rejected, Recovered }
```
---
## Domain Services
| Service | Input | Output | Responsibility |
|---------|-------|--------|----------------|
| `FrameValidationService` | `RawFrame`, `AdapterProfile`, `ValidationPolicy` | `ValidatedFrame` or `RejectedFrame` | Enforce bounds, finiteness, monotonicity; assign initial `QualityScore`; route rejects to quarantine; emit structured errors |
| `SignalProcessingService` | `ValidatedFrame` stream | `CsiWindow` stream | Run the DSP pipeline; build bounded windows; compute motion energy, presence score, window quality |
| `BaselineDeltaService` | `CsiWindow`, `BaselineModel` | `BaselineDelta` | Subtract the calibrated baseline; measure deviation magnitude |
| `CalibrationService` | `CsiWindow` stream over a calibration window | `CalibrationProfile` (new version) or `CalibrationFailed` | Learn a stable baseline; compute `StabilityScore`; reject unstable calibrations; preserve version history |
| `EventDetectionService` | `CsiWindow` + `BaselineDelta` + `CalibrationVersion` | `CsiEvent` stream | Drive per-source state machines; attach confidence + evidence windows + calibration version; apply suppression policy |
| `EmbeddingService` | `CsiWindow` / `CsiEvent` | `TemporalEmbedding` | Produce frame/window/event vectors (v0: deterministic DSP feature vector; later: AETHER / on-device model) |
| `RfMemoryService` | `TemporalEmbedding`, query | `EmbeddingStored` / similar windows / `DriftScore` | Store to RuVector; similarity search; drift computation against a baseline version |
| `ReplayService` | A captured session bundle | A deterministic frame/window/event stream | Replay preserving timestamps, ordering, validation decisions, event output, calibration version, runtime config |
| `AdapterRegistryService` | — | List of available adapters + `AdapterProfile`s | Discover sources (reuses ADR-049 interface detection); report health; flag unsupported firmware/driver state |
| `AgentGatewayService` | MCP tool call / SDK subscription | Tool result / filtered event stream | Enforce `ToolPermission` (read vs. write-gated), apply `EventFilter`, audit `ToolExecuted` / `PermissionDenied` |
---
## Related
- [rvCSI Platform PRD](../prd/rvcsi-platform-prd.md) — requirements, success criteria, scope
- [ADR-095 — rvCSI Edge RF Sensing Platform](../adr/ADR-095-rvcsi-edge-rf-sensing-platform.md) — the fifteen architectural decisions
- [RuvSense Domain Model](ruvsense-domain-model.md) — adjacent multistatic sensing context
- [Signal Processing Domain Model](signal-processing-domain-model.md) — the DSP primitives `rvcsi-dsp` reuses
- [ADR Index](../adr/README.md)
+842
View File
@@ -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 `v2/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
+497
View File
@@ -0,0 +1,497 @@
# WiFi-Mat Domain Model
## Domain-Driven Design Specification
### Ubiquitous Language
| Term | Definition |
|------|------------|
| **Survivor** | A human detected within a scan zone, potentially trapped |
| **Vital Signs** | Detectable life indicators: breathing, heartbeat, movement |
| **Scan Zone** | A defined geographic area being actively monitored |
| **Detection Event** | An occurrence of vital signs being detected |
| **Triage Status** | Medical priority classification (Immediate/Delayed/Minor/Deceased) |
| **Confidence Score** | Statistical certainty of detection (0.0-1.0) |
| **Penetration Depth** | Estimated distance through debris to survivor |
| **Debris Field** | Collection of materials between sensor and survivor |
---
## Bounded Contexts
### 1. Detection Context
**Responsibility**: Analyze CSI data to detect and classify human vital signs
```
┌─────────────────────────────────────────────────────────┐
│ Detection Context │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Breathing │ │ Heartbeat │ │
│ │ Detector │ │ Detector │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ └─────────┬─────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Movement │ │
│ │ Classifier │ │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Ensemble │──▶ VitalSignsReading │
│ │ Classifier │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
```
**Aggregates**:
- `VitalSignsReading` (Aggregate Root)
**Value Objects**:
- `BreathingPattern`
- `HeartbeatSignature`
- `MovementProfile`
- `ConfidenceScore`
### 2. Localization Context
**Responsibility**: Estimate survivor position within debris field
```
┌─────────────────────────────────────────────────────────┐
│ Localization Context │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │Triangulation │ │Fingerprinting│ │
│ │ Engine │ │ Matcher │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ └─────────┬─────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Depth │ │
│ │ Estimator │ │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Position │──▶ SurvivorLocation │
│ │ Fuser │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
```
**Aggregates**:
- `SurvivorLocation` (Aggregate Root)
**Value Objects**:
- `Coordinates3D`
- `DepthEstimate`
- `LocationUncertainty`
- `DebrisProfile`
### 3. Alerting Context
**Responsibility**: Generate and dispatch alerts based on detections
```
┌─────────────────────────────────────────────────────────┐
│ Alerting Context │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Triage │ │ Alert │ │
│ │ Calculator │ │ Generator │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ └─────────┬─────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Dispatcher │──▶ Alert │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
```
**Aggregates**:
- `Alert` (Aggregate Root)
**Value Objects**:
- `TriageStatus`
- `Priority`
- `AlertPayload`
---
## Core Domain Entities
### Survivor (Entity)
```rust
pub struct Survivor {
id: SurvivorId,
detection_time: DateTime<Utc>,
location: Option<SurvivorLocation>,
vital_signs: VitalSignsHistory,
triage_status: TriageStatus,
confidence: ConfidenceScore,
metadata: SurvivorMetadata,
}
```
**Invariants**:
- Must have at least one vital sign detection to exist
- Triage status must be recalculated on each vital sign update
- Confidence must be >= 0.3 to be considered valid detection
### DisasterEvent (Aggregate Root)
```rust
pub struct DisasterEvent {
id: DisasterEventId,
event_type: DisasterType,
start_time: DateTime<Utc>,
location: GeoLocation,
scan_zones: Vec<ScanZone>,
survivors: Vec<Survivor>,
status: EventStatus,
}
```
**Invariants**:
- Must have at least one scan zone
- All survivors must be within a scan zone
- Cannot add survivors after event is closed
### ScanZone (Entity)
```rust
pub struct ScanZone {
id: ScanZoneId,
bounds: ZoneBounds,
sensor_positions: Vec<SensorPosition>,
scan_parameters: ScanParameters,
status: ZoneStatus,
last_scan: DateTime<Utc>,
}
```
---
## Value Objects
### VitalSignsReading
```rust
pub struct VitalSignsReading {
breathing: Option<BreathingPattern>,
heartbeat: Option<HeartbeatSignature>,
movement: MovementProfile,
timestamp: DateTime<Utc>,
confidence: ConfidenceScore,
}
```
### TriageStatus (Enumeration)
```rust
pub enum TriageStatus {
/// Immediate - Life-threatening, requires immediate intervention
Immediate, // Red tag
/// Delayed - Serious but can wait for treatment
Delayed, // Yellow tag
/// Minor - Walking wounded, minimal treatment needed
Minor, // Green tag
/// Deceased - No vital signs detected over threshold period
Deceased, // Black tag
/// Unknown - Insufficient data for classification
Unknown,
}
```
### BreathingPattern
```rust
pub struct BreathingPattern {
rate_bpm: f32, // Breaths per minute (normal: 12-20)
amplitude: f32, // Signal strength
regularity: f32, // 0.0-1.0, consistency of pattern
pattern_type: BreathingType,
}
pub enum BreathingType {
Normal,
Shallow,
Labored,
Irregular,
Agonal,
}
```
### HeartbeatSignature
```rust
pub struct HeartbeatSignature {
rate_bpm: f32, // Beats per minute (normal: 60-100)
variability: f32, // Heart rate variability
strength: SignalStrength,
}
```
### Coordinates3D
```rust
pub struct Coordinates3D {
x: f64, // East-West offset from reference (meters)
y: f64, // North-South offset from reference (meters)
z: f64, // Depth below surface (meters, negative = below)
uncertainty: LocationUncertainty,
}
pub struct LocationUncertainty {
horizontal_error: f64, // meters (95% confidence)
vertical_error: f64, // meters (95% confidence)
}
```
---
## Domain Events
### Detection Events
```rust
pub enum DetectionEvent {
/// New survivor detected
SurvivorDetected {
survivor_id: SurvivorId,
zone_id: ScanZoneId,
vital_signs: VitalSignsReading,
location: Option<Coordinates3D>,
timestamp: DateTime<Utc>,
},
/// Survivor vital signs updated
VitalsUpdated {
survivor_id: SurvivorId,
previous: VitalSignsReading,
current: VitalSignsReading,
timestamp: DateTime<Utc>,
},
/// Survivor triage status changed
TriageStatusChanged {
survivor_id: SurvivorId,
previous: TriageStatus,
current: TriageStatus,
reason: String,
timestamp: DateTime<Utc>,
},
/// Survivor location refined
LocationRefined {
survivor_id: SurvivorId,
previous: Coordinates3D,
current: Coordinates3D,
timestamp: DateTime<Utc>,
},
/// Survivor no longer detected (may have been rescued or false positive)
SurvivorLost {
survivor_id: SurvivorId,
last_detection: DateTime<Utc>,
reason: LostReason,
},
}
pub enum LostReason {
Rescued,
FalsePositive,
SignalLost,
ZoneDeactivated,
}
```
### Alert Events
```rust
pub enum AlertEvent {
/// New alert generated
AlertGenerated {
alert_id: AlertId,
survivor_id: SurvivorId,
priority: Priority,
payload: AlertPayload,
},
/// Alert acknowledged by rescue team
AlertAcknowledged {
alert_id: AlertId,
acknowledged_by: TeamId,
timestamp: DateTime<Utc>,
},
/// Alert resolved
AlertResolved {
alert_id: AlertId,
resolution: AlertResolution,
timestamp: DateTime<Utc>,
},
}
```
---
## Domain Services
### TriageService
Calculates triage status based on vital signs using START protocol:
```rust
pub trait TriageService {
fn calculate_triage(&self, vitals: &VitalSignsReading) -> TriageStatus;
fn should_upgrade_priority(&self, history: &VitalSignsHistory) -> bool;
}
```
**Rules**:
1. No breathing detected → Check for movement
2. Movement but no breathing → Immediate (airway issue)
3. Breathing > 30/min → Immediate
4. Breathing < 10/min → Immediate
5. No radial pulse equivalent (weak heartbeat) → Immediate
6. Cannot follow commands (no responsive movement) → Immediate
7. Otherwise → Delayed or Minor based on severity
### LocalizationService
Fuses multiple localization techniques:
```rust
pub trait LocalizationService {
fn estimate_position(
&self,
csi_data: &[CsiReading],
sensor_positions: &[SensorPosition],
) -> Result<Coordinates3D, LocalizationError>;
fn estimate_depth(
&self,
signal_attenuation: f64,
debris_profile: &DebrisProfile,
) -> Result<DepthEstimate, LocalizationError>;
}
```
---
## Context Map
```
┌────────────────────────────────────────────────────────────────┐
│ WiFi-Mat System │
├────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Detection │◄───────►│ Localization│ │
│ │ Context │ Partner │ Context │ │
│ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │
│ │ Publishes │ Publishes │
│ ▼ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Event Bus (Domain Events) │ │
│ └─────────────────┬───────────────────┘ │
│ │ │
│ │ Subscribes │
│ ▼ │
│ ┌─────────────┐ │
│ │ Alerting │ │
│ │ Context │ │
│ └─────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────┤
│ UPSTREAM (Conformist) │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │wifi-densepose │ │wifi-densepose │ │wifi-densepose │ │
│ │ -signal │ │ -nn │ │ -hardware │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
**Relationship Types**:
- Detection ↔ Localization: **Partnership** (tight collaboration)
- Detection → Alerting: **Customer/Supplier** (Detection publishes, Alerting consumes)
- WiFi-Mat → Upstream crates: **Conformist** (adapts to their models)
---
## Anti-Corruption Layer
The integration module provides adapters to translate between upstream crate models and WiFi-Mat domain:
```rust
/// Adapts wifi-densepose-signal types to Detection context
pub struct SignalAdapter {
processor: CsiProcessor,
feature_extractor: FeatureExtractor,
}
impl SignalAdapter {
pub fn extract_vital_features(
&self,
raw_csi: &[Complex<f64>],
) -> Result<VitalFeatures, AdapterError>;
}
/// Adapts wifi-densepose-nn for specialized detection models
pub struct NeuralAdapter {
breathing_model: OnnxModel,
heartbeat_model: OnnxModel,
}
impl NeuralAdapter {
pub fn classify_breathing(
&self,
features: &VitalFeatures,
) -> Result<BreathingPattern, AdapterError>;
}
```
---
## Repository Interfaces
```rust
#[async_trait]
pub trait SurvivorRepository {
async fn save(&self, survivor: &Survivor) -> Result<(), RepositoryError>;
async fn find_by_id(&self, id: &SurvivorId) -> Result<Option<Survivor>, RepositoryError>;
async fn find_by_zone(&self, zone_id: &ScanZoneId) -> Result<Vec<Survivor>, RepositoryError>;
async fn find_active(&self) -> Result<Vec<Survivor>, RepositoryError>;
}
#[async_trait]
pub trait DisasterEventRepository {
async fn save(&self, event: &DisasterEvent) -> Result<(), RepositoryError>;
async fn find_active(&self) -> Result<Vec<DisasterEvent>, RepositoryError>;
async fn find_by_location(&self, location: &GeoLocation, radius_km: f64) -> Result<Vec<DisasterEvent>, RepositoryError>;
}
#[async_trait]
pub trait AlertRepository {
async fn save(&self, alert: &Alert) -> Result<(), RepositoryError>;
async fn find_pending(&self) -> Result<Vec<Alert>, RepositoryError>;
async fn find_by_survivor(&self, survivor_id: &SurvivorId) -> Result<Vec<Alert>, RepositoryError>;
}
```