From 7099ee5b86a6dd5c63987d82f82903fbe9f44806 Mon Sep 17 00:00:00 2001 From: Gujiassh Date: Fri, 6 Mar 2026 14:54:06 +0900 Subject: [PATCH] fix: fall back when Linux WiFi probing fails --- v1/src/sensing/ws_server.py | 31 ++++++++++++++------ v1/tests/unit/test_ws_server.py | 51 +++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 v1/tests/unit/test_ws_server.py diff --git a/v1/src/sensing/ws_server.py b/v1/src/sensing/ws_server.py index 8b4448b1..190edcde 100644 --- a/v1/src/sensing/ws_server.py +++ b/v1/src/sensing/ws_server.py @@ -65,6 +65,7 @@ # ESP32 UDP Collector — reads ADR-018 binary frames # --------------------------------------------------------------------------- + class Esp32UdpCollector: """ Collects real CSI data from ESP32 nodes via UDP (ADR-018 binary format). @@ -79,7 +80,7 @@ class Esp32UdpCollector: # ADR-018 header: magic(4) node_id(1) n_ant(1) n_sc(2) freq(4) seq(4) rssi(1) noise(1) reserved(2) MAGIC = 0xC5110001 HEADER_SIZE = 20 - HEADER_FMT = ' None: if self._sock: self._sock.close() self._sock = None - logger.info("Esp32UdpCollector stopped (%d frames received)", self._frames_received) + logger.info( + "Esp32UdpCollector stopped (%d frames received)", self._frames_received + ) def get_samples(self, n: Optional[int] = None) -> List[WifiSample]: if n is not None: @@ -152,8 +155,9 @@ def _parse_and_store(self, raw: bytes, addr) -> None: if len(raw) < self.HEADER_SIZE: return - magic, node_id, n_ant, n_sc, freq_mhz, seq, rssi_u8, noise_u8 = \ + magic, node_id, n_ant, n_sc, freq_mhz, seq, rssi_u8, noise_u8 = ( struct.unpack_from(self.HEADER_FMT, raw, 0) + ) if magic != self.MAGIC: return @@ -167,10 +171,10 @@ def _parse_and_store(self, raw: bytes, addr) -> None: amplitude_list = [] if len(raw) >= iq_bytes_needed and iq_count > 0: - iq_raw = struct.unpack_from(f'<{iq_count * 2}b', raw, self.HEADER_SIZE) + iq_raw = struct.unpack_from(f"<{iq_count * 2}b", raw, self.HEADER_SIZE) i_vals = np.array(iq_raw[0::2], dtype=np.float64) q_vals = np.array(iq_raw[1::2], dtype=np.float64) - amplitudes = np.sqrt(i_vals ** 2 + q_vals ** 2) + amplitudes = np.sqrt(i_vals**2 + q_vals**2) mean_amp = float(np.mean(amplitudes)) amplitude_list = amplitudes.tolist() else: @@ -216,6 +220,7 @@ def _parse_and_store(self, raw: bytes, addr) -> None: # Probe for ESP32 UDP # --------------------------------------------------------------------------- + def probe_esp32_udp(port: int = ESP32_UDP_PORT, timeout: float = 2.0) -> bool: """Return True if an ESP32 is actively streaming on the UDP port.""" sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -225,7 +230,7 @@ def probe_esp32_udp(port: int = ESP32_UDP_PORT, timeout: float = 2.0) -> bool: sock.bind(("0.0.0.0", port)) data, _ = sock.recvfrom(256) if len(data) >= 20: - magic = struct.unpack_from(' bool: # Signal field generator # --------------------------------------------------------------------------- + def generate_signal_field( features: RssiFeatures, result: SensingResult, @@ -309,6 +315,7 @@ def generate_signal_field( # WebSocket server # --------------------------------------------------------------------------- + class SensingWebSocketServer: """Async WebSocket server that broadcasts sensing updates.""" @@ -344,15 +351,20 @@ def _create_collector(self): # In Docker on Mac, Linux is detected but no wireless extensions exist. # Force SimulatedCollector if /proc/net/wireless doesn't exist. import os + if os.path.exists("/proc/net/wireless"): try: collector = LinuxWifiCollector(sample_rate_hz=10.0) + collector.collect_once() # verify the interface is readable before committing to Linux WiFi self.source = "linux_wifi" + logger.info("Using LinuxWifiCollector") return collector - except RuntimeError: - logger.warning("Linux WiFi unavailable, falling back") + except RuntimeError as e: + logger.warning("Linux WiFi unavailable (%s), falling back", e) else: - logger.warning("Linux detected but /proc/net/wireless missing (likely Docker). Falling back.") + logger.warning( + "Linux detected but /proc/net/wireless missing (likely Docker). Falling back." + ) elif system == "Darwin": try: collector = MacosWifiCollector(sample_rate_hz=10.0) @@ -512,6 +524,7 @@ def stop(self) -> None: # Entry point # --------------------------------------------------------------------------- + def main(): logging.basicConfig( level=logging.INFO, diff --git a/v1/tests/unit/test_ws_server.py b/v1/tests/unit/test_ws_server.py new file mode 100644 index 00000000..953fec86 --- /dev/null +++ b/v1/tests/unit/test_ws_server.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import sys +import types +from unittest.mock import patch + +fake_numpy = types.ModuleType("numpy") +setattr(fake_numpy, "array", lambda *args, **kwargs: []) +setattr(fake_numpy, "zeros", lambda *args, **kwargs: []) +setattr(fake_numpy, "linspace", lambda *args, **kwargs: []) +setattr(fake_numpy, "meshgrid", lambda *args, **kwargs: ([], [])) +setattr(fake_numpy, "exp", lambda *args, **kwargs: 0) +setattr(fake_numpy, "sqrt", lambda *args, **kwargs: 0) +setattr(fake_numpy, "clip", lambda value, *_args, **_kwargs: value) +setattr(fake_numpy, "float64", float) +setattr(fake_numpy, "ndarray", list) +sys.modules.setdefault("numpy", fake_numpy) + +fake_numpy_typing = types.ModuleType("numpy.typing") +setattr(fake_numpy_typing, "NDArray", list) +sys.modules.setdefault("numpy.typing", fake_numpy_typing) + +fake_scipy = types.ModuleType("scipy") +setattr(fake_scipy, "fft", types.SimpleNamespace()) +setattr(fake_scipy, "stats", types.SimpleNamespace()) +sys.modules.setdefault("scipy", fake_scipy) + +from v1.src.sensing.ws_server import SensingWebSocketServer + + +def test_create_collector_falls_back_when_linux_wifi_probe_fails() -> None: + server = SensingWebSocketServer() + simulated_collector = object() + + with ( + patch("v1.src.sensing.ws_server.probe_esp32_udp", return_value=False), + patch("v1.src.sensing.ws_server.platform.system", return_value="Linux"), + patch("os.path.exists", return_value=True), + patch( + "v1.src.sensing.ws_server.LinuxWifiCollector.collect_once", + side_effect=RuntimeError("wifi unavailable"), + ), + patch( + "v1.src.sensing.ws_server.SimulatedCollector", + return_value=simulated_collector, + ), + ): + collector = server._create_collector() + + assert collector is simulated_collector + assert server.source == "simulated"