| Field | Value |
|---|---|
| Status | Partially Implemented |
| Date | 2026-02-28 |
| Deciders | ruv |
| Relates to | ADR-014 (SOTA Signal Processing), ADR-017 (RuVector-Signal-MAT), ADR-019 (Sensing-Only UI), ADR-020 (Rust RuVector AI Model Migration) |
WiFi-based vital sign monitoring is a rapidly maturing field. Channel State Information (CSI) captures fine-grained multipath propagation changes caused by physiological movements -- chest displacement from respiration (1-5 mm amplitude, 0.1-0.5 Hz) and body surface displacement from cardiac activity (0.1-0.5 mm, 0.8-2.0 Hz). Our existing WiFi-DensePose project already implements motion detection, presence sensing, and body velocity profiling (BVP), but lacks a dedicated vital sign extraction pipeline.
Vital sign detection extends the project's value from occupancy sensing into health monitoring, enabling contactless respiratory rate and heart rate estimation for applications in eldercare, sleep monitoring, disaster survivor detection (ADR-001), and clinical triage.
The vendor/ruvector codebase provides a rich set of signal processing primitives that map directly to vital sign detection requirements. Rather than building from scratch, we can compose existing rvdna components into a vital sign pipeline. The key crates and their relevance:
| Crate | Key Primitives | Vital Sign Relevance |
|---|---|---|
ruvector-temporal-tensor |
TemporalTensorCompressor, TieredStore, TierPolicy, tiered quantization (8/7/5/3-bit) |
Stores compressed CSI temporal streams with adaptive precision -- hot (real-time vital signs) at 8-bit, warm (historical) at 5-bit, cold (archive) at 3-bit |
ruvector-nervous-system |
PredictiveLayer, OscillatoryRouter, GlobalWorkspace, DVSEvent, EventRingBuffer, ShardedEventBus, EpropSynapse, Dendrite, ModernHopfield |
Predictive coding suppresses static CSI components (90-99% bandwidth reduction), oscillatory routing isolates respiratory vs cardiac frequency bands, event bus handles high-throughput CSI streams |
ruvector-attention |
ScaledDotProductAttention, Mixture of Experts (MoE), PDE attention, sparse attention |
Attention-weighted subcarrier selection for vital sign sensitivity, already used in BVP extraction |
ruvector-coherence |
SpectralCoherenceScore, HnswHealthMonitor, spectral gap estimation, Fiedler value |
Spectral analysis of CSI time series, coherence between subcarrier pairs for breathing/heartbeat isolation |
ruvector-gnn |
GnnLayer, Linear, LayerNorm, graph attention, EWC training |
Graph neural network over subcarrier correlation topology, learning which subcarrier groups carry vital sign information |
ruvector-core |
VectorDB, HNSW index, SIMD distance, quantization |
Fingerprint-based pattern matching of vital sign waveform templates |
sona |
SonaEngine, TrajectoryBuilder, micro-LoRA, EWC++ |
Self-optimizing adaptation of vital sign extraction parameters per environment |
ruvector-sparse-inference |
Sparse model execution, precision management | Efficient inference on edge devices with constrained compute |
ruQu |
FilterPipeline (Structural/Shift/Evidence), AdaptiveThresholds (Welford, EMA, CUSUM-style), DriftDetector (step-change, variance expansion, oscillation), QuantumFabric (256-tile parallel processing) |
Three-filter decision pipeline for vital sign gating -- structural filter detects signal partition/degradation, shift filter catches distribution drift in vital sign baselines, evidence filter provides anytime-valid statistical rigor. DriftDetector directly detects respiratory/cardiac parameter drift. AdaptiveThresholds self-tunes anomaly thresholds with outcome feedback (precision/recall/F1). 256-tile fabric maps to parallel subcarrier processing. |
DNA example (examples/dna) |
BiomarkerProfile, StreamProcessor, RingBuffer, BiomarkerReading, z-score anomaly detection, CUSUM changepoint detection, EMA, trend analysis |
Direct analog -- the biomarker streaming engine processes time-series health data with anomaly detection, which maps exactly to vital sign monitoring |
The Rust port (rust-port/wifi-densepose-rs/) already contains:
wifi-densepose-signal: CSI processing, BVP extraction, phase sanitization, Hampel filter, spectrogram generation, Fresnel geometry, motion detection, subcarrier selectionwifi-densepose-sensing-server: Axum server receiving ESP32 CSI frames (UDP 5005), WebSocket broadcasting sensing updates, signal field generation, with three data source modes:- ESP32 mode (
--source esp32): Receives ADR-018 binary frames via UDP:5005. Frame format: magic0xC511_0001, 20-byte header (node_id,n_antennas,n_subcarriers,freq_mhz,sequence,rssi,noise_floor), packed I/Q pairs. Theparse_esp32_frame()function extracts amplitude (sqrt(I^2+Q^2)) and phase (atan2(Q,I)) per subcarrier. ESP32 mode also runs abroadcast_tick_taskfor re-broadcasting buffered state to WebSocket clients between frames. - Windows WiFi mode (
--source wifi): Usesnetsh wlan show interfacesto extract RSSI/signal% and creates pseudo-single-subcarrier frames. Useful for development but lacks multi-subcarrier CSI. - Simulation mode (
--source simulate): Generates synthetic 56-subcarrier frames with sinusoidal amplitude/phase variation. Used for UI testing.
- ESP32 mode (
- Auto-detection:
main()probes ESP32 UDP first, then Windows WiFi, then falls back to simulation. The vital sign module must integrate with all three modes but will only produce meaningful HR/RR in ESP32 mode (multi-subcarrier CSI). - Existing features used by vitals:
extract_features_from_frame()already computesbreathing_band_power(low-frequency subcarrier variance) andmotion_band_power(high-frequency variance). Thegenerate_signal_field()function already models abreath_ringmodulated by variance and tick. These serve as integration anchors for the vital sign pipeline. - Existing ADR-019/020: Sensing-only UI mode with Three.js visualization and Rust migration plan
What is missing is a dedicated vital sign extraction stage between the CSI processing pipeline and the UI visualization.
Implement a vital sign detection module as a new crate wifi-densepose-vitals within the Rust port workspace, composed from rvdna primitives. The module extracts heart rate (HR) and respiratory rate (RR) from WiFi CSI data and integrates with the existing sensing server and UI.
- Composition over invention: Use existing rvdna crates as building blocks rather than reimplementing signal processing from scratch.
- Streaming-first architecture: Process CSI frames as they arrive using ring buffers and event-driven processing, modeled on the
biomarker_stream::StreamProcessorpattern. - Environment-adaptive: Use SONA's self-optimizing loop to adapt extraction parameters (filter cutoffs, subcarrier weights, noise thresholds) per deployment.
- Tiered storage: Use
ruvector-temporal-tensorto store vital sign time series at variable precision based on access patterns. - Privacy by design: All processing is local and on-device; no raw CSI data leaves the device.
┌─────────────────────────────────────────────────────────┐
│ wifi-densepose-vitals crate │
│ │
ESP32 CSI (UDP:5005) ──▶│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ CsiVitalPreproc │ │ VitalSignExtractor │ │
┌───────────────────│ │ (ruvector-nervous │──▶│ ┌────────────────────┐ │ │
│ │ │ -system: │ │ │ BreathingExtractor │ │ │──▶ WebSocket
│ wifi-densepose- │ │ PredictiveLayer │ │ │ (Bandpass 0.1-0.5) │ │ │ (/ws/vitals)
│ signal crate │ │ + EventRingBuffer)│ │ └────────────────────┘ │ │
│ ┌─────────────┐ │ └──────────────────┘ │ ┌────────────────────┐ │ │──▶ REST API
│ │CsiProcessor │ │ │ │ │ HeartRateExtractor │ │ │ (/api/v1/vitals)
│ │PhaseSntzr │──│───────────┘ │ │ (Bandpass 0.8-2.0) │ │ │
│ │HampelFilter │ │ │ └────────────────────┘ │ │
│ │SubcarrierSel│ │ ┌──────────────────┐ │ ┌────────────────────┐ │ │
│ └─────────────┘ │ │ SubcarrierWeighter│ │ │ MotionArtifact │ │ │
│ │ │ (ruvector-attention│ │ │ Rejector │ │ │
└───────────────────│ │ + ruvector-gnn) │──▶│ └────────────────────┘ │ │
│ └──────────────────┘ └──────────────────────────┘ │
│ │ │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ VitalSignStore │ │ AnomalyDetector │ │
│ │ (ruvector-temporal │◀──│ (biomarker_stream │ │
│ │ -tensor:TieredSt)│ │ pattern: z-score, │ │
│ └──────────────────┘ │ CUSUM, EMA, trend) │ │
│ └──────────────────────────┘ │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ VitalCoherenceGate│ │ PatternMatcher │ │
│ │ (ruQu: 3-filter │ │ (ruvector-core:VectorDB │ │
│ │ pipeline, drift │ │ + ModernHopfield) │ │
│ │ detection, │ └──────────────────────────┘ │
│ │ adaptive thresh) │ │
│ └──────────────────┘ ┌──────────────────────────┐ │
│ ┌──────────────────┐ │ SonaAdaptation │ │
│ │ ESP32 Frame Input │ │ (sona:SonaEngine │ │
│ │ (UDP:5005, magic │ │ micro-LoRA adapt) │ │
│ │ 0xC511_0001, │ └──────────────────────────┘ │
│ │ 20B hdr + I/Q) │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────┘
rust-port/wifi-densepose-rs/crates/wifi-densepose-vitals/
├── Cargo.toml
└── src/
├── lib.rs # Public API and re-exports
├── config.rs # VitalSignConfig, band definitions
├── preprocess.rs # CsiVitalPreprocessor (PredictiveLayer-based)
├── extractor.rs # VitalSignExtractor (breathing + heartrate)
├── breathing.rs # BreathingExtractor (respiratory rate)
├── heartrate.rs # HeartRateExtractor (cardiac rate)
├── subcarrier_weight.rs # AttentionSubcarrierWeighter (GNN + attention)
├── artifact.rs # MotionArtifactRejector
├── anomaly.rs # VitalAnomalyDetector (z-score, CUSUM, EMA)
├── coherence_gate.rs # VitalCoherenceGate (ruQu three-filter pipeline + drift detection)
├── store.rs # VitalSignStore (TieredStore wrapper)
├── pattern.rs # VitalPatternMatcher (Hopfield + HNSW)
├── adaptation.rs # SonaVitalAdapter (environment adaptation)
├── types.rs # VitalReading, VitalSign, VitalStatus
└── error.rs # VitalError type
The existing wifi-densepose-signal crate handles raw CSI ingestion:
- ESP32 frame parsing:
parse_esp32_frame()extracts I/Q amplitudes and phases from the ADR-018 binary frame format (magic0xC511_0001, 20-byte header + packed I/Q pairs). - Phase sanitization:
PhaseSanitizerperforms linear phase removal, unwrapping, and Hampel outlier filtering. - Subcarrier selection:
subcarrier_selectionmodule identifies motion-sensitive subcarriers.
The vital sign module adds a PredictiveLayer gate from ruvector-nervous-system::routing:
use ruvector_nervous_system::routing::PredictiveLayer;
pub struct CsiVitalPreprocessor {
/// Predictive coding layer -- suppresses static CSI components.
/// Only transmits residuals (changes) exceeding threshold.
/// Achieves 90-99% bandwidth reduction on stable environments.
predictive: PredictiveLayer,
/// Ring buffer for CSI amplitude history per subcarrier.
/// Modeled on biomarker_stream::RingBuffer.
amplitude_buffers: Vec<RingBuffer<f64>>,
/// Phase difference buffers (consecutive packet delta-phase).
phase_diff_buffers: Vec<RingBuffer<f64>>,
/// Number of subcarriers being tracked.
n_subcarriers: usize,
/// Sampling rate derived from ESP32 packet arrival rate.
sample_rate_hz: f64,
}
impl CsiVitalPreprocessor {
pub fn new(n_subcarriers: usize, window_size: usize) -> Self {
Self {
// 10% threshold: only transmit when CSI changes by >10%
predictive: PredictiveLayer::new(n_subcarriers, 0.10),
amplitude_buffers: (0..n_subcarriers)
.map(|_| RingBuffer::new(window_size))
.collect(),
phase_diff_buffers: (0..n_subcarriers)
.map(|_| RingBuffer::new(window_size))
.collect(),
n_subcarriers,
sample_rate_hz: 100.0, // Default; calibrated from packet timing
}
}
/// Ingest a new CSI frame and return preprocessed vital-sign-ready data.
/// Returns None if the frame is predictable (no change).
pub fn ingest(&mut self, amplitudes: &[f64], phases: &[f64]) -> Option<VitalFrame> {
let amp_f32: Vec<f32> = amplitudes.iter().map(|&a| a as f32).collect();
// PredictiveLayer gates: only process if residual exceeds threshold
if !self.predictive.should_transmit(&_f32) {
self.predictive.update(&_f32);
return None; // Static environment, skip processing
}
self.predictive.update(&_f32);
// Buffer amplitude and phase-difference data
for (i, (&, &phase)) in amplitudes.iter().zip(phases.iter()).enumerate() {
if i < self.n_subcarriers {
self.amplitude_buffers[i].push(amp);
self.phase_diff_buffers[i].push(phase);
}
}
Some(VitalFrame {
amplitudes: amplitudes.to_vec(),
phases: phases.to_vec(),
timestamp_us: /* from ESP32 frame */,
})
}
}Not all subcarriers carry vital sign information equally. Some are dominated by static multipath, others by motion artifacts. The subcarrier weighting stage uses ruvector-attention and ruvector-gnn to learn which subcarriers are most sensitive to physiological movements.
use ruvector_attention::ScaledDotProductAttention;
use ruvector_attention::traits::Attention;
pub struct AttentionSubcarrierWeighter {
/// Attention mechanism for subcarrier importance scoring.
/// Keys: subcarrier variance profiles.
/// Queries: target vital sign frequency band power.
/// Values: subcarrier amplitude time series.
attention: ScaledDotProductAttention,
/// GNN layer operating on subcarrier correlation graph.
/// Nodes = subcarriers, edges = cross-correlation strength.
/// Learns spatial-spectral patterns indicative of vital signs.
gnn_layer: ruvector_gnn::GnnLayer,
/// Weights per subcarrier (updated each processing window).
weights: Vec<f32>,
}The approach mirrors how BVP extraction in wifi-densepose-signal::bvp already uses ScaledDotProductAttention to weight subcarrier contributions to velocity profiles. For vital signs, the attention query vector encodes the expected spectral content (breathing band 0.1-0.5 Hz, cardiac band 0.8-2.0 Hz), and the keys encode each subcarrier's current spectral profile.
The GNN layer from ruvector-gnn::layer builds a correlation graph over subcarriers (node = subcarrier, edge weight = cross-correlation coefficient), then performs message passing to identify subcarrier clusters that exhibit coherent vital-sign-band oscillations. This is directly analogous to ADR-006's GNN-enhanced CSI pattern recognition.
Two parallel extractors operate on the weighted, preprocessed CSI data:
pub struct BreathingExtractor {
/// Bandpass filter: 0.1 - 0.5 Hz (6-30 breaths/min)
filter_low: f64, // 0.1 Hz
filter_high: f64, // 0.5 Hz
/// Oscillatory router from ruvector-nervous-system.
/// Configured at ~0.25 Hz (mean breathing frequency).
/// Phase-locks to the dominant respiratory component in CSI.
oscillator: OscillatoryRouter,
/// Ring buffer of filtered breathing-band signal.
/// Modeled on biomarker_stream::RingBuffer<f64>.
signal_buffer: RingBuffer<f64>,
/// Peak detector state for breath counting.
last_peak_time: Option<u64>,
peak_intervals: RingBuffer<f64>,
}
impl BreathingExtractor {
pub fn extract(&mut self, weighted_csi: &[f64], timestamp_us: u64) -> BreathingEstimate {
// 1. Bandpass filter CSI to breathing band (0.1-0.5 Hz)
let breathing_signal = self.bandpass_filter(weighted_csi);
// 2. Aggregate across subcarriers (weighted sum)
let composite = self.aggregate(breathing_signal);
// 3. Buffer and detect peaks
self.signal_buffer.push(composite);
// 4. Count inter-peak intervals for rate estimation
// Uses Welford online mean/variance (same as biomarker_stream::window_mean_std)
let rate_bpm = self.estimate_rate();
BreathingEstimate {
rate_bpm,
confidence: self.compute_confidence(),
waveform_sample: composite,
timestamp_us,
}
}
}pub struct HeartRateExtractor {
/// Bandpass filter: 0.8 - 2.0 Hz (48-120 beats/min)
filter_low: f64, // 0.8 Hz
filter_high: f64, // 2.0 Hz
/// Hopfield network for cardiac pattern template matching.
/// Stores learned heartbeat waveform templates.
/// Retrieval acts as matched filter against noisy CSI.
hopfield: ModernHopfield,
/// Signal buffer for spectral analysis.
signal_buffer: RingBuffer<f64>,
/// Spectral coherence tracker from ruvector-coherence.
coherence: SpectralTracker,
}Heart rate extraction is inherently harder than breathing due to the much smaller displacement (0.1-0.5 mm vs 1-5 mm). The ModernHopfield network from ruvector-nervous-system::hopfield stores learned cardiac waveform templates with exponential storage capacity (Ramsauer et al. 2020 formulation). Retrieval performs a soft matched filter: the noisy CSI signal is compared against all stored templates via the transformer-style attention mechanism (beta-parameterized softmax), and the closest template's period determines heart rate.
The ruvector-coherence::spectral::SpectralTracker monitors the spectral gap and Fiedler value of the subcarrier correlation graph over time. A strong spectral gap in the cardiac band indicates high signal quality and reliable HR estimation.
Large body movements (walking, gesturing) overwhelm the subtle vital sign signals. The artifact rejector uses the existing MotionDetector from wifi-densepose-signal::motion and the DVSEvent/EventRingBuffer system from ruvector-nervous-system::eventbus:
pub struct MotionArtifactRejector {
/// Event ring buffer for motion events.
/// DVSEvent.polarity=true indicates motion onset, false indicates motion offset.
event_buffer: EventRingBuffer<DVSEvent>,
/// Backpressure controller from ruvector-nervous-system::eventbus.
/// Suppresses vital sign output during high-motion periods.
backpressure: BackpressureController,
/// Global workspace from ruvector-nervous-system::routing.
/// Limited-capacity broadcast (Miller's Law: 4-7 items).
/// Vital signs compete with motion signals for workspace slots.
/// Only when motion signal loses the competition can vital signs broadcast.
workspace: GlobalWorkspace,
/// Motion energy threshold for blanking.
motion_threshold: f64,
/// Blanking duration after motion event (seconds).
blanking_duration: f64,
}The GlobalWorkspace (Baars 1988 model) from the nervous system routing module implements limited-capacity competition. Vital sign representations and motion representations compete for workspace access. During high motion, motion signals dominate the workspace and vital sign output is suppressed. When motion subsides, vital sign representations win the competition and are broadcast to consumers.
Modeled directly on examples/dna/src/biomarker_stream.rs::StreamProcessor:
pub struct VitalAnomalyDetector {
/// Per-vital-sign ring buffers and rolling statistics.
/// Directly mirrors biomarker_stream::StreamProcessor architecture.
buffers: HashMap<VitalSignType, RingBuffer<f64>>,
stats: HashMap<VitalSignType, VitalStats>,
/// Z-score threshold for anomaly detection (default: 2.5, same as biomarker_stream).
z_threshold: f64,
/// CUSUM changepoint detection parameters.
/// Detects sustained shifts in vital signs (e.g., respiratory arrest onset).
cusum_threshold: f64, // 4.0 (same as biomarker_stream)
cusum_drift: f64, // 0.5
/// EMA smoothing factor (alpha = 0.1).
ema_alpha: f64,
}
pub struct VitalStats {
pub mean: f64,
pub variance: f64,
pub min: f64,
pub max: f64,
pub count: u64,
pub anomaly_rate: f64,
pub trend_slope: f64,
pub ema: f64,
pub cusum_pos: f64,
pub cusum_neg: f64,
pub changepoint_detected: bool,
}This is a near-direct port of the biomarker_stream architecture. The same Welford online algorithm computes rolling mean and standard deviation, the same CUSUM algorithm detects changepoints (apnea onset, tachycardia), and the same linear regression computes trend slopes.
The ruQu crate provides a production-grade three-filter decision pipeline originally designed for quantum error correction, but its abstractions map precisely to vital sign signal quality gating. Rather than reimplementing quality gates from scratch, we compose ruQu's filters into a vital sign coherence gate:
use ruqu::{
AdaptiveThresholds, DriftDetector, DriftConfig, DriftProfile, LearningConfig,
FilterPipeline, FilterConfig, Verdict,
};
pub struct VitalCoherenceGate {
/// Three-filter pipeline adapted for vital sign gating:
/// - Structural: min-cut on subcarrier correlation graph (low cut = signal degradation)
/// - Shift: distribution drift in vital sign baselines (detects environmental changes)
/// - Evidence: anytime-valid e-value accumulation for statistical rigor
filter_pipeline: FilterPipeline,
/// Adaptive thresholds that self-tune based on outcome feedback.
/// Uses Welford online stats, EMA tracking, and precision/recall/F1 scoring.
/// Directly ports ruQu's AdaptiveThresholds with LearningConfig.
adaptive: AdaptiveThresholds,
/// Drift detector for vital sign baselines.
/// Detects 5 drift profiles from ruQu:
/// - Stable: normal operation
/// - Linear: gradual respiratory rate shift (e.g., falling asleep)
/// - StepChange: sudden HR change (e.g., startle response)
/// - Oscillating: periodic artifact (e.g., fan interference)
/// - VarianceExpansion: increasing noise (e.g., subject moving)
rr_drift: DriftDetector,
hr_drift: DriftDetector,
}
impl VitalCoherenceGate {
pub fn new() -> Self {
Self {
filter_pipeline: FilterPipeline::new(FilterConfig::default()),
adaptive: AdaptiveThresholds::new(LearningConfig {
learning_rate: 0.01,
history_window: 10_000,
warmup_samples: 500, // ~5 seconds at 100 Hz
ema_decay: 0.99,
auto_adjust: true,
..Default::default()
}),
rr_drift: DriftDetector::with_config(DriftConfig {
window_size: 300, // 3-second window at 100 Hz
min_samples: 100,
mean_shift_threshold: 2.0,
variance_threshold: 1.5,
trend_sensitivity: 0.1,
}),
hr_drift: DriftDetector::with_config(DriftConfig {
window_size: 500, // 5-second window (cardiac needs longer baseline)
min_samples: 200,
mean_shift_threshold: 2.5,
variance_threshold: 2.0,
trend_sensitivity: 0.05,
}),
}
}
/// Gate a vital sign reading: returns Verdict (Permit/Deny/Defer)
pub fn gate(&mut self, reading: &VitalReading) -> Verdict {
// Feed respiratory rate to drift detector
self.rr_drift.push(reading.respiratory_rate.value_bpm);
self.hr_drift.push(reading.heart_rate.value_bpm);
// Record metrics for adaptive threshold learning
let cut = reading.signal_quality;
let shift = self.rr_drift.severity().max(self.hr_drift.severity());
let evidence = reading.respiratory_rate.confidence.min(reading.heart_rate.confidence);
self.adaptive.record_metrics(cut, shift, evidence);
// Three-filter decision: all must pass for PERMIT
// This ensures only high-confidence vital signs reach the UI
let verdict = self.filter_pipeline.evaluate(cut, shift, evidence);
// If drift detected, compensate adaptive thresholds
if let Some(profile) = self.rr_drift.detect() {
if !matches!(profile, DriftProfile::Stable) {
self.adaptive.apply_drift_compensation(&profile);
}
}
verdict
}
/// Record whether the gate decision was correct (for learning)
pub fn record_outcome(&mut self, was_deny: bool, was_actually_bad: bool) {
self.adaptive.record_outcome(was_deny, was_actually_bad);
}
}Why ruQu fits here:
| ruQu Concept | Vital Sign Mapping |
|---|---|
| Syndrome round (detector bitmap) | CSI frame (subcarrier amplitudes/phases) |
| Structural min-cut | Subcarrier correlation graph connectivity (low cut = signal breakup) |
| Shift filter (distribution drift) | Respiratory/cardiac baseline drift from normal |
| Evidence filter (e-value) | Statistical confidence accumulation over time |
DriftDetector with 5 profiles |
Detects sleep onset (Linear), startle (StepChange), fan interference (Oscillating), subject motion (VarianceExpansion) |
AdaptiveThresholds with Welford/EMA |
Self-tuning anomaly thresholds with outcome-based F1 optimization |
| PERMIT / DENY / DEFER | Only emit vital signs to UI when quality is proven |
256-tile QuantumFabric |
Future: parallel per-subcarrier processing on WASM |
use ruvector_temporal_tensor::{TieredStore, TierPolicy, Tier};
use ruvector_temporal_tensor::core_trait::{TensorStore, TensorStoreExt};
pub struct VitalSignStore {
store: TieredStore,
tier_policy: TierPolicy,
}Vital sign data is stored in the TieredStore from ruvector-temporal-tensor:
| Tier | Bits | Compression | Purpose |
|---|---|---|---|
| Tier1 (Hot) | 8-bit | 4x | Real-time vital signs (last 5 minutes), fed to UI |
| Tier2 (Warm) | 5-bit | 6.4x | Recent history (last 1 hour), trend analysis |
| Tier3 (Cold) | 3-bit | 10.67x | Long-term archive (24+ hours), pattern library |
| Tier0 (Evicted) | metadata only | N/A | Expired data with reconstruction policy |
The BlockKey maps naturally to vital sign storage:
tensor_id: encodes vital sign type (0 = breathing rate, 1 = heart rate, 2 = composite waveform)block_index: encodes time window index
use sona::{SonaEngine, SonaConfig, TrajectoryBuilder};
pub struct SonaVitalAdapter {
engine: SonaEngine,
}
impl SonaVitalAdapter {
pub fn begin_extraction(&self, csi_embedding: Vec<f32>) -> TrajectoryBuilder {
self.engine.begin_trajectory(csi_embedding)
}
pub fn end_extraction(&self, builder: TrajectoryBuilder, quality: f32) {
// quality = confidence * accuracy of vital sign estimate
self.engine.end_trajectory(builder, quality);
}
/// Apply micro-LoRA adaptation to filter parameters.
pub fn adapt_filters(&self, filter_params: &[f32], adapted: &mut [f32]) {
self.engine.apply_micro_lora(filter_params, adapted);
}
}The SONA engine's 4-step intelligence pipeline (RETRIEVE, JUDGE, DISTILL, CONSOLIDATE) enables:
- RETRIEVE: Find past successful extraction parameters for similar environments via HNSW.
- JUDGE: Score extraction quality based on physiological plausibility (HR 40-180 BPM, RR 4-40 BPM).
- DISTILL: Extract key parameter adjustments via micro-LoRA.
- CONSOLIDATE: Prevent forgetting of previously learned environments via EWC++.
ESP32 CSI Frame (UDP :5005)
│ Magic: 0xC511_0001 | 20-byte header | packed I/Q pairs
│ parse_esp32_frame() → Esp32Frame { node_id, n_antennas,
│ n_subcarriers, freq_mhz, sequence, rssi, noise_floor,
│ amplitudes: Vec<f64>, phases: Vec<f64> }
│
▼
[wifi-densepose-signal] CsiProcessor + PhaseSanitizer + HampelFilter
│
▼
[wifi-densepose-vitals] CsiVitalPreprocessor (PredictiveLayer gate)
│
├──▶ Static environment? (predictable) ──▶ Skip (90-99% frames filtered)
│
▼ (residual frames with physiological changes)
[wifi-densepose-vitals] AttentionSubcarrierWeighter (attention + GNN)
│
▼
[wifi-densepose-vitals] MotionArtifactRejector (GlobalWorkspace competition)
│
├──▶ High motion? ──▶ Blank vital sign output, report motion-only
│
▼ (low-motion frames)
├──▶ BreathingExtractor ──▶ RR estimate (BPM + confidence)
├──▶ HeartRateExtractor ──▶ HR estimate (BPM + confidence)
│
▼
[wifi-densepose-vitals] VitalAnomalyDetector (z-score, CUSUM, EMA)
│
├──▶ Anomaly? ──▶ Alert (apnea, tachycardia, bradycardia)
│
▼
[wifi-densepose-vitals] VitalCoherenceGate (ruQu three-filter pipeline)
│
├──▶ DENY (low quality) ──▶ Suppress reading, keep previous valid
├──▶ DEFER (accumulating) ──▶ Buffer, await more evidence
│
▼ PERMIT (high-confidence vital signs)
[wifi-densepose-vitals] VitalSignStore (TieredStore: 8/5/3-bit)
│
▼
[wifi-densepose-sensing-server] WebSocket broadcast (/ws/vitals)
│ AppStateInner extended with latest_vitals + vitals_tx channel
│ ESP32 mode: udp_receiver_task feeds amplitudes/phases to VitalSignExtractor
│ WiFi mode: pseudo-frame (single subcarrier) → VitalStatus::Unreliable
│ Simulate mode: synthetic CSI → calibration/demo vital signs
│
▼
[UI] SensingTab.js: vital sign visualization overlay
ESP32 Integration Detail: The udp_receiver_task in the sensing server already receives and parses ESP32 frames. The vital sign module hooks into this path:
// In udp_receiver_task, after parse_esp32_frame():
if let Some(frame) = parse_esp32_frame(&buf[..len]) {
let (features, classification) = extract_features_from_frame(&frame);
// NEW: Feed into vital sign extractor
let vital_reading = s.vital_extractor.process_frame(
&frame.amplitudes,
&frame.phases,
frame.sequence as u64 * 10_000, // approximate timestamp_us
);
if let Some(reading) = vital_reading {
s.latest_vitals = Some(reading.into());
if let Ok(json) = serde_json::to_string(&s.latest_vitals) {
let _ = s.vitals_tx.send(json);
}
}
// ... existing sensing update logic unchanged ...
}{
"type": "vital_update",
"timestamp": 1709146800.123,
"source": "esp32",
"vitals": {
"respiratory_rate": {
"value_bpm": 16.2,
"confidence": 0.87,
"waveform": [0.12, 0.15, 0.21, ...],
"status": "normal"
},
"heart_rate": {
"value_bpm": 72.5,
"confidence": 0.63,
"waveform": [0.02, 0.03, 0.05, ...],
"status": "normal"
},
"motion_level": "low",
"signal_quality": 0.78
},
"anomalies": [],
"stats": {
"rr_mean": 15.8,
"rr_trend": -0.02,
"hr_mean": 71.3,
"hr_trend": 0.01,
"rr_ema": 16.0,
"hr_ema": 72.1
}
}The wifi-densepose-sensing-server crate's AppStateInner is extended with vital sign state:
struct AppStateInner {
latest_update: Option<SensingUpdate>,
latest_vitals: Option<VitalUpdate>, // NEW
vital_extractor: VitalSignExtractor, // NEW
rssi_history: VecDeque<f64>,
tick: u64,
source: String,
tx: broadcast::Sender<String>,
vitals_tx: broadcast::Sender<String>, // NEW: separate channel for vitals
total_detections: u64,
start_time: std::time::Instant,
}New Axum routes:
Router::new()
.route("/ws/vitals", get(ws_vitals_handler))
.route("/api/v1/vitals/current", get(get_current_vitals))
.route("/api/v1/vitals/history", get(get_vital_history))
.route("/api/v1/vitals/config", get(get_vital_config).put(set_vital_config))The existing SensingTab.js Gaussian splat visualization (ADR-019) is extended with:
- Breathing ring: Already prototyped in
generate_signal_field()as thebreath_ringvariable -- amplitude modulated byvarianceandtick. This is replaced with the actual breathing waveform from the vital sign extractor. - Heart rate indicator: Pulsing opacity overlay synced to estimated heart rate.
- Vital sign panel: Side panel showing HR/RR values, trend sparklines, and anomaly alerts.
wifi-densepose-vitals depends on wifi-densepose-signal for CSI preprocessing and on the rvdna crates for its core algorithms. The dependency graph:
wifi-densepose-vitals
├── wifi-densepose-signal (CSI preprocessing)
├── ruvector-nervous-system (PredictiveLayer, EventBus, Hopfield, GlobalWorkspace)
├── ruvector-attention (subcarrier attention weighting)
├── ruvector-gnn (subcarrier correlation graph)
├── ruvector-coherence (spectral analysis, signal quality)
├── ruvector-temporal-tensor (tiered storage)
├── ruvector-core (VectorDB for pattern matching)
├── ruqu (three-filter coherence gate, adaptive thresholds, drift detection)
└── sona (environment adaptation)
/// Main vital sign extraction engine.
pub struct VitalSignExtractor {
preprocessor: CsiVitalPreprocessor,
weighter: AttentionSubcarrierWeighter,
breathing: BreathingExtractor,
heartrate: HeartRateExtractor,
artifact_rejector: MotionArtifactRejector,
anomaly_detector: VitalAnomalyDetector,
coherence_gate: VitalCoherenceGate, // ruQu three-filter quality gate
store: VitalSignStore,
adapter: SonaVitalAdapter,
config: VitalSignConfig,
}
impl VitalSignExtractor {
/// Create a new extractor with default configuration.
pub fn new(config: VitalSignConfig) -> Self;
/// Process a single CSI frame and return vital sign estimates.
/// Returns None during motion blanking or static environment periods.
pub fn process_frame(
&mut self,
amplitudes: &[f64],
phases: &[f64],
timestamp_us: u64,
) -> Option<VitalReading>;
/// Get current vital sign estimates.
pub fn current(&self) -> VitalStatus;
/// Get historical vital sign data from tiered store.
pub fn history(&mut self, duration_secs: u64) -> Vec<VitalReading>;
/// Get anomaly alerts.
pub fn anomalies(&self) -> Vec<VitalAnomaly>;
/// Get signal quality assessment.
pub fn signal_quality(&self) -> SignalQuality;
}
/// Configuration for vital sign extraction.
pub struct VitalSignConfig {
/// Number of subcarriers to track.
pub n_subcarriers: usize,
/// CSI sampling rate (Hz). Calibrated from ESP32 packet rate.
pub sample_rate_hz: f64,
/// Ring buffer window size (samples).
pub window_size: usize,
/// Breathing band (Hz).
pub breathing_band: (f64, f64),
/// Heart rate band (Hz).
pub heartrate_band: (f64, f64),
/// PredictiveLayer residual threshold.
pub predictive_threshold: f32,
/// Z-score anomaly threshold.
pub anomaly_z_threshold: f64,
/// Motion blanking duration (seconds).
pub motion_blank_secs: f64,
/// Tiered store capacity (bytes).
pub store_capacity: usize,
/// Enable SONA adaptation.
pub enable_adaptation: bool,
}
impl Default for VitalSignConfig {
fn default() -> Self {
Self {
n_subcarriers: 56,
sample_rate_hz: 100.0,
window_size: 1024, // ~10 seconds at 100 Hz
breathing_band: (0.1, 0.5),
heartrate_band: (0.8, 2.0),
predictive_threshold: 0.10,
anomaly_z_threshold: 2.5,
motion_blank_secs: 2.0,
store_capacity: 4 * 1024 * 1024, // 4 MB
enable_adaptation: true,
}
}
}
/// Single vital sign reading at a point in time.
pub struct VitalReading {
pub timestamp_us: u64,
pub respiratory_rate: VitalEstimate,
pub heart_rate: VitalEstimate,
pub motion_level: MotionLevel,
pub signal_quality: f64,
}
/// Estimated vital sign value with confidence.
pub struct VitalEstimate {
pub value_bpm: f64,
pub confidence: f64,
pub waveform_sample: f64,
pub status: VitalStatus,
}
pub enum VitalStatus {
Normal,
Elevated,
Depressed,
Critical,
Unreliable, // Confidence below threshold
Blanked, // Motion artifact blanking
}
pub enum MotionLevel {
Static,
Minimal, // Micro-movements (breathing, heartbeat)
Low, // Small movements (fidgeting)
Moderate, // Walking
High, // Running, exercising
}| Stage | Target Latency | Mechanism |
|---|---|---|
| CSI frame parsing | <50 us | Existing parse_esp32_frame() |
| Predictive gating | <10 us | PredictiveLayer.should_transmit() is a single RMS computation |
| Subcarrier weighting | <100 us | Attention: O(n_subcarriers * dim), GNN: single layer forward |
| Bandpass filtering | <50 us | FIR filter, vectorized |
| Peak detection | <10 us | Simple threshold comparison |
| Anomaly detection | <5 us | Welford online update + CUSUM |
| Tiered store put | <20 us | Quantize + memcpy |
| Total per frame | <250 us | Well within 10ms frame budget at 100 Hz |
The PredictiveLayer from ruvector-nervous-system::routing achieves 90-99% bandwidth reduction on stable signals. For vital sign monitoring where the subject is stationary (the primary use case), most CSI frames are predictable. Only frames with physiological residuals (breathing, heartbeat) pass through, reducing computational load by 10-100x.
| Component | Estimated Memory |
|---|---|
| Ring buffers (56 subcarriers x 1024 samples x 8 bytes) | ~450 KB |
| Attention weights (56 x 64 dim) | ~14 KB |
| GNN layer (56 nodes, single layer) | ~25 KB |
| Hopfield network (128-dim, 100 templates) | ~50 KB |
| TieredStore (4 MB budget) | 4 MB |
| SONA engine (64-dim hidden) | ~10 KB |
| Total | ~4.6 MB |
This fits comfortably within the sensing server's target footprint (ADR-019: ~5 MB RAM for the whole server).
Based on WiFi vital sign literature and the quality of rvdna primitives:
| Metric | Target | Notes |
|---|---|---|
| Respiratory rate error | < 1.5 BPM (median) | Breathing is the easier signal; large chest displacement |
| Heart rate error | < 5 BPM (median) | Harder; requires high SNR, stationary subject |
| Detection latency | < 15 seconds | Time to first reliable estimate after initialization |
| Motion rejection | > 95% true positive | Correctly blanks during gross motion |
| False anomaly rate | < 2% | CUSUM + z-score with conservative thresholds |
- No cloud transmission: All vital sign processing occurs on-device. CSI data and extracted vital signs never leave the local network.
- No PII in CSI: WiFi CSI captures environmental propagation patterns, not biometric identifiers. Vital signs are statistical aggregates (rates), not waveforms that could identify individuals.
- Local storage encryption: The
TieredStorecan be wrapped with at-rest encryption for the cold tier. The existingrvf-cryptocrate in the rvdna workspace provides post-quantum cryptographic primitives (ADR-007). - Access control: REST API endpoints for vital sign history require authentication when deployed in multi-user environments.
- Data retention: Configurable TTL on
TieredStoreblocks. Default: hot tier expires after 5 minutes, warm after 1 hour, cold after 24 hours.
Vital signs extracted from WiFi CSI are not medical devices and should not be used for clinical diagnosis. The system provides wellness-grade monitoring suitable for:
- Occupancy-aware HVAC optimization
- Eldercare activity monitoring (alert on prolonged stillness)
- Sleep quality estimation
- Disaster survivor detection (ADR-001)
Implement simple bandpass filters and FFT peak detection without using rvdna components.
Rejected because: This approach lacks adaptive subcarrier selection, environment calibration, artifact rejection sophistication, and anomaly detection. The resulting system would be fragile across environments and sensor placements. The rvdna components provide production-grade primitives for exactly these challenges.
Extend the existing Python ws_server.py with scipy signal processing.
Rejected because: ADR-020 establishes Rust as the primary backend. Adding vital sign processing in Python contradicts the migration direction and doubles the dependency burden. The rvdna crates are Rust-native and already vendored.
Train a deep learning model to extract vital signs from raw CSI and run it via ONNX Runtime.
Partially adopted: ONNX-based models may be added in Phase 3 as an alternative extractor. However, the primary pipeline uses interpretable signal processing (bandpass + peak detection) because: (a) it works without training data, (b) it is debuggable, (c) it runs on resource-constrained edge devices without ONNX Runtime. The SONA adaptation layer provides learned optimization on top of the interpretable pipeline.
Use dedicated FMCW radar hardware instead of WiFi CSI.
Rejected because: WiFi CSI reuses existing infrastructure (commodity routers, ESP32). No additional hardware is required. The project's core value proposition is infrastructure-free sensing.
- Extends sensing capabilities: The project goes from presence/motion detection to vital sign monitoring without additional hardware.
- Leverages existing investment: Reuses rvdna crates already vendored and understood, avoiding new dependencies.
- Production-grade primitives: PredictiveLayer, TieredStore, CUSUM, Hopfield matching, SONA adaptation are all tested components with known performance characteristics.
- Composable architecture: Each stage is independently testable and replaceable.
- Edge-friendly: 4.6 MB memory footprint and <250 us per-frame latency fit ESP32-class devices.
- Privacy-preserving: Local-only processing with no cloud dependency.
- Signal-to-noise challenge: WiFi-based heart rate detection has inherently low SNR. Confidence scores may frequently be "Unreliable" in noisy environments.
- Calibration requirement: Each deployment environment has different multipath characteristics. SONA adaptation mitigates this but requires an initial calibration period (15-60 seconds).
- Single-person limitation: Multi-person vital sign separation from a single TX-RX pair is an open research problem. This design assumes one dominant subject in the sensing zone.
- Additional crate dependencies: The vital sign module adds 6 rvdna crate dependencies to the workspace, increasing compile time.
- Not medical grade: Cannot replace clinical monitoring devices. Must be clearly labeled as wellness-grade.
- Create
wifi-densepose-vitalscrate with module structure - Implement
CsiVitalPreprocessorwithPredictiveLayergate - Implement
BreathingExtractorwith bandpass filter and peak detection - Implement
VitalAnomalyDetector(portbiomarker_stream::StreamProcessorpattern) - Basic unit tests with synthetic CSI data
- Integration with
wifi-densepose-sensing-serverWebSocket
- Implement
AttentionSubcarrierWeighterusingruvector-attention - Implement
HeartRateExtractorwithModernHopfieldtemplate matching - Implement
MotionArtifactRejectorwithGlobalWorkspacecompetition - Implement
VitalSignStorewithTieredStore - End-to-end integration test with ESP32 CSI data
- Implement
SonaVitalAdapterfor environment calibration - Add GNN-based subcarrier correlation analysis
- Extend UI SensingTab with vital sign visualization
- Add REST API endpoints for vital sign history
- Performance benchmarking and optimization
- CUSUM changepoint detection for apnea/tachycardia alerts
- Multi-environment testing and SONA training
- Security review (data retention, access control)
- Documentation and API reference
- Optional: ONNX-based alternative extractor
The current Windows WiFi mode (--source wifi) uses netsh wlan show interfaces to extract a single RSSI/signal% value per tick. This yields a pseudo-single-subcarrier frame that is insufficient for multi-subcarrier vital sign extraction. However, ruQu and rvdna primitives can still enhance this mode:
| Capability | Mechanism | Quality |
|---|---|---|
| Presence detection | RSSI variance over time via DriftDetector |
Good -- ruQu detects StepChange when a person enters/leaves |
| Coarse breathing estimate | RSSI temporal modulation at 0.1-0.5 Hz | Fair -- single-signal source, needs 30+ seconds of stationary RSSI |
| Environmental drift | AdaptiveThresholds + DriftDetector on RSSI series |
Good -- detects linear trends, step changes, oscillating interference |
| Signal quality gating | ruQu FilterPipeline gates unreliable readings |
Good -- suppresses false readings during WiFi fluctuations |
| Capability | Why Not |
|---|---|
| Heart rate extraction | Requires multi-subcarrier CSI phase coherence (0.1-0.5 mm displacement resolution) |
| Multi-person separation | Single omnidirectional RSSI cannot distinguish spatial sources |
| Subcarrier attention weighting | Only 1 subcarrier available |
| GNN correlation graph | Needs >= 2 subcarrier nodes |
// In windows_wifi_task, after collecting RSSI:
// Feed RSSI time series to a simplified vital pipeline
let mut wifi_vitals = WifiRssiVitalEstimator {
// ruQu adaptive thresholds for RSSI gating
adaptive: AdaptiveThresholds::new(LearningConfig::conservative()),
// Drift detection on RSSI (detects presence events)
drift: DriftDetector::new(60), // 60 samples = ~30 seconds at 2 Hz
// Simple breathing estimator on RSSI temporal modulation
breathing_buffer: RingBuffer::new(120), // 60 seconds of RSSI history
};
// Every tick:
wifi_vitals.breathing_buffer.push(rssi_dbm);
wifi_vitals.drift.push(rssi_dbm);
// Attempt coarse breathing rate from RSSI oscillation
let rr_estimate = wifi_vitals.estimate_breathing_from_rssi();
// Gate quality using ruQu
let verdict = wifi_vitals.adaptive.current_thresholds();
// Only emit if signal quality justifies it
let vitals = VitalReading {
respiratory_rate: VitalEstimate {
value_bpm: rr_estimate.unwrap_or(0.0),
confidence: if rr_estimate.is_some() { 0.3 } else { 0.0 },
status: VitalStatus::Unreliable, // Always marked as low-confidence
..
},
heart_rate: VitalEstimate {
confidence: 0.0,
status: VitalStatus::Unreliable, // Cannot estimate from single RSSI
..
},
..
};Bottom line: Windows WiFi mode gets presence/drift detection and coarse breathing via ruQu's adaptive thresholds and drift detector. For meaningful vital signs (HR, high-confidence RR), ESP32 CSI is required.
The wifi-densepose-wifiscan crate implements the Windows WiFi enhancement strategy described above as a complete 8-stage pipeline (ADR-022 Phase 2). All stages are pure Rust with no external vendor dependencies:
| Stage | Module | Implementation | Tests |
|---|---|---|---|
| 1. Predictive Gating | predictive_gate.rs |
EMA-based residual filter (replaces PredictiveLayer) |
4 |
| 2. Attention Weighting | attention_weighter.rs |
Softmax dot-product attention (replaces ScaledDotProductAttention) |
4 |
| 3. Spatial Correlation | correlator.rs |
Pearson correlation + BFS clustering | 5 |
| 4. Motion Estimation | motion_estimator.rs |
Weighted variance + EMA smoothing | 6 |
| 5. Breathing Extraction | breathing_extractor.rs |
IIR bandpass (0.1-0.5 Hz) + zero-crossing | 6 |
| 6. Quality Gate | quality_gate.rs |
Three-filter (structural/shift/evidence) inspired by ruQu | 8 |
| 7. Fingerprint Matching | fingerprint_matcher.rs |
Cosine similarity templates (replaces ModernHopfield) |
8 |
| 8. Orchestrator | orchestrator.rs |
WindowsWifiPipeline domain service composing stages 1-7 |
7 |
Total: 124 passing tests, 0 failures.
Domain model (Phase 1) includes:
MultiApFrame: Multi-BSSID frame value object with amplitudes, phases, variances, historiesBssidRegistry: Aggregate root managing BSSID lifecycle with Welford running statisticsNetshBssidScanner: Adapter parsingnetsh wlan show networks mode=bssidoutputEnhancedSensingResult: Pipeline output with motion, breathing, posture, quality metrics
The wifi-densepose-vitals crate (ESP32 CSI-grade vital signs) has not yet been implemented. Required for:
- Heart rate extraction from multi-subcarrier CSI phase coherence
- Multi-person vital sign separation
- SONA-based environment adaptation
- VitalSignStore with tiered temporal compression
- Ramsauer et al. (2020). "Hopfield Networks is All You Need." ICLR 2021. (ModernHopfield formulation)
- Fries (2015). "Rhythms for Cognition: Communication through Coherence." Neuron. (OscillatoryRouter basis)
- Bellec et al. (2020). "A solution to the learning dilemma for recurrent networks of spiking neurons." Nature Communications. (E-prop online learning)
- Baars (1988). "A Cognitive Theory of Consciousness." Cambridge UP. (GlobalWorkspace model)
- Liu et al. (2023). "WiFi-based Contactless Breathing and Heart Rate Monitoring." IEEE Sensors Journal.
- Wang et al. (2022). "Robust Vital Signs Monitoring Using WiFi CSI." ACM MobiSys.
- Widar 3.0 (MobiSys 2019). "Zero-Effort Cross-Domain Gesture Recognition with WiFi." (BVP extraction basis)