Support BLE UUID format for macOS Bluetooth devices (#575)

* Initial plan

* Add BLE UUID support for macOS devices

Co-authored-by: apo-mak <25563515+apo-mak@users.noreply.github.com>

* docs: Add UUID format example for macOS BLE connections

Co-authored-by: apo-mak <25563515+apo-mak@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: apo-mak <25563515+apo-mak@users.noreply.github.com>
This commit is contained in:
apo-mak
2025-12-20 21:21:59 +02:00
committed by GitHub
parent 4dc03f33ca
commit 42f4e80a26
3 changed files with 71 additions and 5 deletions
+3 -2
View File
@@ -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
+12 -3
View File
@@ -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)
+56
View File
@@ -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