Files
ruvnet--RuView/v2/crates/wifi-densepose-wifiscan/src/adapter/wlanapi_scanner.rs
T
ruv a0e72eef50 feat(wifiscan,sensing): native wlanapi.dll FFI + real Matter manual code
wifiscan (Tier 2 wlanapi adapter ONLY):
- Real native wlanapi.dll BSS-list FFI (new adapter/wlanapi_native.rs):
  WlanOpenHandle -> WlanEnumInterfaces -> WlanGetNetworkBssList ->
  WlanFreeMemory/WlanCloseHandle via windows-sys 0.59 (already in lock
  tree). Per-BSSID RSSI(dBm)/channel/band/radio-type/SSID + CSI-capable
  filter. #[cfg(windows)] real path; #[cfg(not(windows))] returns typed
  WifiScanError::Unsupported (honest, never fabricated).
- wlanapi_scanner now native-first with documented netsh fallback,
  native_scans metric, scan_native()/scan_native_csi_capable(), and a
  benchmark() that MEASURES real Hz (no hardcoded "10x" claim).
- MEASURED 9.74 Hz native on ruvzen (30 iters, Native backend) vs netsh
  ~2 Hz baseline. Live measurement kept as an #[ignore] test.
- Cargo.toml: unsafe_code forbid->deny so only the audited wlan_ffi
  module opts into unsafe; all unsafe confined + null-checked + freed.

sensing-server (Matter commissioning):
- Replaced the lossy modulo placeholder in matter/commissioning.rs with
  the real Matter Core Spec 1.3 §5.1.4.1.1 field-packing. Canonical
  vector (20202021, 3840) now encodes to the published 34970112332.
- Added ManualPairingCode::decode + DecodedManualCode proving the code
  is real/lossless (passcode round-trips bit-for-bit; short
  discriminator = top 4 bits) with Verhoeff integrity, incl. proptest.

Tests: wifi-densepose-wifiscan 145 passed (real FFI exercised on
Windows); wifi-densepose-sensing-server 614 passed. 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-11 21:39:42 -04:00

564 lines
20 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Tier 2: Windows WLAN API adapter with a native `wlanapi.dll` scan path.
//!
//! This adapter prefers the **native** [`wlanapi_native::scan_native`] FFI
//! (`WlanOpenHandle` → `WlanEnumInterfaces` → `WlanGetNetworkBssList`),
//! which reads the driver's cached BSS list with no `netsh.exe`
//! subprocess. The native read path is bounded by WLAN-service IPC rather
//! than a `CreateProcess` per scan (the Tier 1 [`NetshBssidScanner`]'s
//! ~2 Hz ceiling), so polling it in a loop can observe BSSID updates
//! faster. The exact achieved rate is **measured** by
//! [`WlanApiScanner::benchmark`] on the running machine, not assumed —
//! this module makes no fixed "10×" claim.
//!
//! When the native path is unavailable (non-Windows, or the WLAN service
//! returns an error) the adapter transparently falls back to the
//! documented `netsh` Tier 1 scanner, so callers always get a result on
//! Windows and a typed [`WifiScanError::Unsupported`] only where no
//! backend exists.
//!
//! # API
//!
//! - **Sync scan** via [`WlanScanPort`] (native-first, netsh fallback).
//! - **Native-only scan** via [`WlanApiScanner::scan_native`] (no
//! fallback; surfaces the platform gate honestly).
//! - **Async scan** (`"wlanapi"` feature) via `tokio::task::spawn_blocking`.
//! - **Scan metrics** + **measured-rate benchmark**.
//!
//! # Platform
//!
//! Native FFI is Windows-only and lives in [`wlanapi_native`]; the rest of
//! this module compiles everywhere.
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use crate::adapter::netsh_scanner::NetshBssidScanner;
use crate::adapter::wlanapi_native;
use crate::domain::bssid::BssidObservation;
use crate::error::WifiScanError;
use crate::port::WlanScanPort;
// ---------------------------------------------------------------------------
// Scan metrics
// ---------------------------------------------------------------------------
/// Accumulated metrics from scan operations.
#[derive(Debug, Clone)]
pub struct ScanMetrics {
/// Total number of scans performed since creation.
pub scan_count: u64,
/// Total number of BSSIDs observed across all scans.
pub total_bssids_observed: u64,
/// Duration of the most recent scan.
pub last_scan_duration: Option<Duration>,
/// Estimated scan rate in Hz based on the last scan duration.
/// Returns `None` if no scans have been performed yet.
pub estimated_rate_hz: Option<f64>,
/// How many scans so far used the native FFI path (vs the netsh
/// fallback). Lets callers verify the native path is actually live.
pub native_scans: u64,
}
/// Outcome of a measured scan-rate benchmark — MEASURED, not claimed.
#[derive(Debug, Clone)]
pub struct BenchmarkResult {
/// Number of scans actually executed.
pub iterations: u32,
/// Wall-clock time the benchmark took.
pub total: Duration,
/// Measured scans per second over the whole run.
pub rate_hz: f64,
/// Mean BSSIDs observed per scan.
pub mean_bssids: f64,
/// Which backend produced the samples.
pub backend: ScanBackend,
}
/// Which backend serviced a scan.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScanBackend {
/// Native `wlanapi.dll` BSS-list FFI.
Native,
/// `netsh wlan show networks` subprocess fallback.
Netsh,
}
// ---------------------------------------------------------------------------
// WlanApiScanner
// ---------------------------------------------------------------------------
/// Tier 2 WLAN API scanner: native-first with a netsh fallback, plus scan
/// metrics and a measured-rate benchmark.
///
/// # Example (sync)
///
/// ```no_run
/// use wifi_densepose_wifiscan::adapter::wlanapi_scanner::WlanApiScanner;
/// use wifi_densepose_wifiscan::port::WlanScanPort;
///
/// let scanner = WlanApiScanner::new();
/// let observations = scanner.scan().unwrap();
/// for obs in &observations {
/// println!("{}: {} dBm", obs.bssid, obs.rssi_dbm);
/// }
/// // Measure the REAL achieved rate on this machine (no hardcoded claim).
/// if let Ok(bench) = scanner.benchmark(20) {
/// println!("measured {:.1} Hz via {:?}", bench.rate_hz, bench.backend);
/// }
/// ```
pub struct WlanApiScanner {
/// The underlying Tier 1 scanner (fallback path).
inner: NetshBssidScanner,
/// Number of scans performed.
scan_count: AtomicU64,
/// Total BSSIDs observed across all scans.
total_bssids: AtomicU64,
/// Number of scans serviced by the native FFI path.
native_scans: AtomicU64,
/// Timestamp of the most recent scan start (for rate estimation).
last_scan_start: std::sync::Mutex<Option<Instant>>,
/// Duration of the most recent scan.
last_scan_duration: std::sync::Mutex<Option<Duration>>,
}
impl WlanApiScanner {
/// Create a new Tier 2 scanner.
pub fn new() -> Self {
Self {
inner: NetshBssidScanner::new(),
scan_count: AtomicU64::new(0),
total_bssids: AtomicU64::new(0),
native_scans: AtomicU64::new(0),
last_scan_start: std::sync::Mutex::new(None),
last_scan_duration: std::sync::Mutex::new(None),
}
}
/// Return accumulated scan metrics.
pub fn metrics(&self) -> ScanMetrics {
let scan_count = self.scan_count.load(Ordering::Relaxed);
let total_bssids_observed = self.total_bssids.load(Ordering::Relaxed);
let native_scans = self.native_scans.load(Ordering::Relaxed);
let last_scan_duration = *self
.last_scan_duration
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let estimated_rate_hz = last_scan_duration.map(|d| {
let secs = d.as_secs_f64();
if secs > 0.0 {
1.0 / secs
} else {
f64::INFINITY
}
});
ScanMetrics {
scan_count,
total_bssids_observed,
last_scan_duration,
estimated_rate_hz,
native_scans,
}
}
/// Return the number of scans performed so far.
pub fn scan_count(&self) -> u64 {
self.scan_count.load(Ordering::Relaxed)
}
/// Number of scans serviced by the native `wlanapi.dll` FFI path.
pub fn native_scan_count(&self) -> u64 {
self.native_scans.load(Ordering::Relaxed)
}
/// Whether the native path is available on this build/platform.
///
/// `true` on Windows (FFI compiled), `false` elsewhere. Honest report
/// of the platform gate without performing a scan.
pub fn native_available() -> bool {
cfg!(windows)
}
/// Run one native-only scan with **no** netsh fallback.
///
/// Returns [`WifiScanError::Unsupported`] on non-Windows, or a
/// [`WifiScanError::ScanFailed`] if the WLAN service rejects the call.
/// Use this when a caller must know whether the native path worked.
pub fn scan_native(&self) -> Result<Vec<BssidObservation>, WifiScanError> {
let start = Instant::now();
let results = wlanapi_native::scan_native()?;
self.record(start, results.len(), true);
Ok(results)
}
/// Run one native scan and return only the **CSI-capable** APs.
///
/// Filters the native BSS list to access points whose advertised PHY
/// (HT/VHT/HE/EHT) supports channel sounding — the candidates usable as
/// a CSI source. Honest about the platform gate: returns
/// [`WifiScanError::Unsupported`] off-Windows.
pub fn scan_native_csi_capable(&self) -> Result<Vec<BssidObservation>, WifiScanError> {
let all = self.scan_native()?;
Ok(all
.into_iter()
.filter(|obs| wlanapi_native::is_csi_capable(obs.radio_type))
.collect())
}
/// Record metrics for one completed scan.
fn record(&self, start: Instant, bssid_count: usize, native: bool) {
if let Ok(mut guard) = self.last_scan_start.lock() {
*guard = Some(start);
}
let elapsed = start.elapsed();
if let Ok(mut guard) = self.last_scan_duration.lock() {
*guard = Some(elapsed);
}
self.scan_count.fetch_add(1, Ordering::Relaxed);
self.total_bssids
.fetch_add(bssid_count as u64, Ordering::Relaxed);
if native {
self.native_scans.fetch_add(1, Ordering::Relaxed);
}
}
/// Perform a synchronous scan: native FFI first, netsh fallback.
///
/// On Windows this attempts [`wlanapi_native::scan_native`]; if that
/// errors (e.g. WLAN service unavailable) it falls back to the Tier 1
/// netsh scanner. On non-Windows the native path returns `Unsupported`
/// and the netsh fallback is used directly.
fn scan_instrumented(&self) -> Result<Vec<BssidObservation>, WifiScanError> {
let start = Instant::now();
match wlanapi_native::scan_native() {
Ok(results) => {
self.record(start, results.len(), true);
tracing::debug!(
bssid_count = results.len(),
elapsed_ms = start.elapsed().as_millis(),
backend = "native",
"Tier 2 native scan complete"
);
Ok(results)
}
Err(native_err) => {
tracing::debug!(%native_err, "native scan unavailable; falling back to netsh");
let results = self.inner.scan_sync()?;
self.record(start, results.len(), false);
tracing::debug!(
bssid_count = results.len(),
elapsed_ms = start.elapsed().as_millis(),
backend = "netsh",
"Tier 2 netsh fallback scan complete"
);
Ok(results)
}
}
}
/// Measure the **real** achieved scan rate over `iterations` scans.
///
/// This is the honest answer to "how fast is the native path on this
/// box": it runs `iterations` back-to-back scans, times the whole run,
/// and reports scans/second. No rate is hardcoded or extrapolated. The
/// reported [`ScanBackend`] tells you whether the samples came from the
/// native FFI or the netsh fallback.
///
/// # Errors
///
/// Propagates the first scan error; returns
/// [`WifiScanError::ScanFailed`] if `iterations` is 0.
pub fn benchmark(&self, iterations: u32) -> Result<BenchmarkResult, WifiScanError> {
if iterations == 0 {
return Err(WifiScanError::ScanFailed {
reason: "benchmark requires iterations >= 1".to_string(),
});
}
// Decide the backend once up front so the measurement is single-path.
let native_first = wlanapi_native::scan_native();
let (backend, mut total_bssids, mut done) = match &native_first {
Ok(list) => (ScanBackend::Native, list.len() as u64, 1u32),
Err(_) => (ScanBackend::Netsh, 0u64, 0u32),
};
let start = Instant::now();
while done < iterations {
let list = match backend {
ScanBackend::Native => wlanapi_native::scan_native()?,
ScanBackend::Netsh => self.inner.scan_sync()?,
};
total_bssids += list.len() as u64;
done += 1;
}
let total = start.elapsed();
let secs = total.as_secs_f64().max(f64::MIN_POSITIVE);
Ok(BenchmarkResult {
iterations,
total,
rate_hz: f64::from(iterations) / secs,
mean_bssids: total_bssids as f64 / f64::from(iterations),
backend,
})
}
/// Perform an async scan by offloading the blocking call to a
/// background thread (native-first, netsh fallback inside the task).
///
/// Gated behind the `"wlanapi"` feature (requires `tokio`).
///
/// # Errors
///
/// Returns [`WifiScanError::ScanFailed`] if the background task panics
/// or is cancelled, or propagates any error from the underlying scan.
#[cfg(feature = "wlanapi")]
pub async fn scan_async(&self) -> Result<Vec<BssidObservation>, WifiScanError> {
let inner = NetshBssidScanner::new();
let start = Instant::now();
let (results, native) = tokio::task::spawn_blocking(
move || -> Result<(Vec<BssidObservation>, bool), WifiScanError> {
match wlanapi_native::scan_native() {
Ok(r) => Ok((r, true)),
Err(_) => Ok((inner.scan_sync()?, false)),
}
},
)
.await
.map_err(|e| WifiScanError::ScanFailed {
reason: format!("async scan task failed: {e}"),
})??;
self.record(start, results.len(), native);
tracing::debug!(
scan_count = self.scan_count.load(Ordering::Relaxed),
bssid_count = results.len(),
elapsed_ms = start.elapsed().as_millis(),
native,
"Tier 2 async scan complete"
);
Ok(results)
}
}
impl Default for WlanApiScanner {
fn default() -> Self {
Self::new()
}
}
// ---------------------------------------------------------------------------
// WlanScanPort implementation (sync)
// ---------------------------------------------------------------------------
impl WlanScanPort for WlanApiScanner {
fn scan(&self) -> Result<Vec<BssidObservation>, WifiScanError> {
self.scan_instrumented()
}
fn connected(&self) -> Result<Option<BssidObservation>, WifiScanError> {
// Heuristic: strongest visible BSSID is the likely-connected AP.
let mut results = self.scan_instrumented()?;
if results.is_empty() {
return Ok(None);
}
results.sort_by(|a, b| {
b.rssi_dbm
.partial_cmp(&a.rssi_dbm)
.unwrap_or(std::cmp::Ordering::Equal)
});
Ok(Some(results.swap_remove(0)))
}
}
// ===========================================================================
// Tests
// ===========================================================================
#[cfg(test)]
mod tests {
use super::*;
// -- construction ---------------------------------------------------------
#[test]
fn new_creates_scanner_with_zero_metrics() {
let scanner = WlanApiScanner::new();
assert_eq!(scanner.scan_count(), 0);
assert_eq!(scanner.native_scan_count(), 0);
let m = scanner.metrics();
assert_eq!(m.scan_count, 0);
assert_eq!(m.total_bssids_observed, 0);
assert_eq!(m.native_scans, 0);
assert!(m.last_scan_duration.is_none());
assert!(m.estimated_rate_hz.is_none());
}
#[test]
fn default_creates_scanner() {
let scanner = WlanApiScanner::default();
assert_eq!(scanner.scan_count(), 0);
}
// -- native availability is an honest platform gate -----------------------
#[test]
fn native_available_matches_platform() {
assert_eq!(WlanApiScanner::native_available(), cfg!(windows));
}
/// On non-Windows the native-only path must be a typed `Unsupported`.
#[cfg(not(windows))]
#[test]
fn native_scan_unsupported_off_windows() {
let scanner = WlanApiScanner::new();
match scanner.scan_native() {
Err(WifiScanError::Unsupported(_)) => {}
other => panic!("expected Unsupported off-Windows, got {other:?}"),
}
// A failed native-only scan must not bump counters.
assert_eq!(scanner.scan_count(), 0);
assert_eq!(scanner.native_scan_count(), 0);
}
/// On Windows the native-only path runs the real FFI and, on success,
/// records a native scan in the metrics.
#[cfg(windows)]
#[test]
fn native_scan_records_metrics_on_windows() {
let scanner = WlanApiScanner::new();
match scanner.scan_native() {
Ok(_) => {
assert_eq!(scanner.native_scan_count(), 1);
assert_eq!(scanner.scan_count(), 1);
}
// WLAN service off in CI is acceptable; just not Unsupported.
Err(WifiScanError::ScanFailed { .. }) => {}
Err(e) => panic!("unexpected native scan error on Windows: {e:?}"),
}
}
// -- benchmark guards -----------------------------------------------------
#[test]
fn benchmark_rejects_zero_iterations() {
let scanner = WlanApiScanner::new();
assert!(matches!(
scanner.benchmark(0),
Err(WifiScanError::ScanFailed { .. })
));
}
// -- WlanScanPort trait compliance ----------------------------------------
#[test]
fn implements_wlan_scan_port() {
fn assert_port<T: WlanScanPort>() {}
assert_port::<WlanApiScanner>();
}
#[test]
fn implements_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<WlanApiScanner>();
}
// -- metrics structure ----------------------------------------------------
#[test]
fn scan_metrics_debug_display() {
let m = ScanMetrics {
scan_count: 42,
total_bssids_observed: 126,
last_scan_duration: Some(Duration::from_millis(150)),
estimated_rate_hz: Some(1.0 / 0.15),
native_scans: 40,
};
let debug = format!("{m:?}");
assert!(debug.contains("42"));
assert!(debug.contains("126"));
}
#[test]
fn scan_metrics_clone() {
let m = ScanMetrics {
scan_count: 1,
total_bssids_observed: 5,
last_scan_duration: None,
estimated_rate_hz: None,
native_scans: 1,
};
let m2 = m.clone();
assert_eq!(m2.scan_count, 1);
assert_eq!(m2.total_bssids_observed, 5);
assert_eq!(m2.native_scans, 1);
}
#[test]
fn benchmark_result_clone_and_fields() {
let b = BenchmarkResult {
iterations: 10,
total: Duration::from_millis(500),
rate_hz: 20.0,
mean_bssids: 7.0,
backend: ScanBackend::Native,
};
let b2 = b.clone();
assert_eq!(b2.iterations, 10);
assert_eq!(b2.backend, ScanBackend::Native);
assert!((b2.rate_hz - 20.0).abs() < f64::EPSILON);
}
// -- rate estimation ------------------------------------------------------
#[test]
fn estimated_rate_from_known_duration() {
let scanner = WlanApiScanner::new();
{
let mut guard = scanner.last_scan_duration.lock().unwrap();
*guard = Some(Duration::from_millis(100));
}
let m = scanner.metrics();
let rate = m.estimated_rate_hz.unwrap();
assert!((rate - 10.0).abs() < 0.01, "expected ~10 Hz, got {rate}");
}
#[test]
fn estimated_rate_none_before_first_scan() {
let scanner = WlanApiScanner::new();
assert!(scanner.metrics().estimated_rate_hz.is_none());
}
/// MEASURED scan-rate harness. `#[ignore]` so it never runs in CI (it
/// touches the live WLAN service and takes seconds), but
/// `cargo test -p wifi-densepose-wifiscan -- --ignored --nocapture
/// measure_native_scan_rate` prints the *real* Hz on the running box.
/// This is the honest measurement path: the number it prints is what
/// the machine actually achieved, not a hardcoded claim.
#[cfg(windows)]
#[test]
#[ignore = "live WLAN measurement; run explicitly with --ignored --nocapture"]
fn measure_native_scan_rate() {
let scanner = WlanApiScanner::new();
let bench = scanner
.benchmark(30)
.expect("benchmark should run on a Windows box with a WLAN adapter");
println!(
"MEASURED native scan rate: {:.2} Hz over {} iters ({:?} backend), \
mean {:.1} BSSIDs/scan, total {:?}",
bench.rate_hz, bench.iterations, bench.backend, bench.mean_bssids, bench.total
);
assert!(bench.rate_hz > 0.0);
}
}