diff --git a/README.md b/README.md index 576970f..39d587d 100644 --- a/README.md +++ b/README.md @@ -202,8 +202,9 @@ Run the script with `INSTANCE_DOMAIN` and `API_TOKEN` to keep updating node records and parsing new incoming messages. Enable debug output with `DEBUG=1`, specify the connection target with `CONNECTION` (default `/dev/ttyACM0`) or set it to an IP address (for example `192.168.1.20:4403`) to use the Meshtastic TCP -interface. `CONNECTION` also accepts Bluetooth device addresses (e.g., -`ED:4D:9E:95:CF:60`) and the script attempts a BLE connection if available. To keep +interface. `CONNECTION` also accepts Bluetooth device addresses in MAC format (e.g., +`ED:4D:9E:95:CF:60`) or UUID format for macOS (e.g., `C0AEA92F-045E-9B82-C9A6-A1FD822B3A9E`) +and the script attempts a BLE connection if available. To keep ingestion limited, set `ALLOWED_CHANNELS` to a comma-separated whitelist (for example `ALLOWED_CHANNELS="Chat,Ops"`); packets on other channels are discarded. Use `HIDDEN_CHANNELS` to block specific channels from the web UI even when they diff --git a/data/mesh_ingestor/interfaces.py b/data/mesh_ingestor/interfaces.py index 9d2f717..128611f 100644 --- a/data/mesh_ingestor/interfaces.py +++ b/data/mesh_ingestor/interfaces.py @@ -628,7 +628,13 @@ _DEFAULT_SERIAL_PATTERNS = ( "/dev/cu.usbserial*", ) -_BLE_ADDRESS_RE = re.compile(r"^(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$") +# Support both MAC addresses (Linux/Windows) and UUIDs (macOS) +_BLE_ADDRESS_RE = re.compile( + r"^(?:" + r"(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}|" # MAC address format + r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" # UUID format + r")$" +) class _DummySerialInterface: @@ -642,13 +648,13 @@ class _DummySerialInterface: def _parse_ble_target(value: str) -> str | None: - """Return an uppercase BLE MAC address when ``value`` matches the format. + """Return a normalized BLE address (MAC or UUID) when ``value`` matches the format. Parameters: value: User-provided target string. Returns: - The normalised MAC address or ``None`` when validation fails. + The normalised MAC address or UUID, or ``None`` when validation fails. """ if not value: @@ -772,10 +778,13 @@ def _create_serial_interface(port: str) -> tuple[object, str]: return _DummySerialInterface(), "mock" ble_target = _parse_ble_target(port_value) if ble_target: + # Determine if it's a MAC address or UUID + address_type = "MAC" if ":" in ble_target else "UUID" config._debug_log( "Using BLE interface", context="interfaces.ble", address=ble_target, + address_type=address_type, ) return _load_ble_interface()(address=ble_target), ble_target network_target = _parse_network_target(port_value) diff --git a/tests/test_mesh.py b/tests/test_mesh.py index e7ac5bd..ad3b37d 100644 --- a/tests/test_mesh.py +++ b/tests/test_mesh.py @@ -2640,6 +2640,62 @@ def test_parse_ble_target_rejects_invalid_values(mesh_module): assert mesh._parse_ble_target("zz:zz:zz:zz:zz:zz") is None +def test_parse_ble_target_accepts_mac_addresses(mesh_module): + """Test that _parse_ble_target accepts valid MAC address format (Linux/Windows).""" + mesh = mesh_module + + # Valid MAC addresses should be accepted and normalized to uppercase + assert mesh._parse_ble_target("ED:4D:9E:95:CF:60") == "ED:4D:9E:95:CF:60" + assert mesh._parse_ble_target("ed:4d:9e:95:cf:60") == "ED:4D:9E:95:CF:60" + assert mesh._parse_ble_target("AA:BB:CC:DD:EE:FF") == "AA:BB:CC:DD:EE:FF" + assert mesh._parse_ble_target("00:11:22:33:44:55") == "00:11:22:33:44:55" + + # With whitespace + assert mesh._parse_ble_target(" ED:4D:9E:95:CF:60 ") == "ED:4D:9E:95:CF:60" + + # Invalid MAC addresses should be rejected + assert mesh._parse_ble_target("ED:4D:9E:95:CF") is None # Too short + assert mesh._parse_ble_target("ED:4D:9E:95:CF:60:AB") is None # Too long + assert mesh._parse_ble_target("GG:HH:II:JJ:KK:LL") is None # Invalid hex + + +def test_parse_ble_target_accepts_uuids(mesh_module): + """Test that _parse_ble_target accepts valid UUID format (macOS).""" + mesh = mesh_module + + # Valid UUIDs should be accepted and normalized to uppercase + assert ( + mesh._parse_ble_target("C0AEA92F-045E-9B82-C9A6-A1FD822B3A9E") + == "C0AEA92F-045E-9B82-C9A6-A1FD822B3A9E" + ) + assert ( + mesh._parse_ble_target("c0aea92f-045e-9b82-c9a6-a1fd822b3a9e") + == "C0AEA92F-045E-9B82-C9A6-A1FD822B3A9E" + ) + assert ( + mesh._parse_ble_target("12345678-1234-5678-9ABC-DEF012345678") + == "12345678-1234-5678-9ABC-DEF012345678" + ) + + # With whitespace + assert ( + mesh._parse_ble_target(" C0AEA92F-045E-9B82-C9A6-A1FD822B3A9E ") + == "C0AEA92F-045E-9B82-C9A6-A1FD822B3A9E" + ) + + # Invalid UUIDs should be rejected + assert mesh._parse_ble_target("C0AEA92F-045E-9B82-C9A6") is None # Too short + assert ( + mesh._parse_ble_target("C0AEA92F-045E-9B82-C9A6-A1FD822B3A9E-EXTRA") is None + ) # Too long + assert ( + mesh._parse_ble_target("GGGGGGGG-GGGG-GGGG-GGGG-GGGGGGGGGGGG") is None + ) # Invalid hex + assert ( + mesh._parse_ble_target("C0AEA92F:045E:9B82:C9A6:A1FD822B3A9E") is None + ) # Wrong separator + + def test_parse_network_target_additional_cases(mesh_module): mesh = mesh_module