Back to Ruview

ADR-025: macOS CoreWLAN WiFi Sensing via Swift Helper Bridge

docs/adr/ADR-025-macos-corewlan-wifi-sensing.md

0.7.016.8 KB
Original Source

ADR-025: macOS CoreWLAN WiFi Sensing via Swift Helper Bridge

FieldValue
StatusProposed
Date2026-03-01
Decidersruv
CodenameORCA — OS-native Radio Channel Acquisition
Relates toADR-013 (Feature-Level Sensing Commodity Gear), ADR-022 (Windows WiFi Enhanced Fidelity), ADR-014 (SOTA Signal Processing), ADR-018 (ESP32 Dev Implementation)
Issue#56
Build/Test TargetMac Mini (M2 Pro, macOS 26.3)

1. Context

1.1 The Gap: macOS Is a Silent Fallback

The --source auto path in sensing-server probes for ESP32 UDP, then Windows netsh, then falls back to simulated mode. macOS users hit the simulation path silently — there is no macOS WiFi adapter. This is the only major desktop platform without real WiFi sensing support.

1.2 Platform Constraints (macOS 26.3+)

ConstraintDetail
airport CLI removedApple removed /System/Library/PrivateFrameworks/.../airport in macOS 15. No CLI fallback exists.
CoreWLAN is the only pathCWWiFiClient (Swift/ObjC) is the supported API for WiFi scanning. Returns RSSI, channel, SSID, noise, PHY mode, security.
BSSIDs redactedmacOS privacy policy redacts MAC addresses from CWNetwork.bssid unless the app has Location Services + WiFi entitlement. Apps without entitlement see nil for BSSID.
No raw CSIApple does not expose CSI or per-subcarrier data. macOS WiFi sensing is RSSI-only, same tier as Windows netsh.
Scan rateCWInterface.scanForNetworks() takes ~2-4 seconds. Effective rate: ~0.3-0.5 Hz without caching.
PermissionsLocation Services prompt required for BSSID access. Without it, SSID + RSSI + channel still available.

1.3 The Opportunity: Multi-AP RSSI Diversity

Same principle as ADR-022 (Windows): visible APs serve as pseudo-subcarriers. A typical indoor environment exposes 10-30+ SSIDs across 2.4 GHz and 5 GHz bands. Each AP's RSSI responds differently to human movement based on geometry, creating spatial diversity.

SourceEffective SubcarriersSample RateCapabilities
ESP32-S3 (CSI)56-19220 HzFull: pose, vitals, through-wall
Windows netsh (ADR-022)10-30 BSSIDs~2 HzPresence, motion, coarse breathing
macOS CoreWLAN (this ADR)10-30 SSIDs~0.3-0.5 HzPresence, motion

The lower scan rate vs Windows is offset by higher signal quality — CoreWLAN returns calibrated dBm (not percentage) plus noise floor, enabling proper SNR computation.

1.4 Why Swift Subprocess (Not FFI)

ApproachComplexityMaintenanceBuildVerdict
Swift CLI → JSON → stdoutLowIndependent binary, versionableswiftc (ships with Xcode CLT)Chosen
ObjC FFI via cc crateMediumFragile header bindings, ABI churnRequires Xcode headersRejected
objc2 crate (Rust ObjC bridge)HighCoreWLAN not in upstream objc2-frameworksRequires manual class definitionsRejected
swift-bridge crateHighYoung ecosystem, async bridging unsupportedRequires Swift build integration in CargoRejected

The Command::new() + parse JSON pattern is proven — it's exactly what NetshBssidScanner does for Windows. The subprocess boundary also isolates Apple framework dependencies from the Rust build graph.

1.5 SOTA: Platform-Adaptive WiFi Sensing

Recent work validates multi-platform RSSI-based sensing:

  • WiFind (2024): Cross-platform WiFi fingerprinting using RSSI vectors from heterogeneous hardware. Demonstrates that normalization across scan APIs (dBm, percentage, raw) is critical for model portability.
  • WiGesture (2025): RSSI variance-based gesture recognition achieving 89% accuracy on commodity hardware with 15+ APs. Shows that temporal RSSI variance alone carries significant motion information.
  • CrossSense (2024): Transfer learning from CSI-rich hardware to RSSI-only devices. Pre-trained signal features transfer with 78% effectiveness, validating multi-tier hardware strategy.

2. Decision

Implement a macOS CoreWLAN sensing adapter as a Swift helper binary + Rust adapter pair, following the established NetshBssidScanner subprocess pattern from ADR-022. Real RSSI data flows through the existing 8-stage WindowsWifiPipeline (which operates on BssidObservation structs regardless of platform origin).

2.1 Design Principles

  1. Subprocess isolation — Swift binary is a standalone tool, built and versioned independently of the Rust workspace.
  2. Same domain types — macOS adapter produces Vec<BssidObservation>, identical to the Windows path. All downstream processing reuses as-is.
  3. SSID:channel as synthetic BSSID — When real BSSIDs are redacted (no Location Services), sha256(ssid + channel)[:12] generates a stable pseudo-BSSID. Documented limitation: same-SSID same-channel APs collapse to one observation.
  4. #[cfg(target_os = "macos")] gating — macOS-specific code compiles only on macOS. Windows and Linux builds are unaffected.
  5. Graceful degradation — If the Swift helper is not found or fails, --source auto skips macOS WiFi and falls back to simulated mode with a clear warning.

3. Architecture

3.1 Component Overview

┌─────────────────────────────────────────────────────────────────────┐
│                     macOS WiFi Sensing Path                         │
│                                                                     │
│  ┌──────────────────────┐     ┌───────────────────────────────────┐│
│  │  Swift Helper Binary  │     │  Rust Adapter + Existing Pipeline ││
│  │  (tools/macos-wifi-   │     │                                   ││
│  │   scan/main.swift)    │     │  MacosCoreWlanScanner             ││
│  │                       │     │       │                           ││
│  │  CWWiFiClient         │JSON │       ▼                           ││
│  │  scanForNetworks()  ──┼────►│  Vec<BssidObservation>            ││
│  │  interface()          │     │       │                           ││
│  │                       │     │       ▼                           ││
│  │  Outputs:             │     │  BssidRegistry                   ││
│  │  - ssid               │     │       │                           ││
│  │  - rssi (dBm)         │     │       ▼                           ││
│  │  - noise (dBm)        │     │  WindowsWifiPipeline (reused)    ││
│  │  - channel            │     │  [8-stage signal intelligence]   ││
│  │  - band (2.4/5/6)     │     │       │                           ││
│  │  - phy_mode           │     │       ▼                           ││
│  │  - bssid (if avail)   │     │  SensingUpdate → REST/WS         ││
│  └──────────────────────┘     └───────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────┘

3.2 Swift Helper Binary

File: rust-port/wifi-densepose-rs/tools/macos-wifi-scan/main.swift

swift
// Modes:
//   (no args)    → Full scan, output JSON array to stdout
//   --probe      → Quick availability check, output {"available": true/false}
//   --connected  → Connected network info only
//
// Output schema (scan mode):
// [
//   {
//     "ssid": "MyNetwork",
//     "rssi": -52,
//     "noise": -90,
//     "channel": 36,
//     "band": "5GHz",
//     "phy_mode": "802.11ax",
//     "bssid": "aa:bb:cc:dd:ee:ff" | null,
//     "security": "wpa2_personal"
//   }
// ]

Build:

bash
# Requires Xcode Command Line Tools (xcode-select --install)
cd tools/macos-wifi-scan
swiftc -framework CoreWLAN -framework Foundation -O -o macos-wifi-scan main.swift

Build script: tools/macos-wifi-scan/build.sh

3.3 Rust Adapter

File: crates/wifi-densepose-wifiscan/src/adapter/macos_scanner.rs

rust
// #[cfg(target_os = "macos")]

pub struct MacosCoreWlanScanner {
    helper_path: PathBuf,  // Resolved at construction: $PATH or sibling of server binary
}

impl MacosCoreWlanScanner {
    pub fn new() -> Result<Self, WifiScanError>  // Finds helper or errors
    pub fn probe() -> bool                        // Runs --probe, returns availability
    pub fn scan_sync(&self) -> Result<Vec<BssidObservation>, WifiScanError>
    pub fn connected_sync(&self) -> Result<Option<BssidObservation>, WifiScanError>
}

Key mappings:

CoreWLAN fieldBssidObservation fieldTransform
rssi (dBm)signal_dbmDirect (CoreWLAN gives calibrated dBm)
rssi (dBm)amplituderssi_to_amplitude() (existing)
noise (dBm)snrrssi - noise (new field, macOS advantage)
channelchannelDirect
bandbandBandType::from_channel() (existing)
phy_moderadio_typeMap string → RadioType enum
bssidbssid_idDirect if available, else sha256(ssid:channel)[:12]
ssidssidDirect

3.4 Sensing Server Integration

File: crates/wifi-densepose-sensing-server/src/main.rs

FunctionPurpose
probe_macos_wifi()Calls MacosCoreWlanScanner::probe(), returns bool
macos_wifi_task()Async loop: scan → build BssidObservation vec → feed into BssidRegistry + WindowsWifiPipeline → emit SensingUpdate. Same structure as windows_wifi_task().

Auto-detection order (updated):

1. ESP32 UDP probe (port 5005)     → --source esp32
2. Windows netsh probe             → --source wifi (Windows)
3. macOS CoreWLAN probe  [NEW]     → --source wifi (macOS)
4. Simulated fallback              → --source simulated

3.5 Pipeline Reuse

The existing 8-stage WindowsWifiPipeline (ADR-022) operates entirely on BssidObservation / MultiApFrame types:

StageReusable?Notes
1. Predictive GatingYesFilters static APs by temporal variance
2. Attention WeightingYesWeights APs by motion sensitivity
3. Spatial CorrelationYesCross-AP signal correlation
4. Motion EstimationYesRSSI variance → motion level
5. Breathing ExtractionMarginal0.3 Hz scan rate is below Nyquist for breathing (0.1-0.5 Hz). May detect very slow breathing only.
6. Quality GatingYesRejects low-confidence estimates
7. Fingerprint MatchingYesLocation/posture classification
8. OrchestrationYesFuses all stages

Limitation: CoreWLAN scan rate (~0.3-0.5 Hz) is significantly slower than netsh (~2 Hz). Breathing extraction (stage 5) will have reduced accuracy. Motion and presence detection remain effective since they depend on variance over longer windows.


4. Files

4.1 New Files

FilePurposeLines (est.)
tools/macos-wifi-scan/main.swiftCoreWLAN scanner, JSON output~120
tools/macos-wifi-scan/build.shBuild script (swiftc invocation)~15
crates/wifi-densepose-wifiscan/src/adapter/macos_scanner.rsRust adapter: spawn helper, parse JSON, produce BssidObservation~200

4.2 Modified Files

FileChange
crates/wifi-densepose-wifiscan/src/adapter/mod.rsAdd #[cfg(target_os = "macos")] pub mod macos_scanner; + re-export
crates/wifi-densepose-wifiscan/src/lib.rsAdd MacosCoreWlanScanner re-export
crates/wifi-densepose-sensing-server/src/main.rsAdd probe_macos_wifi(), macos_wifi_task(), update auto-detect + --source wifi dispatch

4.3 No New Rust Dependencies

  • std::process::Command — subprocess spawning (stdlib)
  • serde_json — JSON parsing (already in workspace)
  • No changes to Cargo.toml

5. Verification Plan

All verification on Mac Mini (M2 Pro, macOS 26.3).

5.1 Swift Helper

TestCommandExpected
Buildcd tools/macos-wifi-scan && ./build.shProduces macos-wifi-scan binary
Probe./macos-wifi-scan --probe{"available": true}
Scan./macos-wifi-scanJSON array with real SSIDs, RSSI in dBm, channels
Connected./macos-wifi-scan --connectedSingle JSON object for connected network
No WiFiDisable WiFi → ./macos-wifi-scan{"available": false} or empty array

5.2 Rust Adapter

TestMethodExpected
Unit: JSON parsing#[test] with fixture JSONCorrect BssidObservation values
Unit: synthetic BSSID#[test] with nil bssid inputStable sha256(ssid:channel)[:12]
Unit: helper not found#[test] with bad pathWifiScanError::ProcessError
Integration: real scancargo test on Mac MiniLive observations from CoreWLAN

5.3 End-to-End

StepCommandVerify
1cargo build --release (Mac Mini)Clean build, no warnings
2cargo test --workspaceAll existing tests pass + new macOS tests
3./target/release/sensing-server --source wifiServer starts, logs source: wifi (macOS CoreWLAN)
4curl http://localhost:8080/api/v1/sensing/latestsource: "wifi:<SSID>", real RSSI values
5curl http://localhost:8080/api/v1/vital-signsMotion detection responds to physical movement
6Open UI at http://localhost:8080Signal field updates with real RSSI variation
7--source autoAuto-detects macOS WiFi, does not fall back to simulated

5.4 Cross-Platform Regression

PlatformBuildExpected
macOS (Mac Mini)cargo build --releasemacOS adapter compiled, works
Windowscargo build --releasemacOS adapter skipped (#[cfg]), Windows path unchanged
Linuxcargo build --releasemacOS adapter skipped, ESP32/simulated paths unchanged

6. Limitations

LimitationImpactMitigation
BSSID redactionSame-SSID same-channel APs collapse to one observationUse sha256(ssid:channel) as pseudo-BSSID; document edge case. Rare in practice (mesh networks).
Slow scan rate (~0.3 Hz)Breathing extraction unreliable (below Nyquist)Motion/presence still work. Breathing marked low-confidence. Future: cache + connected AP fast-poll hybrid.
Requires Swift helper in PATHExtra build step for source buildsbuild.sh provided. Docker image pre-bundles it. Clear error message when missing.
Location Services for BSSIDFull BSSID requires user permission promptSystem degrades gracefully to SSID:channel pseudo-BSSID without permission.
No CSICannot match ESP32 pose estimation accuracyExpected — this is RSSI-tier sensing (presence + motion). Same limitation as Windows.

7. Future Work

EnhancementDescriptionDepends On
Fast-poll connected APPoll connected AP's RSSI at ~10 Hz via CWInterface.rssiValue() (no full scan needed)CoreWLAN rssiValue() performance testing
Linux iw adapterSame subprocess pattern with iw dev wlan0 scan outputLinux machine for testing
Unified RssiPipeline renameRename WindowsWifiPipelineRssiPipeline to reflect multi-platform useADR-022 update
802.11bf sensingApple may expose CSI via 802.11bf in future macOSApple framework availability
Docker macOS imagePre-built macOS Docker image with Swift helper bundledDocker multi-arch build

8. References