# ADR-025: macOS CoreWLAN WiFi Sensing via Swift Helper Bridge | Field | Value | |-------|-------| | **Status** | Proposed | | **Date** | 2026-03-01 | | **Deciders** | ruv | | **Codename** | **ORCA** — OS-native Radio Channel Acquisition | | **Relates to** | ADR-013 (Feature-Level Sensing Commodity Gear), ADR-022 (Windows WiFi Enhanced Fidelity), ADR-014 (SOTA Signal Processing), ADR-018 (ESP32 Dev Implementation) | | **Issue** | [#56](https://github.com/ruvnet/wifi-densepose/issues/56) | | **Build/Test Target** | Mac 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+) | Constraint | Detail | |------------|--------| | **`airport` CLI removed** | Apple removed `/System/Library/PrivateFrameworks/.../airport` in macOS 15. No CLI fallback exists. | | **CoreWLAN is the only path** | `CWWiFiClient` (Swift/ObjC) is the supported API for WiFi scanning. Returns RSSI, channel, SSID, noise, PHY mode, security. | | **BSSIDs redacted** | macOS 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 CSI** | Apple does not expose CSI or per-subcarrier data. macOS WiFi sensing is RSSI-only, same tier as Windows `netsh`. | | **Scan rate** | `CWInterface.scanForNetworks()` takes ~2-4 seconds. Effective rate: ~0.3-0.5 Hz without caching. | | **Permissions** | Location 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. | Source | Effective Subcarriers | Sample Rate | Capabilities | |--------|----------------------|-------------|-------------| | ESP32-S3 (CSI) | 56-192 | 20 Hz | Full: pose, vitals, through-wall | | Windows `netsh` (ADR-022) | 10-30 BSSIDs | ~2 Hz | Presence, motion, coarse breathing | | **macOS CoreWLAN (this ADR)** | **10-30 SSIDs** | **~0.3-0.5 Hz** | **Presence, 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) | Approach | Complexity | Maintenance | Build | Verdict | |----------|-----------|-------------|-------|---------| | **Swift CLI → JSON → stdout** | Low | Independent binary, versionable | `swiftc` (ships with Xcode CLT) | **Chosen** | | ObjC FFI via `cc` crate | Medium | Fragile header bindings, ABI churn | Requires Xcode headers | Rejected | | `objc2` crate (Rust ObjC bridge) | High | CoreWLAN not in upstream `objc2-frameworks` | Requires manual class definitions | Rejected | | `swift-bridge` crate | High | Young ecosystem, async bridging unsupported | Requires Swift build integration in Cargo | Rejected | 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`, 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 ││ │ │ 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 // Finds helper or errors pub fn probe() -> bool // Runs --probe, returns availability pub fn scan_sync(&self) -> Result, WifiScanError> pub fn connected_sync(&self) -> Result, WifiScanError> } ``` **Key mappings:** | CoreWLAN field | → | BssidObservation field | Transform | |----------------|---|----------------------|-----------| | `rssi` (dBm) | → | `signal_dbm` | Direct (CoreWLAN gives calibrated dBm) | | `rssi` (dBm) | → | `amplitude` | `rssi_to_amplitude()` (existing) | | `noise` (dBm) | → | `snr` | `rssi - noise` (new field, macOS advantage) | | `channel` | → | `channel` | Direct | | `band` | → | `band` | `BandType::from_channel()` (existing) | | `phy_mode` | → | `radio_type` | Map string → `RadioType` enum | | `bssid` | → | `bssid_id` | Direct if available, else `sha256(ssid:channel)[:12]` | | `ssid` | → | `ssid` | Direct | ### 3.4 Sensing Server Integration **File:** `crates/wifi-densepose-sensing-server/src/main.rs` | Function | Purpose | |----------|---------| | `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: | Stage | Reusable? | Notes | |-------|-----------|-------| | 1. Predictive Gating | Yes | Filters static APs by temporal variance | | 2. Attention Weighting | Yes | Weights APs by motion sensitivity | | 3. Spatial Correlation | Yes | Cross-AP signal correlation | | 4. Motion Estimation | Yes | RSSI variance → motion level | | 5. Breathing Extraction | **Marginal** | 0.3 Hz scan rate is below Nyquist for breathing (0.1-0.5 Hz). May detect very slow breathing only. | | 6. Quality Gating | Yes | Rejects low-confidence estimates | | 7. Fingerprint Matching | Yes | Location/posture classification | | 8. Orchestration | Yes | Fuses 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 | File | Purpose | Lines (est.) | |------|---------|-------------| | `tools/macos-wifi-scan/main.swift` | CoreWLAN scanner, JSON output | ~120 | | `tools/macos-wifi-scan/build.sh` | Build script (`swiftc` invocation) | ~15 | | `crates/wifi-densepose-wifiscan/src/adapter/macos_scanner.rs` | Rust adapter: spawn helper, parse JSON, produce `BssidObservation` | ~200 | ### 4.2 Modified Files | File | Change | |------|--------| | `crates/wifi-densepose-wifiscan/src/adapter/mod.rs` | Add `#[cfg(target_os = "macos")] pub mod macos_scanner;` + re-export | | `crates/wifi-densepose-wifiscan/src/lib.rs` | Add `MacosCoreWlanScanner` re-export | | `crates/wifi-densepose-sensing-server/src/main.rs` | Add `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 | Test | Command | Expected | |------|---------|----------| | Build | `cd tools/macos-wifi-scan && ./build.sh` | Produces `macos-wifi-scan` binary | | Probe | `./macos-wifi-scan --probe` | `{"available": true}` | | Scan | `./macos-wifi-scan` | JSON array with real SSIDs, RSSI in dBm, channels | | Connected | `./macos-wifi-scan --connected` | Single JSON object for connected network | | No WiFi | Disable WiFi → `./macos-wifi-scan` | `{"available": false}` or empty array | ### 5.2 Rust Adapter | Test | Method | Expected | |------|--------|----------| | Unit: JSON parsing | `#[test]` with fixture JSON | Correct `BssidObservation` values | | Unit: synthetic BSSID | `#[test]` with nil bssid input | Stable `sha256(ssid:channel)[:12]` | | Unit: helper not found | `#[test]` with bad path | `WifiScanError::ProcessError` | | Integration: real scan | `cargo test` on Mac Mini | Live observations from CoreWLAN | ### 5.3 End-to-End | Step | Command | Verify | |------|---------|--------| | 1 | `cargo build --release` (Mac Mini) | Clean build, no warnings | | 2 | `cargo test --workspace` | All existing tests pass + new macOS tests | | 3 | `./target/release/sensing-server --source wifi` | Server starts, logs `source: wifi (macOS CoreWLAN)` | | 4 | `curl http://localhost:8080/api/v1/sensing/latest` | `source: "wifi:"`, real RSSI values | | 5 | `curl http://localhost:8080/api/v1/vital-signs` | Motion detection responds to physical movement | | 6 | Open UI at `http://localhost:8080` | Signal field updates with real RSSI variation | | 7 | `--source auto` | Auto-detects macOS WiFi, does not fall back to simulated | ### 5.4 Cross-Platform Regression | Platform | Build | Expected | |----------|-------|----------| | macOS (Mac Mini) | `cargo build --release` | macOS adapter compiled, works | | Windows | `cargo build --release` | macOS adapter skipped (`#[cfg]`), Windows path unchanged | | Linux | `cargo build --release` | macOS adapter skipped, ESP32/simulated paths unchanged | --- ## 6. Limitations | Limitation | Impact | Mitigation | |------------|--------|-----------| | **BSSID redaction** | Same-SSID same-channel APs collapse to one observation | Use `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 PATH** | Extra build step for source builds | `build.sh` provided. Docker image pre-bundles it. Clear error message when missing. | | **Location Services for BSSID** | Full BSSID requires user permission prompt | System degrades gracefully to SSID:channel pseudo-BSSID without permission. | | **No CSI** | Cannot match ESP32 pose estimation accuracy | Expected — this is RSSI-tier sensing (presence + motion). Same limitation as Windows. | --- ## 7. Future Work | Enhancement | Description | Depends On | |-------------|-------------|-----------| | **Fast-poll connected AP** | Poll connected AP's RSSI at ~10 Hz via `CWInterface.rssiValue()` (no full scan needed) | CoreWLAN `rssiValue()` performance testing | | **Linux `iw` adapter** | Same subprocess pattern with `iw dev wlan0 scan` output | Linux machine for testing | | **Unified `RssiPipeline` rename** | Rename `WindowsWifiPipeline` → `RssiPipeline` to reflect multi-platform use | ADR-022 update | | **802.11bf sensing** | Apple may expose CSI via 802.11bf in future macOS | Apple framework availability | | **Docker macOS image** | Pre-built macOS Docker image with Swift helper bundled | Docker multi-arch build | --- ## 8. References - [Apple CoreWLAN Documentation](https://developer.apple.com/documentation/corewlan) - [CWWiFiClient](https://developer.apple.com/documentation/corewlan/cwwificlient) — Primary WiFi interface API - [CWNetwork](https://developer.apple.com/documentation/corewlan/cwnetwork) — Scan result type (SSID, RSSI, channel, noise) - [macOS 15 airport removal](https://developer.apple.com/forums/thread/732431) — Apple Developer Forums - ADR-022: Windows WiFi Enhanced Fidelity (analogous platform adapter) - ADR-013: Feature-Level Sensing from Commodity Gear - Issue [#56](https://github.com/ruvnet/wifi-densepose/issues/56): macOS support request