Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 09f01d5

Browse files
committed
feat(sensing): native macOS CoreWLAN WiFi sensing adapter
Add native macOS LiDAR / WiFi sensing support via CoreWLAN: - mac_wifi.swift: Swift helper to poll RSSI/Noise at 10Hz - MacosWifiCollector: Python adapter for the sensing pipeline - Auto-detect Darwin platform in ws_server.py
1 parent 5124a07 commit 09f01d5

3 files changed

Lines changed: 193 additions & 4 deletions

File tree

v1/src/sensing/mac_wifi.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Foundation
2+
import CoreWLAN
3+
4+
// Output format: JSON lines for easy parsing by Python
5+
// {"timestamp": 1234567.89, "rssi": -50, "noise": -90, "tx_rate": 866.0}
6+
7+
func main() {
8+
guard let interface = CWWiFiClient.shared().interface() else {
9+
fputs("{\"error\": \"No WiFi interface found\"}\n", stderr)
10+
exit(1)
11+
}
12+
13+
// Flush stdout automatically to prevent buffering issues with Python subprocess
14+
setbuf(stdout, nil)
15+
16+
// Run at ~10Hz
17+
let interval: TimeInterval = 0.1
18+
19+
while true {
20+
let timestamp = Date().timeIntervalSince1970
21+
let rssi = interface.rssiValue()
22+
let noise = interface.noiseMeasurement()
23+
let txRate = interface.transmitRate()
24+
25+
let json = """
26+
{"timestamp": \(timestamp), "rssi": \(rssi), "noise": \(noise), "tx_rate": \(txRate)}
27+
"""
28+
print(json)
29+
30+
Thread.sleep(forTimeInterval: interval)
31+
}
32+
}
33+
34+
main()

v1/src/sensing/rssi_collector.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,3 +602,143 @@ def _read_sample(self) -> WifiSample:
602602
retry_count=0,
603603
interface=self._interface,
604604
)
605+
606+
607+
# ---------------------------------------------------------------------------
608+
# macOS WiFi collector (real hardware via Swift CoreWLAN utility)
609+
# ---------------------------------------------------------------------------
610+
611+
class MacosWifiCollector:
612+
"""
613+
Collects real RSSI data from a macOS WiFi interface using a Swift utility.
614+
615+
Data source: A small compiled Swift binary (`mac_wifi`) that polls the
616+
CoreWLAN `CWWiFiClient.shared().interface()` at a high rate.
617+
"""
618+
619+
def __init__(
620+
self,
621+
sample_rate_hz: float = 10.0,
622+
buffer_seconds: int = 120,
623+
) -> None:
624+
self._rate = sample_rate_hz
625+
self._buffer = RingBuffer(max_size=int(sample_rate_hz * buffer_seconds))
626+
self._running = False
627+
self._thread: Optional[threading.Thread] = None
628+
self._process: Optional[subprocess.Popen] = None
629+
self._interface = "en0" # CoreWLAN automatically targets the active Wi-Fi interface
630+
631+
# Compile the Swift utility if the binary doesn't exist
632+
import os
633+
base_dir = os.path.dirname(os.path.abspath(__file__))
634+
self.swift_src = os.path.join(base_dir, "mac_wifi.swift")
635+
self.swift_bin = os.path.join(base_dir, "mac_wifi")
636+
637+
# -- public API ----------------------------------------------------------
638+
639+
@property
640+
def sample_rate_hz(self) -> float:
641+
return self._rate
642+
643+
def start(self) -> None:
644+
if self._running:
645+
return
646+
647+
# Ensure binary exists
648+
import os
649+
if not os.path.exists(self.swift_bin):
650+
logger.info("Compiling mac_wifi.swift to %s", self.swift_bin)
651+
try:
652+
subprocess.run(["swiftc", "-O", "-o", self.swift_bin, self.swift_src], check=True, capture_output=True)
653+
except subprocess.CalledProcessError as e:
654+
raise RuntimeError(f"Failed to compile macOS WiFi utility: {e.stderr.decode('utf-8')}")
655+
except FileNotFoundError:
656+
raise RuntimeError("swiftc is not installed. Please install Xcode Command Line Tools to use native macOS WiFi sensing.")
657+
658+
self._running = True
659+
self._thread = threading.Thread(
660+
target=self._sample_loop, daemon=True, name="mac-rssi-collector"
661+
)
662+
self._thread.start()
663+
logger.info("MacosWifiCollector started at %.1f Hz", self._rate)
664+
665+
def stop(self) -> None:
666+
self._running = False
667+
if self._process:
668+
self._process.terminate()
669+
try:
670+
self._process.wait(timeout=1.0)
671+
except subprocess.TimeoutExpired:
672+
self._process.kill()
673+
self._process = None
674+
675+
if self._thread is not None:
676+
self._thread.join(timeout=2.0)
677+
self._thread = None
678+
logger.info("MacosWifiCollector stopped")
679+
680+
def get_samples(self, n: Optional[int] = None) -> List[WifiSample]:
681+
if n is not None:
682+
return self._buffer.get_last_n(n)
683+
return self._buffer.get_all()
684+
685+
# -- internals -----------------------------------------------------------
686+
687+
def _sample_loop(self) -> None:
688+
import json
689+
690+
# Start the Swift binary
691+
self._process = subprocess.Popen(
692+
[self.swift_bin],
693+
stdout=subprocess.PIPE,
694+
stderr=subprocess.PIPE,
695+
text=True,
696+
bufsize=1 # Line buffered
697+
)
698+
699+
synth_tx = 0
700+
synth_rx = 0
701+
702+
while self._running and self._process and self._process.poll() is None:
703+
try:
704+
line = self._process.stdout.readline()
705+
if not line:
706+
continue
707+
708+
line = line.strip()
709+
if not line:
710+
continue
711+
712+
if line.startswith("{"):
713+
data = json.loads(line)
714+
if "error" in data:
715+
logger.error("macOS WiFi utility error: %s", data["error"])
716+
continue
717+
718+
rssi = float(data.get("rssi", -80.0))
719+
noise = float(data.get("noise", -95.0))
720+
721+
link_quality = max(0.0, min(1.0, (rssi + 100.0) / 60.0))
722+
723+
synth_tx += 1500
724+
synth_rx += 3000
725+
726+
sample = WifiSample(
727+
timestamp=time.time(),
728+
rssi_dbm=rssi,
729+
noise_dbm=noise,
730+
link_quality=link_quality,
731+
tx_bytes=synth_tx,
732+
rx_bytes=synth_rx,
733+
retry_count=0,
734+
interface=self._interface,
735+
)
736+
self._buffer.append(sample)
737+
except Exception as e:
738+
logger.error("Error reading macOS WiFi stream: %s", e)
739+
time.sleep(1.0)
740+
741+
# Process exited unexpectedly
742+
if self._running:
743+
logger.error("macOS WiFi utility exited unexpectedly. Collector stopped.")
744+
self._running = False

v1/src/sensing/ws_server.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
LinuxWifiCollector,
4242
SimulatedCollector,
4343
WindowsWifiCollector,
44+
MacosWifiCollector,
4445
WifiSample,
4546
RingBuffer,
4647
)
@@ -340,12 +341,26 @@ def _create_collector(self):
340341
except Exception as e:
341342
logger.warning("Windows WiFi unavailable (%s), falling back", e)
342343
elif system == "Linux":
344+
# In Docker on Mac, Linux is detected but no wireless extensions exist.
345+
# Force SimulatedCollector if /proc/net/wireless doesn't exist.
346+
import os
347+
if os.path.exists("/proc/net/wireless"):
348+
try:
349+
collector = LinuxWifiCollector(sample_rate_hz=10.0)
350+
self.source = "linux_wifi"
351+
return collector
352+
except RuntimeError:
353+
logger.warning("Linux WiFi unavailable, falling back")
354+
else:
355+
logger.warning("Linux detected but /proc/net/wireless missing (likely Docker). Falling back.")
356+
elif system == "Darwin":
343357
try:
344-
collector = LinuxWifiCollector(sample_rate_hz=10.0)
345-
self.source = "linux_wifi"
358+
collector = MacosWifiCollector(sample_rate_hz=10.0)
359+
logger.info("Using MacosWifiCollector")
360+
self.source = "macos_wifi"
346361
return collector
347-
except RuntimeError:
348-
logger.warning("Linux WiFi unavailable, falling back")
362+
except Exception as e:
363+
logger.warning("macOS WiFi unavailable (%s), falling back", e)
349364

350365
# 3. Simulated
351366
logger.info("Using SimulatedCollector")

0 commit comments

Comments
 (0)