feat(macos): add explicit wifi bridge source
This commit is contained in:
parent
9fecc029bc
commit
9b5db0bead
|
|
@ -25,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- `CrossDomainEvaluator` — 6-metric evaluation protocol (MPJPE in-domain/cross-domain/few-shot/cross-hardware, domain gap ratio, adaptation speedup)
|
||||
- ADR-027: Cross-Environment Domain Generalization — 10 SOTA citations (PerceptAlign, X-Fi ICLR 2025, AM-FM, DGSense, CVPR 2024)
|
||||
- **Cross-platform RSSI adapters** — macOS CoreWLAN (`MacosCoreWlanScanner`) and Linux `iw` (`LinuxIwScanner`) Rust adapters with `#[cfg(target_os)]` gating
|
||||
- macOS CoreWLAN tooling with canonical Swift helper (`tools/macos-wifi-scan/main.swift`)
|
||||
- macOS CoreWLAN bridge tooling with canonical Swift helper (`tools/macos-wifi-scan/main.swift`) and explicit `macos-bridge` UDP source
|
||||
- macOS synthetic BSSID generation (SHA-256 with locally administered MACs) for Sonoma 14.4+ BSSID redaction
|
||||
- Linux `iw dev <iface> scan` parser with freq-to-channel conversion and `scan dump` (no-root) mode
|
||||
- ADR-025: macOS CoreWLAN WiFi Sensing (ORCA)
|
||||
|
|
|
|||
|
|
@ -1614,6 +1614,10 @@ graph TB
|
|||
# Start with Windows WiFi RSSI
|
||||
./target/release/sensing-server --source wifi
|
||||
|
||||
# Start with the experimental macOS bridge fallback
|
||||
python3 scripts/macos_wifi_bridge.py --interval-ms 100 &
|
||||
./target/release/sensing-server --source macos-bridge --tick-ms 100
|
||||
|
||||
# Run vital sign benchmark
|
||||
./target/release/sensing-server --benchmark
|
||||
|
||||
|
|
@ -1629,7 +1633,7 @@ graph TB
|
|||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--source` | Data source: `auto`, `wifi`, `esp32`, `simulate` |
|
||||
| `--source` | Data source: `auto`, `wifi`, `esp32`, `simulate`, `macos-bridge` |
|
||||
| `--http-port` | HTTP port for UI and REST API (default: 8080) |
|
||||
| `--ws-port` | WebSocket port (default: 8765) |
|
||||
| `--udp-port` | UDP port for ESP32 CSI frames (default: 5005) |
|
||||
|
|
|
|||
|
|
@ -245,6 +245,15 @@ All verification on Mac Mini (M2 Pro, macOS 26.3).
|
|||
| Unit: helper not found | `#[test]` with bad path | `WifiScanError::ProcessError` |
|
||||
| Integration: real scan | `cargo test` on Mac Mini | Live observations from CoreWLAN |
|
||||
|
||||
### 5.2.1 Bridge Fallback
|
||||
|
||||
| Test | Command | Expected |
|
||||
|------|---------|----------|
|
||||
| Bridge CLI validation | `python3 scripts/macos_wifi_bridge.py --help` | Shows helper/host/port/interval arguments |
|
||||
| Bridge syntax | `python3 -m py_compile scripts/macos_wifi_bridge.py` | Passes |
|
||||
| Bridge startup order | `python3 scripts/macos_wifi_bridge.py --interval-ms 100 &` then `./target/release/sensing-server --source macos-bridge --tick-ms 100` | Server binds and labels source `wifi-bridge:macos` |
|
||||
| Bridge payload rejection | Send ESP32 binary or malformed JSON | Server logs rejection and keeps waiting |
|
||||
|
||||
### 5.3 End-to-End
|
||||
|
||||
| Step | Command | Verify |
|
||||
|
|
@ -262,6 +271,7 @@ All verification on Mac Mini (M2 Pro, macOS 26.3).
|
|||
- No compiled helper binaries are committed.
|
||||
- Public docs describe macOS as RSSI-only presence/coarse-motion sensing, not CSI parity.
|
||||
- Helper discovery order is documented as env override, repo-local build output, then `PATH`.
|
||||
- `macos-bridge` stays explicit-only and is never auto-selected.
|
||||
- PR includes manual macOS QA evidence because CI is Linux-centric.
|
||||
|
||||
### 5.5 Cross-Platform Regression
|
||||
|
|
|
|||
|
|
@ -251,6 +251,10 @@ export RUVIEW_MAC_WIFI_HELPER="$PWD/rust-port/wifi-densepose-rs/target/tools/mac
|
|||
|
||||
# Run native macOS Wi-Fi sensing
|
||||
./target/release/sensing-server --source wifi --http-port 3000 --ws-port 3001 --tick-ms 500
|
||||
|
||||
# Experimental fallback bridge (explicit, never auto-selected)
|
||||
python3 scripts/macos_wifi_bridge.py --interval-ms 100 &
|
||||
./target/release/sensing-server --source macos-bridge --http-port 3000 --ws-port 3001 --tick-ms 100
|
||||
```
|
||||
|
||||
See [ADR-025](adr/ADR-025-macos-corewlan-wifi-sensing.md) for details.
|
||||
|
|
@ -478,7 +482,7 @@ The Rust sensing server binary accepts the following flags:
|
|||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `--source` | `auto` | Data source: `auto`, `simulate`, `wifi`, `esp32` |
|
||||
| `--source` | `auto` | Data source: `auto`, `simulate`, `wifi`, `esp32`, `macos-bridge` |
|
||||
| `--http-port` | `8080` | HTTP port for REST API and UI |
|
||||
| `--ws-port` | `8765` | WebSocket port |
|
||||
| `--udp-port` | `5005` | UDP port for ESP32 CSI frames |
|
||||
|
|
@ -507,6 +511,10 @@ The Rust sensing server binary accepts the following flags:
|
|||
# Windows WiFi RSSI
|
||||
./target/release/sensing-server --source wifi --tick-ms 500
|
||||
|
||||
# Experimental macOS bridge fallback
|
||||
python3 scripts/macos_wifi_bridge.py --interval-ms 100 &
|
||||
./target/release/sensing-server --source macos-bridge --tick-ms 100
|
||||
|
||||
# Run benchmark
|
||||
./target/release/sensing-server --benchmark
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ struct Args {
|
|||
#[arg(long, default_value = "127.0.0.1", env = "SENSING_BIND_ADDR")]
|
||||
bind_addr: String,
|
||||
|
||||
/// Data source: auto, wifi, esp32, simulate
|
||||
/// Data source: auto, wifi, esp32, simulate, macos-bridge
|
||||
#[arg(long, default_value = "auto")]
|
||||
source: String,
|
||||
|
||||
|
|
@ -148,12 +148,15 @@ struct Args {
|
|||
build_index: Option<String>,
|
||||
}
|
||||
|
||||
const MACOS_BRIDGE_UDP_PORT: u16 = 5006;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum RequestedSource {
|
||||
Auto,
|
||||
Wifi,
|
||||
Esp32,
|
||||
Simulate,
|
||||
MacosBridge,
|
||||
}
|
||||
|
||||
impl RequestedSource {
|
||||
|
|
@ -163,8 +166,9 @@ impl RequestedSource {
|
|||
"wifi" => Ok(Self::Wifi),
|
||||
"esp32" => Ok(Self::Esp32),
|
||||
"simulate" | "simulated" => Ok(Self::Simulate),
|
||||
"macos-bridge" => Ok(Self::MacosBridge),
|
||||
other => Err(format!(
|
||||
"unsupported source '{other}'. Expected one of: auto, wifi, esp32, simulate"
|
||||
"unsupported source '{other}'. Expected one of: auto, wifi, esp32, simulate, macos-bridge"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
@ -175,6 +179,7 @@ enum ResolvedSource {
|
|||
Wifi,
|
||||
Esp32,
|
||||
Simulate,
|
||||
MacosBridge,
|
||||
}
|
||||
|
||||
impl ResolvedSource {
|
||||
|
|
@ -183,6 +188,7 @@ impl ResolvedSource {
|
|||
Self::Wifi => "wifi",
|
||||
Self::Esp32 => "esp32",
|
||||
Self::Simulate => "simulate",
|
||||
Self::MacosBridge => "macos-bridge",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -194,6 +200,22 @@ enum PlatformFlavor {
|
|||
Other,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MacosBridgeRecord {
|
||||
bridge_kind: String,
|
||||
timestamp: f64,
|
||||
interface: String,
|
||||
ssid: String,
|
||||
bssid: String,
|
||||
bssid_synthetic: bool,
|
||||
rssi: f64,
|
||||
noise: f64,
|
||||
channel: u8,
|
||||
band: String,
|
||||
tx_rate_mbps: f64,
|
||||
is_connected: bool,
|
||||
}
|
||||
|
||||
// ── Data types ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// ADR-018 ESP32 CSI binary frame header (20 bytes)
|
||||
|
|
@ -1248,6 +1270,14 @@ fn resolve_explicit_source(
|
|||
"`--source wifi` is only supported on Windows and macOS in this server".to_string(),
|
||||
),
|
||||
},
|
||||
RequestedSource::MacosBridge => match platform {
|
||||
PlatformFlavor::Macos => wifi_available
|
||||
.then_some(ResolvedSource::MacosBridge)
|
||||
.ok_or_else(|| {
|
||||
"`--source macos-bridge` requires a working macOS helper and active Wi-Fi association".to_string()
|
||||
}),
|
||||
_ => Err("`--source macos-bridge` is only supported on macOS".to_string()),
|
||||
},
|
||||
RequestedSource::Auto => Err("internal error: auto source must be resolved separately".into()),
|
||||
}
|
||||
}
|
||||
|
|
@ -1277,6 +1307,7 @@ async fn resolve_source(args: &Args) -> Result<ResolvedSource, String> {
|
|||
PlatformFlavor::Other => {}
|
||||
},
|
||||
ResolvedSource::Simulate => info!(" No hardware detected, using simulation"),
|
||||
ResolvedSource::MacosBridge => {}
|
||||
}
|
||||
Ok(resolved)
|
||||
}
|
||||
|
|
@ -1288,10 +1319,78 @@ async fn resolve_source(args: &Args) -> Result<ResolvedSource, String> {
|
|||
};
|
||||
resolve_explicit_source(requested, platform, wifi_available)
|
||||
}
|
||||
RequestedSource::MacosBridge => {
|
||||
if !matches!(platform, PlatformFlavor::Macos) {
|
||||
return Err("`--source macos-bridge` is only supported on macOS".to_string());
|
||||
}
|
||||
if !probe_macos_wifi().await {
|
||||
return Err(
|
||||
"`--source macos-bridge` requires a working macOS helper and active Wi-Fi association"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if !probe_macos_bridge().await {
|
||||
return Err(format!(
|
||||
"`--source macos-bridge` requires a local NDJSON bridge sender on udp://127.0.0.1:{MACOS_BRIDGE_UDP_PORT}"
|
||||
));
|
||||
}
|
||||
Ok(ResolvedSource::MacosBridge)
|
||||
}
|
||||
other => resolve_explicit_source(other, platform, false),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_macos_bridge_datagram(buf: &[u8]) -> Result<MacosBridgeRecord, String> {
|
||||
if parse_esp32_frame(buf).is_some() {
|
||||
return Err("macos-bridge expects JSON datagrams, not ESP32 binary frames".to_string());
|
||||
}
|
||||
|
||||
let text = std::str::from_utf8(buf)
|
||||
.map_err(|e| format!("macos-bridge datagram must be UTF-8 JSON: {e}"))?;
|
||||
serde_json::from_str::<MacosBridgeRecord>(text.trim())
|
||||
.map_err(|e| format!("invalid macos-bridge JSON payload: {e}"))
|
||||
}
|
||||
|
||||
fn macos_bridge_to_observation(record: MacosBridgeRecord) -> Result<BssidObservation, String> {
|
||||
if record.bridge_kind != "connected_rssi" {
|
||||
return Err(format!(
|
||||
"field `bridge_kind` must equal 'connected_rssi'; got '{}'",
|
||||
record.bridge_kind
|
||||
));
|
||||
}
|
||||
if record.interface.trim().is_empty() {
|
||||
return Err("field `interface` must not be empty".to_string());
|
||||
}
|
||||
if !record.is_connected {
|
||||
return Err("field `is_connected` must be true for macos-bridge records".to_string());
|
||||
}
|
||||
if record.channel == 0 {
|
||||
return Err("field `channel` must be greater than 0".to_string());
|
||||
}
|
||||
|
||||
let band = parse_band_label(&record.band, record.channel)?;
|
||||
let bssid = BssidId::parse(&record.bssid)
|
||||
.map_err(|_| format!("field `bssid` is not a valid MAC address: {}", record.bssid))?;
|
||||
let signal_pct = ((record.rssi + 100.0) * 2.0).clamp(0.0, 100.0);
|
||||
let _ = (
|
||||
record.timestamp,
|
||||
record.bssid_synthetic,
|
||||
record.noise,
|
||||
record.tx_rate_mbps,
|
||||
);
|
||||
|
||||
Ok(BssidObservation {
|
||||
bssid,
|
||||
rssi_dbm: record.rssi,
|
||||
signal_pct,
|
||||
channel: record.channel,
|
||||
band,
|
||||
radio_type: infer_wifi_radio_type(record.channel),
|
||||
ssid: record.ssid,
|
||||
timestamp: Instant::now(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn scan_with_port<T>(scanner: Arc<T>) -> Result<Vec<BssidObservation>, String>
|
||||
where
|
||||
T: WlanScanPort + 'static,
|
||||
|
|
@ -1744,6 +1843,83 @@ async fn macos_wifi_task(_state: SharedState, _tick_ms: u64) {
|
|||
error!("macOS Wi-Fi task requested on a non-macOS build");
|
||||
}
|
||||
|
||||
async fn macos_bridge_task(state: SharedState, tick_ms: u64) {
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], MACOS_BRIDGE_UDP_PORT));
|
||||
let socket = match UdpSocket::bind(addr).await {
|
||||
Ok(socket) => socket,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to bind macOS bridge UDP port {}: {e}",
|
||||
MACOS_BRIDGE_UDP_PORT
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut seq: u32 = 0;
|
||||
let mut registry = BssidRegistry::new(8, 16);
|
||||
let mut pipeline = WindowsWifiPipeline::new();
|
||||
let mut buf = vec![0u8; 4096];
|
||||
|
||||
info!(
|
||||
"macOS bridge listener active on udp://127.0.0.1:{} (expected interval ≈ {} ms)",
|
||||
MACOS_BRIDGE_UDP_PORT, tick_ms
|
||||
);
|
||||
state.write().await.source = "wifi-bridge:macos".to_string();
|
||||
|
||||
loop {
|
||||
let (len, peer) =
|
||||
match tokio::time::timeout(Duration::from_secs(5), socket.recv_from(&mut buf)).await {
|
||||
Ok(Ok(result)) => result,
|
||||
Ok(Err(e)) => {
|
||||
warn!("macOS bridge UDP receive failed: {e}");
|
||||
continue;
|
||||
}
|
||||
Err(_) => {
|
||||
warn!(
|
||||
"macOS bridge listener is waiting for NDJSON packets on udp://127.0.0.1:{}",
|
||||
MACOS_BRIDGE_UDP_PORT
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if !peer.ip().is_loopback() {
|
||||
warn!("Ignoring non-loopback macOS bridge sender: {peer}");
|
||||
continue;
|
||||
}
|
||||
|
||||
let record = match parse_macos_bridge_datagram(&buf[..len]) {
|
||||
Ok(record) => record,
|
||||
Err(e) => {
|
||||
warn!("Rejected macOS bridge payload from {peer}: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let observation = match macos_bridge_to_observation(record) {
|
||||
Ok(observation) => observation,
|
||||
Err(e) => {
|
||||
warn!("Rejected macOS bridge observation from {peer}: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
seq = seq.wrapping_add(1);
|
||||
publish_multi_bssid_tick(
|
||||
&state,
|
||||
&mut registry,
|
||||
&mut pipeline,
|
||||
vec![observation],
|
||||
seq,
|
||||
tick_ms,
|
||||
"wifi-bridge:macos",
|
||||
Some("wifi-bridge:macos"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Probe if Windows WiFi is connected
|
||||
async fn probe_windows_wifi() -> bool {
|
||||
match tokio::process::Command::new("netsh")
|
||||
|
|
@ -1771,6 +1947,22 @@ async fn probe_macos_wifi() -> bool {
|
|||
false
|
||||
}
|
||||
|
||||
async fn probe_macos_bridge() -> bool {
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], MACOS_BRIDGE_UDP_PORT));
|
||||
match UdpSocket::bind(addr).await {
|
||||
Ok(socket) => {
|
||||
let mut buf = [0u8; 4096];
|
||||
match tokio::time::timeout(Duration::from_secs(2), socket.recv_from(&mut buf)).await {
|
||||
Ok(Ok((len, peer))) if peer.ip().is_loopback() => {
|
||||
parse_macos_bridge_datagram(&buf[..len]).is_ok()
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Probe if ESP32 is streaming on UDP port
|
||||
async fn probe_esp32(port: u16) -> bool {
|
||||
let addr = format!("0.0.0.0:{port}");
|
||||
|
|
@ -4119,6 +4311,9 @@ async fn main() {
|
|||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
ResolvedSource::MacosBridge => {
|
||||
tokio::spawn(macos_bridge_task(state.clone(), args.tick_ms));
|
||||
}
|
||||
ResolvedSource::Simulate => {
|
||||
tokio::spawn(simulated_data_task(state.clone(), args.tick_ms));
|
||||
}
|
||||
|
|
@ -4307,4 +4502,40 @@ mod tests {
|
|||
assert!(err.contains("only supported on Windows and macOS"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_parser_accepts_ndjson_payload() {
|
||||
let payload = br#"{"bridge_kind":"connected_rssi","timestamp":1.0,"interface":"en0","ssid":"Lab","bssid":"aa:bb:cc:dd:ee:ff","bssid_synthetic":false,"rssi":-51.0,"noise":-92.0,"channel":44,"band":"5ghz","tx_rate_mbps":400.0,"is_connected":true}"#;
|
||||
let record = parse_macos_bridge_datagram(payload).expect("bridge payload should parse");
|
||||
let obs = macos_bridge_to_observation(record).expect("bridge record should map");
|
||||
|
||||
assert_eq!(obs.ssid, "Lab");
|
||||
assert_eq!(obs.bssid.to_string(), "aa:bb:cc:dd:ee:ff");
|
||||
assert_eq!(obs.channel, 44);
|
||||
assert_eq!(obs.band, BandType::Band5GHz);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_parser_rejects_esp32_frames() {
|
||||
let mut frame = vec![0u8; 22];
|
||||
frame[0..4].copy_from_slice(&0xC511_0001u32.to_le_bytes());
|
||||
frame[4] = 1;
|
||||
frame[5] = 1;
|
||||
frame[6] = 1;
|
||||
frame[8..10].copy_from_slice(&2437u16.to_le_bytes());
|
||||
frame[10..14].copy_from_slice(&1u32.to_le_bytes());
|
||||
frame[20] = 1;
|
||||
frame[21] = 1;
|
||||
|
||||
let err = parse_macos_bridge_datagram(&frame).expect_err("ESP32 binary should be rejected");
|
||||
assert!(err.contains("not ESP32 binary frames"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_parser_rejects_disconnected_records() {
|
||||
let payload = br#"{"bridge_kind":"connected_rssi","timestamp":1.0,"interface":"en0","ssid":"Lab","bssid":"aa:bb:cc:dd:ee:ff","bssid_synthetic":false,"rssi":-51.0,"noise":-92.0,"channel":44,"band":"5ghz","tx_rate_mbps":400.0,"is_connected":false}"#;
|
||||
let record = parse_macos_bridge_datagram(payload).expect("bridge payload should parse");
|
||||
let err = macos_bridge_to_observation(record)
|
||||
.expect_err("disconnected bridge record should be rejected");
|
||||
assert!(err.contains("is_connected"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Forward the canonical macOS CoreWLAN helper stream to the explicit bridge UDP source."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
BRIDGE_KIND = "connected_rssi"
|
||||
DEFAULT_HOST = "127.0.0.1"
|
||||
DEFAULT_PORT = 5006
|
||||
DEFAULT_INTERVAL_MS = 100
|
||||
HELPER_ENV_VAR = "RUVIEW_MAC_WIFI_HELPER"
|
||||
REPO_HELPER_REL = Path("rust-port/wifi-densepose-rs/target/tools/macos-wifi-scan/macos-wifi-scan")
|
||||
|
||||
REQUIRED_FIELDS = {
|
||||
"timestamp",
|
||||
"interface",
|
||||
"ssid",
|
||||
"bssid",
|
||||
"bssid_synthetic",
|
||||
"rssi",
|
||||
"noise",
|
||||
"channel",
|
||||
"band",
|
||||
"tx_rate_mbps",
|
||||
"is_connected",
|
||||
}
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
return Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def positive_int(value: str) -> int:
|
||||
parsed = int(value)
|
||||
if parsed <= 0:
|
||||
raise argparse.ArgumentTypeError("value must be a positive integer")
|
||||
return parsed
|
||||
|
||||
|
||||
def resolve_helper(explicit: str | None) -> str:
|
||||
if explicit:
|
||||
return explicit
|
||||
|
||||
env_override = os.environ.get(HELPER_ENV_VAR)
|
||||
if env_override:
|
||||
return env_override
|
||||
|
||||
repo_helper = repo_root() / REPO_HELPER_REL
|
||||
if repo_helper.is_file():
|
||||
return str(repo_helper)
|
||||
|
||||
return "macos-wifi-scan"
|
||||
|
||||
|
||||
def validate_record(record: object) -> dict[str, object]:
|
||||
if not isinstance(record, dict):
|
||||
raise ValueError("helper output must be a JSON object")
|
||||
|
||||
missing = sorted(REQUIRED_FIELDS.difference(record))
|
||||
if missing:
|
||||
raise ValueError(f"helper output missing required fields: {', '.join(missing)}")
|
||||
|
||||
if not record.get("is_connected", False):
|
||||
raise ValueError("helper stream record is not marked as connected")
|
||||
|
||||
bridged = dict(record)
|
||||
bridged["bridge_kind"] = BRIDGE_KIND
|
||||
return bridged
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Forward macOS CoreWLAN helper records to the explicit RuView macOS bridge source."
|
||||
)
|
||||
parser.add_argument("--helper", help="Path to the macOS Wi-Fi helper binary")
|
||||
parser.add_argument("--host", default=DEFAULT_HOST, help="Bridge receiver host (default: 127.0.0.1)")
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=positive_int,
|
||||
default=DEFAULT_PORT,
|
||||
help="Bridge receiver UDP port (default: 5006)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--interval-ms",
|
||||
type=positive_int,
|
||||
default=DEFAULT_INTERVAL_MS,
|
||||
help="Polling interval passed to the helper stream mode (default: 100)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
helper = resolve_helper(args.helper)
|
||||
command = [helper, "--stream", "--interval-ms", str(args.interval_ms)]
|
||||
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=sys.stderr,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
)
|
||||
except OSError as exc:
|
||||
print(
|
||||
f"failed to start macOS Wi-Fi helper '{helper}': {exc}. "
|
||||
f"Build it with scripts/build-mac-wifi.sh or set {HELPER_ENV_VAR}.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
destination = (args.host, args.port)
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
|
||||
try:
|
||||
assert process.stdout is not None
|
||||
for line in process.stdout:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
try:
|
||||
record = validate_record(json.loads(line))
|
||||
except (json.JSONDecodeError, ValueError) as exc:
|
||||
print(f"skipping helper record: {exc}", file=sys.stderr)
|
||||
continue
|
||||
|
||||
payload = json.dumps(record, separators=(",", ":")).encode("utf-8")
|
||||
sock.sendto(payload, destination)
|
||||
except KeyboardInterrupt:
|
||||
print("stopping macOS Wi-Fi bridge", file=sys.stderr)
|
||||
finally:
|
||||
sock.close()
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=2)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
|
||||
return process.returncode or 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Reference in New Issue