- 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>
1358 lines
55 KiB
Markdown
1358 lines
55 KiB
Markdown
# 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(&litudes);
|
||
self.predictive.update(&litudes);
|
||
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(&litudes)
|
||
} 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),
|
||
&litudes,
|
||
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(®istry, &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, ®istry, 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
|
||
&litudes,
|
||
);
|
||
|
||
// 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
|