Files
wifi-densepose/docs/adr/ADR-022-windows-wifi-enhanced-fidelity-ruvector.md
ruv 3e06970428 feat: Training mode, ADR docs, vitals and wifiscan crates
- Add --train CLI flag with dataset loading, graph transformer training,
  cosine-scheduled SGD, PCK/OKS validation, and checkpoint saving
- Refactor main.rs to import training modules from lib.rs instead of
  duplicating mod declarations
- Add ADR-021 (vital sign detection), ADR-022 (Windows WiFi enhanced
  fidelity), ADR-023 (trained DensePose pipeline) documentation
- Add wifi-densepose-vitals crate: breathing, heartrate, anomaly
  detection, preprocessor, and temporal store
- Add wifi-densepose-wifiscan crate: 8-stage signal intelligence
  pipeline with netsh/wlanapi adapters, multi-BSSID registry,
  attention weighting, spatial correlation, and breathing extraction

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-02-28 23:50:20 -05:00

1358 lines
55 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ADR-022: Enhanced Windows WiFi DensePose Fidelity via RuVector Multi-BSSID Pipeline
| Field | Value |
|-------|-------|
| **Status** | Partially Implemented |
| **Date** | 2026-02-28 |
| **Deciders** | ruv |
| **Relates to** | ADR-013 (Feature-Level Sensing Commodity Gear), ADR-014 (SOTA Signal Processing), ADR-016 (RuVector Integration), ADR-018 (ESP32 Dev Implementation), ADR-021 (Vital Sign Detection) |
---
## 1. Context
### 1.1 The Problem: Single-RSSI Bottleneck
The current Windows WiFi mode in `wifi-densepose-sensing-server` (`:main.rs:382-464`) spawns a `netsh wlan show interfaces` subprocess every 500ms, extracting a single RSSI% value from the connected AP. This creates a pseudo-single-subcarrier `Esp32Frame` with:
- **1 amplitude value** (signal%)
- **0 phase information**
- **~2 Hz effective sampling rate** (process spawn overhead)
- **No spatial diversity** (single observation point)
This is insufficient for any meaningful DensePose estimation. The ESP32 path provides 56 subcarriers with I/Q data at 100+ Hz, while the Windows path provides 1 scalar at 2 Hz -- a **2,800x data deficit**.
### 1.2 The Opportunity: Multi-BSSID Spatial Diversity
A standard Windows WiFi environment exposes **10-30+ BSSIDs** via `netsh wlan show networks mode=bssid`. Testing on the target machine (Intel Wi-Fi 7 BE201 320MHz) reveals:
| Property | Value |
|----------|-------|
| Adapter | Intel Wi-Fi 7 BE201 320MHz (NDIS 6.89) |
| Visible BSSIDs | 23 |
| Bands | 2.4 GHz (channels 3,5,8,11), 5 GHz (channels 36,48) |
| Radio types | 802.11n, 802.11ac, 802.11ax |
| Signal range | 18% to 99% |
Each BSSID travels a different physical path through the environment. A person's body reflects/absorbs/diffracts each path differently depending on the AP's relative position, frequency, and channel. This creates **spatial diversity equivalent to pseudo-subcarriers**.
### 1.3 The Enhancement: Three-Tier Fidelity Improvement
| Tier | Method | Subcarriers | Sample Rate | Implementation |
|------|--------|-------------|-------------|----------------|
| **Current** | `netsh show interfaces` | 1 | ~2 Hz | Subprocess spawn |
| **Tier 1** | `netsh show networks mode=bssid` | 23 | ~2 Hz | Parse multi-BSSID output |
| **Tier 2** | Windows WLAN API (`wlanapi.dll` FFI) | 23 | 10-20 Hz | Native FFI, no subprocess |
| **Tier 3** | Intel Wi-Fi Sensing SDK (802.11bf) | 56+ | 100 Hz | Vendor SDK integration |
This ADR covers Tier 1 and Tier 2. Tier 3 is deferred to a future ADR pending Intel SDK access.
### 1.4 What RuVector Enables
The `vendor/ruvector` crate ecosystem provides signal processing primitives that transform multi-BSSID RSSI vectors into meaningful sensing data:
| RuVector Primitive | Role in Windows WiFi Enhancement |
|---|---|
| `PredictiveLayer` (nervous-system) | Suppresses static BSSIDs (no body interaction), transmits only residual changes. At 23 BSSIDs, 80-95% are typically static. |
| `ScaledDotProductAttention` (attention) | Learns which BSSIDs are most body-sensitive per environment. Attention query = body-motion spectral profile, keys = per-BSSID variance profiles. |
| `RuvectorLayer` (gnn) | Builds cross-correlation graph over BSSIDs. Nodes = BSSIDs, edges = temporal cross-correlation. Message passing identifies BSSID clusters affected by the same person. |
| `OscillatoryRouter` (nervous-system) | Isolates breathing-band (0.1-0.5 Hz) oscillations in multi-BSSID variance for coarse respiratory sensing. |
| `ModernHopfield` (nervous-system) | Template matching for BSSID fingerprint patterns (standing, sitting, walking, empty). |
| `SpectralCoherenceScore` (coherence) | Measures spectral gap in BSSID correlation graph; strong gap = good signal separation. |
| `TieredStore` (temporal-tensor) | Stores multi-BSSID time series with adaptive quantization (8/5/3-bit tiers). |
| `AdaptiveThresholds` (ruQu) | Self-tuning presence/motion thresholds with Welford stats, EMA, outcome-based learning. |
| `DriftDetector` (ruQu) | Detects environmental changes (AP power cycling, furniture movement, new interference sources). 5 drift profiles: Stable, Linear, StepChange, Oscillating, VarianceExpansion. |
| `FilterPipeline` (ruQu) | Three-filter gate (Structural/Shift/Evidence) for signal quality assessment. Only PERMITs readings with statistically rigorous confidence. |
| `SonaEngine` (sona) | Per-environment micro-LoRA adaptation of BSSID weights and filter parameters. |
---
## 2. Decision
Implement an **Enhanced Windows WiFi sensing pipeline** as a new module within the `wifi-densepose-sensing-server` crate (and partially in a new `wifi-densepose-wifiscan` crate), using Domain-Driven Design with bounded contexts. The pipeline scans all visible BSSIDs, constructs multi-dimensional pseudo-CSI frames, and processes them through the RuVector signal pipeline to achieve ESP32-comparable presence/motion detection and coarse vital sign estimation.
### 2.1 Core Design Principles
1. **Multi-BSSID as pseudo-subcarriers**: Each visible BSSID maps to a subcarrier slot in the existing `Esp32Frame` structure, enabling reuse of all downstream signal processing.
2. **Progressive enhancement**: Tier 1 (netsh parsing) ships first with zero new dependencies. Tier 2 (wlanapi FFI) adds `windows-sys` behind a feature flag.
3. **Graceful degradation**: When fewer BSSIDs are visible (<5), the system falls back to single-AP RSSI mode with reduced confidence scores.
4. **Environment learning**: SONA adapts BSSID weights and thresholds per deployment via micro-LoRA, stored in `TieredStore`.
5. **Same API surface**: The output is a standard `SensingUpdate` message, indistinguishable from ESP32 mode to the UI.
---
## 3. Architecture (Domain-Driven Design)
### 3.1 Strategic Design: Bounded Contexts
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ WiFi DensePose Windows Enhancement │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────┐ │
│ │ BSSID Acquisition │ │ Signal Intelligence │ │ Sensing Output │ │
│ │ (Supporting Domain) │ │ (Core Domain) │ │ (Generic Domain) │ │
│ │ │ │ │ │ │ │
│ │ • WlanScanner │ │ • BssidAttention │ │ • FrameBuilder │ │
│ │ • BssidRegistry │ │ • SpatialCorrelator │ │ • UpdateEmitter │ │
│ │ • ScanScheduler │ │ • MotionEstimator │ │ • QualityGate │ │
│ │ • RssiNormalizer │ │ • BreathingExtractor │ │ • HistoryStore │ │
│ │ │ │ • DriftMonitor │ │ │ │
│ │ Port: WlanScanPort │ │ • EnvironmentAdapter │ │ Port: SinkPort │ │
│ │ Adapter: NetshScan │ │ │ │ Adapter: WsSink │ │
│ │ Adapter: WlanApiScan│ │ Port: SignalPort │ │ Adapter: RestSink│ │
│ └──────────────────────┘ └──────────────────────┘ └──────────────────┘ │
│ │ │ │ │
│ │ Anti-Corruption │ Anti-Corruption │ │
│ │ Layer (ACL) │ Layer (ACL) │ │
│ └────────────────────────┘────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ Shared Kernel │ │
│ │ • BssidId, RssiDbm, SignalPercent, ChannelInfo, BandType │ │
│ │ • Esp32Frame (reused as universal frame type) │ │
│ │ • SensingUpdate, FeatureInfo, ClassificationInfo │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
```
### 3.2 Tactical Design: Aggregates and Entities
#### Bounded Context 1: BSSID Acquisition (Supporting Domain)
**Aggregate Root: `BssidRegistry`**
Tracks all visible BSSIDs across scans, maintaining identity stability (BSSIDs appear/disappear as APs beacon).
```rust
/// Value Object: unique BSSID identifier
#[derive(Clone, Hash, Eq, PartialEq)]
pub struct BssidId(pub [u8; 6]); // MAC address
/// Value Object: single BSSID observation
#[derive(Clone, Debug)]
pub struct BssidObservation {
pub bssid: BssidId,
pub rssi_dbm: f64,
pub signal_pct: f64,
pub channel: u8,
pub band: BandType,
pub radio_type: RadioType,
pub ssid: String,
pub timestamp: std::time::Instant,
}
#[derive(Clone, Debug, PartialEq)]
pub enum BandType { Band2_4GHz, Band5GHz, Band6GHz }
#[derive(Clone, Debug, PartialEq)]
pub enum RadioType { N, Ac, Ax, Be }
/// Aggregate Root: tracks all visible BSSIDs
pub struct BssidRegistry {
/// Known BSSIDs with sliding window of observations
entries: HashMap<BssidId, BssidEntry>,
/// Ordered list of BSSID IDs for consistent subcarrier mapping
/// (sorted by first-seen time for stability)
subcarrier_map: Vec<BssidId>,
/// Maximum tracked BSSIDs (maps to max subcarriers)
max_bssids: usize,
}
/// Entity: tracked BSSID with history
pub struct BssidEntry {
pub id: BssidId,
pub meta: BssidMeta,
/// Ring buffer of recent RSSI observations
pub history: RingBuffer<f64>,
/// Welford online stats (mean, variance)
pub stats: RunningStats,
/// Last seen timestamp (for expiry)
pub last_seen: std::time::Instant,
/// Subcarrier index in the pseudo-frame (-1 if unmapped)
pub subcarrier_idx: Option<usize>,
}
```
**Port: `WlanScanPort`** (Hexagonal architecture)
```rust
/// Port: abstracts WiFi scanning backend
#[async_trait::async_trait]
pub trait WlanScanPort: Send + Sync {
/// Perform a scan and return all visible BSSIDs
async fn scan(&self) -> Result<Vec<BssidObservation>>;
/// Get the connected BSSID (if any)
async fn connected(&self) -> Option<BssidObservation>;
/// Trigger an active scan (may not be supported)
async fn trigger_active_scan(&self) -> Result<()>;
}
```
**Adapter 1: `NetshBssidScanner`** (Tier 1)
```rust
/// Tier 1 adapter: parses `netsh wlan show networks mode=bssid`
pub struct NetshBssidScanner;
#[async_trait::async_trait]
impl WlanScanPort for NetshBssidScanner {
async fn scan(&self) -> Result<Vec<BssidObservation>> {
let output = tokio::process::Command::new("netsh")
.args(["wlan", "show", "networks", "mode=bssid"])
.output()
.await?;
let text = String::from_utf8_lossy(&output.stdout);
parse_bssid_scan_output(&text)
}
// ...
}
/// Parse multi-BSSID netsh output into structured observations
fn parse_bssid_scan_output(output: &str) -> Result<Vec<BssidObservation>> {
// Parses blocks like:
// SSID 1 : MyNetwork
// BSSID 1 : aa:bb:cc:dd:ee:ff
// Signal : 84%
// Radio type : 802.11ax
// Band : 2.4 GHz
// Channel : 5
// Returns Vec<BssidObservation> with all fields populated
todo!()
}
```
**Adapter 2: `WlanApiBssidScanner`** (Tier 2, feature-gated)
```rust
/// Tier 2 adapter: uses wlanapi.dll via FFI for 10-20 Hz polling
#[cfg(all(target_os = "windows", feature = "wlanapi"))]
pub struct WlanApiBssidScanner {
handle: WlanHandle,
interface_guid: GUID,
}
#[cfg(all(target_os = "windows", feature = "wlanapi"))]
#[async_trait::async_trait]
impl WlanScanPort for WlanApiBssidScanner {
async fn scan(&self) -> Result<Vec<BssidObservation>> {
// WlanGetNetworkBssList returns WLAN_BSS_LIST with per-BSSID:
// - RSSI (i32, dBm)
// - Link quality (u32, 0-100)
// - Channel (from PHY)
// - BSS type, beacon period, IEs
// Much faster than netsh (~5ms vs ~200ms per call)
let bss_list = unsafe {
wlanapi::WlanGetNetworkBssList(
self.handle.0,
&self.interface_guid,
std::ptr::null(),
wlanapi::dot11_BSS_type_any,
0, // security disabled
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
// ... parse WLAN_BSS_ENTRY structs into BssidObservation
todo!()
}
async fn trigger_active_scan(&self) -> Result<()> {
// WlanScan triggers a fresh scan; results arrive async
unsafe { wlanapi::WlanScan(self.handle.0, &self.interface_guid, ...) };
Ok(())
}
}
```
**Domain Service: `ScanScheduler`**
```rust
/// Coordinates scan timing and BSSID registry updates
pub struct ScanScheduler {
scanner: Box<dyn WlanScanPort>,
registry: BssidRegistry,
/// Scan interval (Tier 1: 500ms, Tier 2: 50-100ms)
interval: Duration,
/// Adaptive scan rate based on motion detection
adaptive_rate: bool,
}
impl ScanScheduler {
/// Run continuous scanning loop, updating registry
pub async fn run(&mut self, frame_tx: mpsc::Sender<MultiApFrame>) {
let mut ticker = tokio::time::interval(self.interval);
loop {
ticker.tick().await;
match self.scanner.scan().await {
Ok(observations) => {
self.registry.update(&observations);
let frame = self.registry.to_pseudo_frame();
let _ = frame_tx.send(frame).await;
}
Err(e) => tracing::warn!("Scan failed: {e}"),
}
}
}
}
```
#### Bounded Context 2: Signal Intelligence (Core Domain)
This is where RuVector primitives compose into a sensing pipeline.
**Domain Service: `WindowsWifiPipeline`**
```rust
/// Core pipeline that transforms multi-BSSID scans into sensing data
pub struct WindowsWifiPipeline {
// ── Stage 1: Predictive Gating ──
/// Suppresses static BSSIDs (no body interaction)
/// ruvector-nervous-system::routing::PredictiveLayer
predictive: PredictiveLayer,
// ── Stage 2: Attention Weighting ──
/// Learns BSSID body-sensitivity per environment
/// ruvector-attention::ScaledDotProductAttention
attention: ScaledDotProductAttention,
// ── Stage 3: Spatial Correlation ──
/// Cross-correlation graph over BSSIDs
/// ruvector-gnn::RuvectorLayer (nodes=BSSIDs, edges=correlation)
correlator: BssidCorrelator,
// ── Stage 4: Motion/Presence Estimation ──
/// Multi-BSSID motion score with per-AP weighting
motion_estimator: MultiApMotionEstimator,
// ── Stage 5: Coarse Vital Signs ──
/// Breathing extraction from body-sensitive BSSID oscillations
/// ruvector-nervous-system::routing::OscillatoryRouter
breathing: CoarseBreathingExtractor,
// ── Stage 6: Quality Gate ──
/// ruQu three-filter pipeline + adaptive thresholds
quality_gate: VitalCoherenceGate,
// ── Stage 7: Fingerprint Matching ──
/// Hopfield template matching for posture classification
/// ruvector-nervous-system::hopfield::ModernHopfield
fingerprint: BssidFingerprintMatcher,
// ── Stage 8: Environment Adaptation ──
/// SONA micro-LoRA per deployment
/// sona::SonaEngine
adapter: SonaEnvironmentAdapter,
// ── Stage 9: Drift Monitoring ──
/// ruQu drift detection per BSSID baseline
drift: Vec<DriftDetector>,
// ── Storage ──
/// Tiered storage for BSSID time series
/// ruvector-temporal-tensor::TieredStore
store: TieredStore,
config: WindowsWifiConfig,
}
```
**Value Object: `WindowsWifiConfig`**
```rust
pub struct WindowsWifiConfig {
/// Maximum BSSIDs to track (default: 32)
pub max_bssids: usize,
/// Scan interval for Tier 1 (default: 500ms)
pub tier1_interval_ms: u64,
/// Scan interval for Tier 2 (default: 50ms)
pub tier2_interval_ms: u64,
/// PredictiveLayer residual threshold (default: 0.05)
pub predictive_threshold: f32,
/// Minimum BSSIDs for multi-AP mode (default: 3)
pub min_bssids: usize,
/// BSSID expiry after no observation (default: 30s)
pub bssid_expiry_secs: u64,
/// Enable coarse breathing extraction (default: true)
pub enable_breathing: bool,
/// Enable fingerprint matching (default: true)
pub enable_fingerprint: bool,
/// Enable SONA adaptation (default: true)
pub enable_adaptation: bool,
/// Breathing band (Hz) — relaxed for low sample rate
pub breathing_band: (f64, f64),
/// Motion variance threshold for presence detection
pub motion_threshold: f64,
}
impl Default for WindowsWifiConfig {
fn default() -> Self {
Self {
max_bssids: 32,
tier1_interval_ms: 500,
tier2_interval_ms: 50,
predictive_threshold: 0.05,
min_bssids: 3,
bssid_expiry_secs: 30,
enable_breathing: true,
enable_fingerprint: true,
enable_adaptation: true,
breathing_band: (0.1, 0.5),
motion_threshold: 0.15,
}
}
}
```
**Domain Service: Stage-by-Stage Processing**
```rust
impl WindowsWifiPipeline {
pub fn process(&mut self, frame: &MultiApFrame) -> Option<EnhancedSensingResult> {
let n = frame.bssid_count;
if n < self.config.min_bssids {
return None; // Too few BSSIDs, degrade to legacy
}
// ── Stage 1: Predictive Gating ──
// Convert RSSI dBm to linear amplitude for PredictiveLayer
let amplitudes: Vec<f32> = frame.rssi_dbm.iter()
.map(|&r| 10.0f32.powf((r as f32 + 100.0) / 20.0))
.collect();
let has_change = self.predictive.should_transmit(&amplitudes);
self.predictive.update(&amplitudes);
if !has_change {
return None; // Environment static, no body present
}
// ── Stage 2: Attention Weighting ──
// Query: variance profile of breathing band per BSSID
// Key: current RSSI variance per BSSID
// Value: amplitude vector
let query = self.compute_breathing_variance_query(frame);
let keys = self.compute_bssid_variance_keys(frame);
let key_refs: Vec<&[f32]> = keys.iter().map(|k| k.as_slice()).collect();
let val_refs: Vec<&[f32]> = amplitudes.chunks(1).collect(); // per-BSSID
let weights = self.attention.compute(&query, &key_refs, &val_refs);
// ── Stage 3: Spatial Correlation ──
// Build correlation graph: edge(i,j) = pearson_r(bssid_i, bssid_j)
let correlation_features = self.correlator.forward(&frame.histories);
// ── Stage 4: Motion Estimation ──
let motion = self.motion_estimator.estimate(
&weights,
&correlation_features,
&frame.per_bssid_variance,
);
// ── Stage 5: Coarse Breathing ──
let breathing = if self.config.enable_breathing && motion.level == MotionLevel::Minimal {
self.breathing.extract_from_weighted_bssids(
&weights,
&frame.histories,
frame.sample_rate_hz,
)
} else {
None
};
// ── Stage 6: Quality Gate (ruQu) ──
let reading = PreliminaryReading {
motion,
breathing,
signal_quality: self.compute_signal_quality(n, &weights),
};
let verdict = self.quality_gate.gate(&reading);
if matches!(verdict, Verdict::Deny) {
return None;
}
// ── Stage 7: Fingerprint Matching ──
let posture = if self.config.enable_fingerprint {
self.fingerprint.classify(&amplitudes)
} else {
None
};
// ── Stage 8: Environment Adaptation ──
if self.config.enable_adaptation {
self.adapter.end_trajectory(reading.signal_quality);
}
// ── Stage 9: Drift Monitoring ──
for (i, drift) in self.drift.iter_mut().enumerate() {
if i < n {
drift.push(frame.rssi_dbm[i]);
}
}
// ── Stage 10: Store ──
let tick = frame.sequence as u64;
self.store.put(
ruvector_temporal_tensor::BlockKey::new(0, tick),
&amplitudes,
ruvector_temporal_tensor::Tier::Hot,
tick,
);
Some(EnhancedSensingResult {
motion,
breathing,
posture,
signal_quality: reading.signal_quality,
bssid_count: n,
verdict,
})
}
}
```
#### Bounded Context 3: Sensing Output (Generic Domain)
**Domain Service: `FrameBuilder`**
Converts `EnhancedSensingResult` to the existing `SensingUpdate` and `Esp32Frame` types for compatibility.
```rust
/// Converts multi-BSSID scan into Esp32Frame for downstream compatibility
pub struct FrameBuilder;
impl FrameBuilder {
pub fn to_esp32_frame(
registry: &BssidRegistry,
observations: &[BssidObservation],
) -> Esp32Frame {
let subcarrier_map = registry.subcarrier_map();
let n_sub = subcarrier_map.len();
let mut amplitudes = vec![0.0f64; n_sub];
let mut phases = vec![0.0f64; n_sub];
for obs in observations {
if let Some(idx) = registry.subcarrier_index(&obs.bssid) {
// Convert RSSI dBm to linear amplitude
amplitudes[idx] = 10.0f64.powf((obs.rssi_dbm + 100.0) / 20.0);
// Phase: encode channel as pseudo-phase (for downstream
// tools that expect phase data)
phases[idx] = (obs.channel as f64 / 48.0) * std::f64::consts::PI;
}
}
Esp32Frame {
magic: 0xC511_0002, // New magic for multi-BSSID frames
node_id: 0,
n_antennas: 1,
n_subcarriers: n_sub as u8,
freq_mhz: 2437, // Mixed; could use median
sequence: 0, // Set by caller
rssi: observations.iter()
.map(|o| o.rssi_dbm as i8)
.max()
.unwrap_or(-90),
noise_floor: -95,
amplitudes,
phases,
}
}
pub fn to_sensing_update(
result: &EnhancedSensingResult,
frame: &Esp32Frame,
registry: &BssidRegistry,
tick: u64,
) -> SensingUpdate {
let nodes: Vec<NodeInfo> = registry.subcarrier_map().iter()
.filter_map(|bssid| registry.get(bssid))
.enumerate()
.map(|(i, entry)| NodeInfo {
node_id: i as u8,
rssi_dbm: entry.stats.mean,
position: estimate_ap_position(entry),
amplitude: vec![frame.amplitudes.get(i).copied().unwrap_or(0.0)],
subcarrier_count: 1,
})
.collect();
SensingUpdate {
msg_type: "sensing_update".to_string(),
timestamp: chrono::Utc::now().timestamp_millis() as f64 / 1000.0,
source: format!("wifi:multi-bssid:{}", result.bssid_count),
tick,
nodes,
features: result.to_feature_info(),
classification: result.to_classification_info(),
signal_field: generate_enhanced_signal_field(result, tick),
}
}
}
```
### 3.3 Module Structure
```
rust-port/wifi-densepose-rs/crates/wifi-densepose-wifiscan/
├── Cargo.toml
└── src/
├── lib.rs # Public API, re-exports
├── domain/
│ ├── mod.rs
│ ├── bssid.rs # BssidId, BssidObservation, BandType, RadioType
│ ├── registry.rs # BssidRegistry aggregate, BssidEntry entity
│ ├── frame.rs # MultiApFrame value object
│ └── result.rs # EnhancedSensingResult, PreliminaryReading
├── port/
│ ├── mod.rs
│ ├── scan_port.rs # WlanScanPort trait
│ └── sink_port.rs # SensingOutputPort trait
├── adapter/
│ ├── mod.rs
│ ├── netsh_scanner.rs # NetshBssidScanner (Tier 1)
│ ├── wlanapi_scanner.rs # WlanApiBssidScanner (Tier 2, feature-gated)
│ └── frame_builder.rs # FrameBuilder (to Esp32Frame / SensingUpdate)
├── pipeline/
│ ├── mod.rs
│ ├── config.rs # WindowsWifiConfig
│ ├── predictive_gate.rs # PredictiveLayer wrapper for multi-BSSID
│ ├── attention_weight.rs # AttentionSubcarrierWeighter for BSSIDs
│ ├── spatial_correlator.rs # GNN-based BSSID correlation
│ ├── motion_estimator.rs # Multi-AP motion/presence estimation
│ ├── breathing.rs # CoarseBreathingExtractor
│ ├── quality_gate.rs # ruQu VitalCoherenceGate
│ ├── fingerprint.rs # ModernHopfield posture fingerprinting
│ ├── drift_monitor.rs # Per-BSSID DriftDetector
│ ├── embedding.rs # BssidEmbedding (SONA micro-LoRA per-BSSID)
│ └── pipeline.rs # WindowsWifiPipeline orchestrator
├── application/
│ ├── mod.rs
│ └── scan_scheduler.rs # ScanScheduler service
└── error.rs # WifiScanError type
```
### 3.4 Cargo.toml Dependencies
```toml
[package]
name = "wifi-densepose-wifiscan"
version = "0.1.0"
edition = "2021"
[features]
default = []
wlanapi = ["windows-sys"] # Tier 2: native WLAN API
full = ["wlanapi"]
[dependencies]
# Internal
wifi-densepose-signal = { path = "../wifi-densepose-signal" }
# RuVector (vendored)
ruvector-nervous-system = { path = "../../../../vendor/ruvector/crates/ruvector-nervous-system" }
ruvector-attention = { path = "../../../../vendor/ruvector/crates/ruvector-attention" }
ruvector-gnn = { path = "../../../../vendor/ruvector/crates/ruvector-gnn" }
ruvector-coherence = { path = "../../../../vendor/ruvector/crates/ruvector-coherence" }
ruvector-temporal-tensor = { path = "../../../../vendor/ruvector/crates/ruvector-temporal-tensor" }
ruvector-core = { path = "../../../../vendor/ruvector/crates/ruvector-core" }
ruqu = { path = "../../../../vendor/ruvector/crates/ruQu" }
sona = { path = "../../../../vendor/ruvector/crates/sona" }
# Async runtime
tokio = { workspace = true }
async-trait = "0.1"
# Serialization
serde = { workspace = true }
serde_json = { workspace = true }
# Logging
tracing = { workspace = true }
# Time
chrono = "0.4"
# Windows native API (Tier 2, optional)
[target.'cfg(target_os = "windows")'.dependencies]
windows-sys = { version = "0.52", features = [
"Win32_NetworkManagement_WiFi",
"Win32_Foundation",
], optional = true }
```
---
## 4. Signal Processing Pipeline Detail
### 4.1 BSSID-to-Subcarrier Mapping
```
Visible BSSIDs (23):
┌──────────────────┬─────┬──────┬──────┬─────────┐
│ BSSID (MAC) │ Ch │ Band │ RSSI │ SubIdx │
├──────────────────┼─────┼──────┼──────┼─────────┤
│ a6:aa:c3:52:1b:28│ 11 │ 2.4G │ -2dBm│ 0 │
│ 82:cd:d6:d6:c3:f5│ 8 │ 2.4G │ -1dBm│ 1 │
│ 16:0a:c5:39:e3:5d│ 5 │ 2.4G │-16dBm│ 2 │
│ 16:27:f5:b2:6b:ae│ 8 │ 2.4G │-17dBm│ 3 │
│ 10:27:f5:b2:6b:ae│ 8 │ 2.4G │-22dBm│ 4 │
│ c8:9e:43:47:a1:3f│ 3 │ 2.4G │-40dBm│ 5 │
│ 90:aa:c3:52:1b:28│ 11 │ 2.4G │ -2dBm│ 6 │
│ ... │ ... │ ... │ ... │ ... │
│ 92:aa:c3:52:1b:20│ 36 │ 5G │ -6dBm│ 20 │
│ c8:9e:43:47:a1:40│ 48 │ 5G │-78dBm│ 21 │
│ ce:9e:43:47:a1:40│ 48 │ 5G │-82dBm│ 22 │
└──────────────────┴─────┴──────┴──────┴─────────┘
Mapping rule: sorted by first-seen time (stable ordering).
New BSSIDs get the next available subcarrier index.
BSSIDs not seen for >30s are expired and their index recycled.
```
### 4.2 Spatial Diversity: Why Multi-BSSID Works
```
┌────[AP1: ch3]
│ │
body │ │ path A (partially blocked)
┌───┐ │ │
│ │──┤ ▼
│ P │ │ ┌──────────┐
│ │──┤ │ WiFi │
└───┘ │ │ Adapter │
│ │ (BE201) │
┌──────┤ └──────────┘
│ │ ▲
[AP2: ch11] │ │ path B (unobstructed)
│ │
└────[AP3: ch36]
│ path C (reflected off wall)
Person P attenuates path A by 3-8 dB, while paths B and C
are unaffected. This differential is the multi-BSSID body signal.
At different body positions/orientations, different AP combinations
show attenuation → spatial diversity ≈ pseudo-subcarrier diversity.
```
### 4.3 RSSI-to-Amplitude Conversion
```rust
/// Convert RSSI dBm to linear amplitude (normalized)
/// RSSI range: -100 dBm (noise) to -20 dBm (very strong)
fn rssi_to_linear(rssi_dbm: f64) -> f64 {
// Map -100..0 dBm to 0..1 linear scale
// Using 10^((rssi+100)/20) gives log-scale amplitude
10.0f64.powf((rssi_dbm + 100.0) / 20.0)
}
/// Convert linear amplitude back to dBm
fn linear_to_rssi(amplitude: f64) -> f64 {
20.0 * amplitude.max(1e-10).log10() - 100.0
}
```
### 4.4 Pseudo-Phase Encoding
Since RSSI provides no phase information, we encode channel and band as a pseudo-phase for downstream tools:
```rust
/// Encode BSSID channel/band as pseudo-phase
/// This preserves frequency-group identity for the GNN correlator
fn encode_pseudo_phase(channel: u8, band: BandType) -> f64 {
let band_offset = match band {
BandType::Band2_4GHz => 0.0,
BandType::Band5GHz => std::f64::consts::PI,
BandType::Band6GHz => std::f64::consts::FRAC_PI_2,
};
// Spread channels across [0, PI) within each band
let ch_phase = (channel as f64 / 48.0) * std::f64::consts::FRAC_PI_2;
band_offset + ch_phase
}
```
---
## 5. RuVector Integration Map
### 5.1 Crate-to-Stage Mapping
| Pipeline Stage | RuVector Crate | Specific Type | Purpose |
|---|---|---|---|
| Predictive Gate | `ruvector-nervous-system` | `PredictiveLayer` | RMS residual gating (threshold 0.05); suppresses scans with no body-caused changes |
| Attention Weight | `ruvector-attention` | `ScaledDotProductAttention` | Query=breathing variance profile, Key=per-BSSID variance, Value=amplitude; outputs per-BSSID importance weights |
| Spatial Correlator | `ruvector-gnn` | `RuvectorLayer` + `LayerNorm` | Correlation graph over BSSIDs; single message-passing layer identifies co-varying BSSID clusters |
| Breathing Extraction | `ruvector-nervous-system` | `OscillatoryRouter` | 0.15 Hz oscillator phase-locks to strongest breathing component in weighted BSSID variance |
| Fingerprint Matching | `ruvector-nervous-system` | `ModernHopfield` | Stores 4 templates: empty-room, standing, sitting, walking; exponential capacity retrieval |
| Signal Quality | `ruvector-coherence` | `SpectralCoherenceScore` | Spectral gap of BSSID correlation graph; higher gap = cleaner body signal |
| Quality Gate | `ruQu` | `FilterPipeline` + `AdaptiveThresholds` | Three-filter PERMIT/DENY/DEFER; self-tunes thresholds with Welford/EMA |
| Drift Monitor | `ruQu` | `DriftDetector` | Per-BSSID baseline tracking; 5 profiles (Stable/Linear/StepChange/Oscillating/VarianceExpansion) |
| Environment Adapt | `sona` | `SonaEngine` | Per-deployment micro-LoRA adaptation of attention weights and filter parameters |
| Tiered Storage | `ruvector-temporal-tensor` | `TieredStore` | 8-bit hot / 5-bit warm / 3-bit cold; 23 BSSIDs × 1024 samples ≈ 24 KB hot |
| Pattern Search | `ruvector-core` | `VectorDB` (HNSW) | BSSID fingerprint nearest-neighbor lookup (<1ms for 1000 templates) |
### 5.2 Data Volume Estimates
| Metric | Tier 1 (netsh) | Tier 2 (wlanapi) |
|---|---|---|
| BSSIDs per scan | 23 | 23 |
| Scan rate | 2 Hz | 20 Hz |
| Samples/sec | 46 | 460 |
| Bytes/sec (raw) | 184 B | 1,840 B |
| Ring buffer memory (1024 samples × 23 BSSIDs × 8 bytes) | 188 KB | 188 KB |
| PredictiveLayer savings | 80-95% suppressed | 90-99% suppressed |
| Net processing rate | 2-9 frames/sec | 2-46 frames/sec |
---
## 6. Expected Fidelity Improvements
### 6.1 Quantitative Targets
| Metric | Current (1 RSSI) | Tier 1 (Multi-BSSID) | Tier 2 (+ Native API) |
|---|---|---|---|
| Presence detection accuracy | ~70% (threshold) | ~88% (multi-AP attention) | ~93% (temporal + spatial) |
| Presence detection latency | 500ms | 500ms | 50ms |
| Motion level classification | 2 levels | 4 levels (static/minimal/moderate/active) | 4 levels + direction |
| Room-level localization | None | Coarse (nearest AP cluster) | Moderate (3-AP trilateration) |
| Breathing rate detection | None | Marginal (0.3 confidence) | Fair (0.5-0.6 confidence) |
| Heart rate detection | None | None | None (need CSI for HR) |
| Posture classification | None | 4 classes (empty/standing/sitting/walking) | 4 classes + confidence |
| Environmental drift resilience | None | Good (ruQu adaptive) | Good (+ SONA adaptation) |
### 6.2 Confidence Score Calibration
```rust
/// Signal quality as a function of BSSID count and variance spread
fn compute_signal_quality(
bssid_count: usize,
attention_weights: &[f32],
spectral_gap: f64,
) -> f64 {
// Factor 1: BSSID diversity (more APs = more spatial info)
let diversity = (bssid_count as f64 / 20.0).min(1.0);
// Factor 2: Attention concentration (body-sensitive BSSIDs dominate)
let max_weight = attention_weights.iter().copied().fold(0.0f32, f32::max);
let mean_weight = attention_weights.iter().sum::<f32>() / attention_weights.len() as f32;
let concentration = (max_weight / mean_weight.max(1e-6) - 1.0).min(5.0) as f64 / 5.0;
// Factor 3: Spectral gap (clean body signal separation)
let separation = spectral_gap.min(1.0);
// Combined quality
(diversity * 0.3 + concentration * 0.4 + separation * 0.3).clamp(0.0, 1.0)
}
```
---
## 7. Integration with Sensing Server
### 7.1 Modified Data Source Selection
```rust
// In main(), extend auto-detection:
let source = match args.source.as_str() {
"auto" => {
if probe_esp32(args.udp_port).await {
"esp32"
} else if probe_multi_bssid().await {
"wifi-enhanced" // NEW: multi-BSSID mode
} else if probe_windows_wifi().await {
"wifi" // Legacy single-RSSI
} else {
"simulate"
}
}
other => other,
};
// Start appropriate background task
match source {
"esp32" => {
tokio::spawn(udp_receiver_task(state.clone(), args.udp_port));
tokio::spawn(broadcast_tick_task(state.clone(), args.tick_ms));
}
"wifi-enhanced" => {
// NEW: multi-BSSID enhanced pipeline
tokio::spawn(enhanced_wifi_task(state.clone(), args.tick_ms));
}
"wifi" => {
tokio::spawn(windows_wifi_task(state.clone(), args.tick_ms));
}
_ => {
tokio::spawn(simulated_data_task(state.clone(), args.tick_ms));
}
}
```
### 7.2 Enhanced WiFi Task
```rust
async fn enhanced_wifi_task(state: SharedState, tick_ms: u64) {
let scanner: Box<dyn WlanScanPort> = {
#[cfg(feature = "wlanapi")]
{ Box::new(WlanApiBssidScanner::new().unwrap_or_else(|_| {
tracing::warn!("WLAN API unavailable, falling back to netsh");
Box::new(NetshBssidScanner)
})) }
#[cfg(not(feature = "wlanapi"))]
{ Box::new(NetshBssidScanner) }
};
let mut registry = BssidRegistry::new(32);
let mut pipeline = WindowsWifiPipeline::new(WindowsWifiConfig::default());
let mut interval = tokio::time::interval(Duration::from_millis(tick_ms));
let mut seq: u32 = 0;
info!("Enhanced WiFi multi-BSSID pipeline active (tick={}ms)", tick_ms);
loop {
interval.tick().await;
seq += 1;
let observations = match scanner.scan().await {
Ok(obs) => obs,
Err(e) => { warn!("Scan failed: {e}"); continue; }
};
registry.update(&observations);
let frame = FrameBuilder::to_esp32_frame(&registry, &observations);
// Run through RuVector-powered pipeline
let multi_frame = registry.to_multi_ap_frame();
let result = pipeline.process(&multi_frame);
let mut s = state.write().await;
s.source = format!("wifi-enhanced:{}", observations.len());
s.tick += 1;
let tick = s.tick;
let update = match result {
Some(r) => FrameBuilder::to_sensing_update(&r, &frame, &registry, tick),
None => {
// Fallback: basic update from frame
let (features, classification) = extract_features_from_frame(&frame);
SensingUpdate {
msg_type: "sensing_update".into(),
timestamp: chrono::Utc::now().timestamp_millis() as f64 / 1000.0,
source: format!("wifi-enhanced:{}", observations.len()),
tick,
nodes: vec![],
features,
classification,
signal_field: generate_signal_field(
frame.rssi as f64, 1.0, 0.05, tick,
),
}
}
};
if let Ok(json) = serde_json::to_string(&update) {
let _ = s.tx.send(json);
}
s.latest_update = Some(update);
}
}
```
---
## 8. Performance Considerations
### 8.1 Latency Budget
| Stage | Tier 1 Latency | Tier 2 Latency | Notes |
|---|---|---|---|
| BSSID scan | ~200ms (netsh) | ~5ms (wlanapi) | Process spawn vs FFI |
| Registry update | <1ms | <1ms | HashMap lookup |
| PredictiveLayer gate | <10us | <10us | 23-element RMS |
| Attention weighting | <50us | <50us | 23×64 matmul |
| GNN correlation | <100us | <100us | 23-node single layer |
| Motion estimation | <20us | <20us | Weighted variance |
| Breathing extraction | <30us | <30us | Bandpass + peak detect |
| ruQu quality gate | <10us | <10us | Three comparisons |
| Fingerprint match | <50us | <50us | Hopfield retrieval |
| **Total per tick** | **~200ms** | **~5ms** | Scan dominates Tier 1 |
### 8.2 Memory Budget
| Component | Memory |
|---|---|
| BssidRegistry (32 entries × history) | ~264 KB |
| PredictiveLayer (32-element) | <1 KB |
| Attention weights | ~8 KB |
| GNN layer | ~12 KB |
| Hopfield (32-dim, 10 templates) | ~3 KB |
| TieredStore (256 KB budget) | 256 KB |
| DriftDetector (32 instances) | ~32 KB |
| **Total** | **~576 KB** |
---
## 9. Security Considerations
- **No raw BSSID data to UI**: Only aggregated sensing updates are broadcast. Individual BSSID MACs, SSIDs, and locations are kept server-side to prevent WiFi infrastructure fingerprinting.
- **BSSID anonymization**: The `NodeInfo.node_id` uses sequential indices, not MAC addresses.
- **Local-only processing**: All signal processing occurs on-device. No scan data is transmitted externally.
- **Scan permission**: `netsh wlan show networks` requires no admin privileges. `WlanGetNetworkBssList` requires the WLAN service to be running (default on Windows).
---
## 10. Alternatives Considered
### Alt 1: Single-AP RSSI Enhancement Only
Improve the current single-RSSI path with better filtering and drift detection, without multi-BSSID.
**Rejected**: A single RSSI value lacks spatial diversity. No amount of temporal filtering can recover spatial information from a 1D signal. Multi-BSSID is the minimum viable path to meaningful presence sensing.
### Alt 2: Monitor Mode / Packet Capture
Put the WiFi adapter into monitor mode to capture raw 802.11 frames with per-subcarrier CSI.
**Rejected for Windows**: Monitor mode requires specialized drivers (nexmon, picoscenes) that are Linux-only for Intel adapters. Windows NDIS does not expose raw CSI. Tier 3 (Intel SDK) is the legitimate Windows path to CSI.
### Alt 3: External USB WiFi Adapter
Use a separate USB adapter in monitor mode on Linux via WSL.
**Rejected**: Adds hardware dependency, WSL USB passthrough complexity, and defeats the "commodity gear, zero setup" value proposition.
### Alt 4: Bluetooth RSSI Augmentation
Scan BLE beacons for additional spatial observations.
**Deferred**: Could complement multi-BSSID but adds BLE scanning complexity. Future enhancement, not core path.
---
## 11. Consequences
### Positive
1. **10-20x data improvement**: From 1 RSSI at 2 Hz to 23 BSSIDs at 2-20 Hz
2. **Spatial awareness**: Different APs provide different body-interaction paths
3. **Reuses existing pipeline**: `Esp32Frame` and `SensingUpdate` are unchanged; UI works without modification
4. **Zero hardware required**: Uses commodity WiFi infrastructure already present
5. **RuVector composition**: Leverages 8 existing crates; ~80% of the intelligence is pre-built
6. **Progressive enhancement**: Tier 1 ships immediately, Tier 2 adds behind feature flag
7. **Environment-adaptive**: SONA + ruQu self-tune per deployment
### Negative
1. **Still no CSI phase**: RSSI-only means no heart rate and limited breathing detection
2. **AP density dependent**: Fewer visible APs = degraded fidelity (min 3 required)
3. **Scan latency**: Tier 1 netsh is slow (~200ms); Tier 2 wlanapi required for real-time
4. **AP mobility**: Moving APs (phones as hotspots) create false motion signals
5. **Cross-platform**: `wlanapi.dll` is Windows-only; Linux/macOS need separate adapters
6. **New crate**: Adds `wifi-densepose-wifiscan` to workspace, increasing compile scope
---
## 12. Implementation Roadmap
### Phase 1: Tier 1 Foundation (Week 1)
- [x] Create `wifi-densepose-wifiscan` crate with DDD module structure
- [x] Implement `BssidId`, `BssidObservation`, `BandType`, `RadioType` value objects
- [x] Implement `BssidRegistry` aggregate with ring buffer history and Welford stats
- [x] Implement `NetshBssidScanner` adapter (parse `netsh wlan show networks mode=bssid`)
- [x] Implement `MultiApFrame`, `EnhancedSensingResult`, `WlanScanPort`, error types
- [x] All 42 unit tests passing (parser, domain types, registry, result types)
- [ ] Implement `FrameBuilder::to_esp32_frame()` (multi-BSSID → pseudo-Esp32Frame)
- [ ] Implement `ScanScheduler` with configurable interval
- [ ] Integration test: scan → registry → pseudo-frame → existing sensing pipeline
- [ ] Wire `enhanced_wifi_task` into sensing server `main()`
### Phase 2: RuVector Signal Pipeline (Weeks 2-3)
- [ ] Implement `PredictiveGate` wrapper over `PredictiveLayer` for multi-BSSID
- [ ] Implement `AttentionSubcarrierWeighter` with breathing-variance query
- [ ] Implement `BssidCorrelator` using `RuvectorLayer` correlation graph
- [ ] Implement `MultiApMotionEstimator` with weighted variance
- [ ] Implement `CoarseBreathingExtractor` with `OscillatoryRouter`
- [ ] Implement `VitalCoherenceGate` (ruQu three-filter pipeline)
- [ ] Implement `BssidFingerprintMatcher` with `ModernHopfield` templates
- [ ] Implement `WindowsWifiPipeline` orchestrator
- [ ] Unit tests with synthetic multi-BSSID data
### Phase 3: Tier 2 + Adaptation (Week 4)
- [ ] Implement `WlanApiBssidScanner` using `windows-sys` FFI
- [ ] Benchmark: netsh vs wlanapi latency
- [ ] Implement `SonaEnvironmentAdapter` for per-deployment learning
- [ ] Implement per-BSSID `DriftDetector` array
- [ ] Implement `TieredStore` wrapper for BSSID time series
- [ ] Performance benchmarking (latency budget validation)
- [ ] End-to-end integration test on real Windows WiFi
### Phase 4: Hardening (Week 5)
- [ ] Signal quality calibration against known ground truth
- [ ] Confidence score validation (presence/motion/breathing)
- [ ] BSSID anonymization in output messages
- [ ] Adaptive scan rate (faster when motion detected)
- [ ] Documentation and API reference
- [ ] Feature flag verification (`wlanapi` on/off)
### Review Errata (Applied)
The following issues were identified during code review against the vendored RuVector source and corrected in this ADR:
| # | Issue | Fix Applied |
|---|---|---|
| 1 | `GnnLayer` does not exist in `ruvector-gnn`; actual export is `RuvectorLayer` | Renamed all references to `RuvectorLayer` |
| 2 | `ScaledDotProductAttention` has no `.forward()` method; actual API is `.compute(query, keys, values)` with `&[&[f32]]` slice-of-slices | Updated Stage 2 code to use `.compute()` with correct parameter types |
| 3 | `SonaEngine::new(SonaConfig{...})` incorrect; actual constructor is `SonaEngine::with_config(config)` and `SonaConfig` uses `micro_lora_lr` not `learning_rate` | Fixed constructor and field names in Section 14 |
| 4 | `apply_micro_lora` returns nothing; actual signature writes into `&mut [f32]` output buffer | Fixed to use mutable output buffer pattern |
| 5 | `TieredStore.put(&data)` missing required params; actual signature: `put(key, data, tier, tick)` | Added `BlockKey`, `Tier`, and `tick` parameters |
| 6 | `WindowsWifiPipeline` mislabeled as "Aggregate Root"; it is a domain service/orchestrator | Relabeled to "Domain Service" |
**Open items from review (not yet addressed):**
- `OscillatoryRouter` is designed for gamma-band (30-90 Hz) neural synchronization; using it at 0.15 Hz for breathing extraction is a semantic stretch. Consider replacing with a dedicated IIR bandpass filter.
- BSSID flapping/index recycling could invalidate GNN correlation graphs; needs explicit invalidation logic.
- `netsh` output is locale-dependent; parser may fail on non-English Windows. Consider positional parsing as fallback.
- Tier 1 breathing detection at 2 Hz is marginal due to subprocess spawn timing jitter; should require Tier 2 for breathing feature.
---
## 13. Testing Strategy
### 13.1 Unit Tests (TDD London School)
```rust
#[cfg(test)]
mod tests {
// Domain: BssidRegistry
#[test]
fn registry_assigns_stable_subcarrier_indices();
#[test]
fn registry_expires_stale_bssids();
#[test]
fn registry_maintains_welford_stats();
// Adapter: NetshBssidScanner
#[test]
fn parse_bssid_scan_output_extracts_all_bssids();
#[test]
fn parse_bssid_scan_output_handles_multi_band();
#[test]
fn parse_bssid_scan_output_handles_empty_output();
// Pipeline: PredictiveGate
#[test]
fn predictive_gate_suppresses_static_environment();
#[test]
fn predictive_gate_transmits_body_caused_changes();
// Pipeline: MotionEstimator
#[test]
fn motion_estimator_detects_presence_from_multi_ap();
#[test]
fn motion_estimator_classifies_four_levels();
// Pipeline: BreathingExtractor
#[test]
fn breathing_extracts_rate_from_oscillating_bssid();
// Integration
#[test]
fn full_pipeline_produces_sensing_update();
#[test]
fn graceful_degradation_with_few_bssids();
}
```
### 13.2 Integration Tests
- Real `netsh` scan on CI Windows runner
- Mock BSSID data for deterministic pipeline testing
- Benchmark: processing latency per tick
---
## 14. Custom BSSID Embeddings with Micro-LoRA (SONA)
### 14.1 The Problem with Raw RSSI Vectors
Raw RSSI values are noisy, device-dependent, and non-stationary. A -50 dBm reading from AP1 on channel 3 is not directly comparable to -50 dBm from AP2 on channel 36 (different propagation, antenna gain, PHY). Feeding raw RSSI into the RuVector pipeline produces suboptimal attention weights and fingerprint matches.
### 14.2 Solution: Learned BSSID Embeddings
Instead of using raw RSSI, we learn a **per-BSSID embedding** that captures each AP's environmental signature using SONA's micro-LoRA adaptation:
```rust
use sona::{SonaEngine, SonaConfig, TrajectoryBuilder};
/// Per-BSSID learned embedding that captures environmental signature
pub struct BssidEmbedding {
/// SONA engine for micro-LoRA parameter adaptation
sona: SonaEngine,
/// Per-BSSID embedding vectors (d_embed dimensions per BSSID)
embeddings: Vec<Vec<f32>>,
/// Embedding dimension
d_embed: usize,
}
impl BssidEmbedding {
pub fn new(max_bssids: usize, d_embed: usize) -> Self {
Self {
sona: SonaEngine::with_config(SonaConfig {
hidden_dim: d_embed,
embedding_dim: d_embed,
micro_lora_lr: 0.001,
ewc_lambda: 100.0, // Prevent forgetting previous environments
..Default::default()
}),
embeddings: vec![vec![0.0; d_embed]; max_bssids],
d_embed,
}
}
/// Encode a BSSID observation into a learned embedding
/// Combines: RSSI, channel, band, radio type, variance, history
pub fn encode(&self, entry: &BssidEntry) -> Vec<f32> {
let mut raw = vec![0.0f32; self.d_embed];
// Static features (learned via micro-LoRA)
raw[0] = rssi_to_linear(entry.stats.mean) as f32;
raw[1] = entry.stats.variance().sqrt() as f32;
raw[2] = channel_to_norm(entry.meta.channel);
raw[3] = band_to_feature(entry.meta.band);
raw[4] = radio_to_feature(entry.meta.radio_type);
// Temporal features (from ring buffer)
if entry.history.len() >= 4 {
raw[5] = entry.history.delta(1) as f32; // 1-step velocity
raw[6] = entry.history.delta(2) as f32; // 2-step velocity
raw[7] = entry.history.trend_slope() as f32;
}
// Apply micro-LoRA adaptation: raw → adapted
let mut adapted = vec![0.0f32; self.d_embed];
self.sona.apply_micro_lora(&raw, &mut adapted);
adapted
}
/// Train embeddings from outcome feedback
/// Called when presence/motion ground truth is available
pub fn train(&mut self, bssid_idx: usize, embedding: &[f32], quality: f32) {
let trajectory = self.sona.begin_trajectory(embedding.to_vec());
self.sona.end_trajectory(trajectory, quality);
// EWC++ prevents catastrophic forgetting of previous environments
}
}
```
### 14.3 Micro-LoRA Adaptation Cycle
```
Scan 1: Raw RSSI [AP1:-42, AP2:-58, AP3:-71, ...]
BssidEmbedding.encode() → [e1, e2, e3, ...] (d_embed=16 per BSSID)
AttentionSubcarrierWeighter (query=breathing_profile, key=embeddings)
Pipeline produces: motion=0.7, breathing=16.2, quality=0.85
User/system feedback: correct=true (person was present)
BssidEmbedding.train(quality=0.85)
SONA micro-LoRA updates embedding weights
EWC++ preserves prior environment learnings
Scan 2: Same raw RSSI → BETTER embeddings → BETTER attention → BETTER output
```
### 14.4 Benefits of Custom Embeddings
| Aspect | Raw RSSI | Learned Embedding |
|---|---|---|
| Device normalization | No | Yes (micro-LoRA adapts per adapter) |
| AP gain compensation | No | Yes (learned per BSSID) |
| Channel/band encoding | Lost | Preserved as features |
| Temporal dynamics | Not captured | Velocity + trend features |
| Cross-environment transfer | No | EWC++ preserves learnings |
| Attention quality | Noisy | Clean (adapted features) |
| Fingerprint matching | Raw distance | Semantically meaningful distance |
### 14.5 Integration with Pipeline Stages
The custom embeddings replace raw RSSI at the attention and fingerprint stages:
```rust
// In WindowsWifiPipeline::process():
// Stage 2 (MODIFIED): Attention on embeddings, not raw RSSI
let bssid_embeddings: Vec<Vec<f32>> = frame.entries.iter()
.map(|entry| self.embedding.encode(entry))
.collect();
let weights = self.attention.forward(
&self.compute_breathing_query(),
&bssid_embeddings, // Learned embeddings, not raw RSSI
&amplitudes,
);
// Stage 7 (MODIFIED): Fingerprint on embedding space
let posture = self.fingerprint.classify_embedding(&bssid_embeddings);
```
---
## Implementation Status (2026-02-28)
### Phase 1: Domain Model -- COMPLETE
- `wifi-densepose-wifiscan` crate created with DDD bounded contexts
- `MultiApFrame` value object with amplitudes, phases, variances, histories
- `BssidRegistry` aggregate root with Welford running statistics (capacity 32, 30s expiry)
- `NetshBssidScanner` adapter parsing `netsh wlan show networks mode=bssid` (56 unit tests)
- `EnhancedSensingResult` output type with motion, breathing, posture, quality
- Hexagonal architecture: `WlanScanPort` trait for adapter abstraction
### Phase 2: Signal Intelligence Pipeline -- COMPLETE
8-stage pure-Rust pipeline with 125 passing tests:
| Stage | Module | Implementation |
|-------|--------|---------------|
| 1 | `predictive_gate` | EMA-based residual filter (replaces `PredictiveLayer`) |
| 2 | `attention_weighter` | Softmax dot-product attention (replaces `ScaledDotProductAttention`) |
| 3 | `correlator` | Pearson correlation + BFS clustering (replaces `RuvectorLayer` GNN) |
| 4 | `motion_estimator` | Weighted variance + EMA smoothing |
| 5 | `breathing_extractor` | IIR bandpass (0.1-0.5 Hz) + zero-crossing |
| 6 | `quality_gate` | Three-filter gate (structural/shift/evidence), inspired by ruQu |
| 7 | `fingerprint_matcher` | Cosine similarity templates (replaces `ModernHopfield`) |
| 8 | `orchestrator` | `WindowsWifiPipeline` domain service |
Performance: ~2.1M frames/sec (debug), ~12M frames/sec (release).
### Phase 3: Server Integration -- IN PROGRESS
- Wiring `WindowsWifiPipeline` into `wifi-densepose-sensing-server`
- Tier 2 `WlanApiScanner` async adapter stub (upgrade path to native WLAN API)
- Extended `SensingUpdate` with enhanced motion, breathing, posture, quality fields
### Phase 4: Tier 2 Native WLAN API -- PLANNED
- Native `wlanapi.dll` FFI for 10-20 Hz scan rates
- SONA adaptation layer for per-environment tuning
- Multi-environment benchmarking
---
## 15. References
- IEEE 802.11bf WiFi Sensing Standard (2024)
- Adib, F. et al. "See Through Walls with WiFi!" SIGCOMM 2013
- Ali, K. et al. "Keystroke Recognition Using WiFi Signals" MobiCom 2015
- Halperin, D. et al. "Tool Release: Gathering 802.11n Traces with Channel State Information" ACM SIGCOMM CCR 2011
- Intel Wi-Fi 7 BE200/BE201 Specifications (2024)
- Microsoft WLAN API Documentation: `WlanGetNetworkBssList`, `WlanScan`
- RuVector v2.0.4 crate documentation