Skip to content

Latest commit

 

History

History
1357 lines (1121 loc) · 55.1 KB

File metadata and controls

1357 lines (1121 loc) · 55.1 KB

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).

/// 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)

/// 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)

/// 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)

/// 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

/// 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

/// 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

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

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.

/// 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

[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

/// 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:

/// 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

/// 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

// 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

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)

  • Create wifi-densepose-wifiscan crate with DDD module structure
  • Implement BssidId, BssidObservation, BandType, RadioType value objects
  • Implement BssidRegistry aggregate with ring buffer history and Welford stats
  • Implement NetshBssidScanner adapter (parse netsh wlan show networks mode=bssid)
  • Implement MultiApFrame, EnhancedSensingResult, WlanScanPort, error types
  • 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)

#[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:

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:

// 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