# 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>`. **Bounded Contexts:** | # | Context | Responsibility | Key ADRs | Code | |---|---------|----------------|----------|------| | 1 | [CSI Ingestion](#1-csi-ingestion-context) | Receive, decode, and feature-extract CSI frames from ESP32 UDP | [ADR-019](../adr/ADR-019-sensing-only-ui-mode.md), [ADR-035](../adr/ADR-035-live-sensing-ui-accuracy.md) | `sensing-server/src/main.rs` | | 2 | [Model Management](#2-model-management-context) | Load, unload, list RVF models; LoRA profile activation | [ADR-043](../adr/ADR-043-sensing-server-ui-api-completion.md) | `sensing-server/src/model_manager.rs` | | 3 | [CSI Recording](#3-csi-recording-context) | Record CSI frames to .jsonl files, manage recording sessions | [ADR-043](../adr/ADR-043-sensing-server-ui-api-completion.md) | `sensing-server/src/recording.rs` | | 4 | [Training Pipeline](#4-training-pipeline-context) | Background training runs, progress streaming, contrastive pretraining | [ADR-043](../adr/ADR-043-sensing-server-ui-api-completion.md) | `sensing-server/src/training_api.rs` | | 5 | [Visualization](#5-visualization-context) | WebSocket streaming to web UI, Gaussian splat rendering, data transparency | [ADR-019](../adr/ADR-019-sensing-only-ui-mode.md), [ADR-035](../adr/ADR-035-live-sensing-ui-accuracy.md) | `ui/` | All code paths shown are relative to `rust-port/wifi-densepose-rs/crates/wifi-densepose-` unless otherwise noted. --- ## Domain-Driven Design Specification ### Ubiquitous Language | Term | Definition | |------|------------| | **Sensing Update** | A complete JSON message broadcast to WebSocket clients each tick, containing node data, features, classification, signal field, and optional vital signs | | **Tick** | One processing cycle of the sensing loop (default 100ms = 10 fps, configurable via `--tick-ms`) | | **Data Source** | Origin of CSI data: `esp32` (UDP port 5005), `wifi` (Windows RSSI), `simulated` (synthetic), or `auto` (try ESP32 then fall back) | | **RVF Model** | A `.rvf` container file holding trained weights, manifest metadata, optional LoRA adapters, and vital sign configuration | | **LoRA Profile** | A lightweight adapter applied on top of a base RVF model for environment-specific fine-tuning without retraining the full model | | **Recording Session** | A period during which CSI frames are appended to a `.csi.jsonl` file, identified by a session ID and optional activity label | | **Training Run** | A background task that loads recorded CSI data, extracts features, trains a regularised linear model, and exports a `.rvf` container | | **Frame History** | A circular buffer of the last 100 CSI amplitude vectors used for temporal analysis (sliding-window variance, Goertzel breathing estimation) | | **Goertzel Filter** | A frequency-domain estimator applied to the frame history to detect breathing rate (0.1--0.5 Hz) via a 9-candidate filter bank | | **Signal Field** | A 20x1x20 grid of interpolated signal intensity values rendered as Gaussian splats in the UI | | **Pose Source** | Whether pose keypoints are `signal_derived` (analytical from CSI features) or `model_inference` (from a loaded RVF model) | | **Progressive Loader** | A two-layer model loading strategy: Layer A loads instantly for basic inference, Layer B loads in background for full accuracy | | **Sensing-Only Mode** | UI mode when the DensePose backend is unavailable; suppresses DensePose tabs, shows only sensing and signal visualization | | **AppStateInner** | The single shared state struct holding all server state, accessed via `Arc>` | | **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, | | | | 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>>. pub struct AppStateInner { /// Most recent sensing update broadcast to clients. latest_update: Option, /// RSSI history for sparkline display. rssi_history: VecDeque, /// Circular buffer of recent CSI amplitude vectors (100 frames). frame_history: VecDeque>, /// Monotonic tick counter. tick: u64, /// Active data source identifier ("esp32", "wifi", "simulated"). source: String, /// Broadcast channel for WebSocket fan-out. tx: broadcast::Sender, /// 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, // per-node CSI data pub features: FeatureInfo, // extracted signal features pub classification: ClassificationInfo, pub signal_field: SignalField, pub vital_signs: Option, pub persons: Option>, pub estimated_persons: Option, } /// 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, 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, } /// 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, pub phases: Vec, } /// 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) | | | +--------+----------+ | | 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, /// Currently active LoRA profile (if any). pub active_lora_profile: Option, /// Model weights (f32 parameters). pub weights: Vec, /// 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, pub has_quantization: bool, pub lora_profiles: Vec, 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, pub active_lora_profile: Option, } /// 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, /// 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, } ``` **Value Objects:** ```rust /// Metadata for a completed or active recording session. pub struct RecordingSession { pub id: String, pub name: String, pub label: Option, pub started_at: String, pub ended_at: Option, 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, 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, pub duration_secs: Option, } ``` **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>, } ``` **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, 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, pub lora_profile: Option, } /// Request to start supervised training. pub struct StartTrainingRequest { pub dataset_ids: Vec, // recording session IDs pub config: TrainingConfig, } /// Request to start contrastive pretraining (ADR-024). pub struct PretrainRequest { pub dataset_ids: Vec, 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, 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`, 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`** for WebSocket fan-out of sensing updates 2. **`broadcast::Sender`** 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` 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` 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>`. ### 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