@@ -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
0 commit comments