Files
l5y 09ea277a40 data/meshcore: fix ble and enable tcp (#669)
* data/meshcore: fix ble and enable tcp

* ingestor: address review comments

* ingestor: address review comments
2026-04-02 22:31:33 +02:00

164 lines
5.2 KiB
Python

# Copyright © 2025-26 l5yth & contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Provider-agnostic connection target helpers.
This module contains utilities shared by all ingestor providers for
parsing and auto-discovering connection targets. It is intentionally
free of any provider-specific imports so that Meshtastic, MeshCore,
and future providers can all rely on the same logic.
"""
from __future__ import annotations
import glob
import re
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
DEFAULT_TCP_PORT: int = 4403
"""Default TCP port used when no port is explicitly supplied."""
DEFAULT_SERIAL_PATTERNS: tuple[str, ...] = (
"/dev/ttyACM*",
"/dev/ttyUSB*",
"/dev/tty.usbmodem*",
"/dev/tty.usbserial*",
"/dev/cu.usbmodem*",
"/dev/cu.usbserial*",
)
"""Glob patterns for common serial device paths on Linux and macOS."""
# 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")$"
)
"""Compiled regex matching a BLE MAC address or UUID."""
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def parse_ble_target(value: str) -> str | None:
"""Return a normalised BLE address (MAC or UUID) when ``value`` matches the format.
Parameters:
value: User-provided target string.
Returns:
The normalised MAC address (upper-cased) or UUID, or ``None`` when
the value does not match a recognised BLE address format.
"""
if not value:
return None
value = value.strip()
if not value:
return None
if BLE_ADDRESS_RE.fullmatch(value):
return value.upper()
return None
def parse_tcp_target(value: str) -> tuple[str, int] | None:
"""Parse a TCP ``host:port`` target, accepting both IPs and hostnames.
Unlike the Meshtastic-specific helper in :mod:`interfaces`, hostnames are
accepted here because MeshCore companions may be reached over a local
network by name (e.g. ``meshcore-node.local:4403``).
BLE MAC addresses (five colons) and bare serial port paths (no colon) are
correctly rejected — they cannot produce a valid ``host:port`` pair.
Parameters:
value: User-provided target string.
Returns:
``(host, port)`` on success, or ``None`` when *value* does not look
like a TCP target.
"""
if not value:
return None
value = value.strip()
if not value:
return None
# Strip URL scheme prefix (e.g. ``tcp://host:4403`` or ``http://host:4403``).
if "://" in value:
value = value.split("://", 1)[1]
# Handle bracketed IPv6: ``[::1]:4403``.
if value.startswith("["):
bracket_end = value.find("]")
if bracket_end == -1:
return None
host = value[1:bracket_end]
rest = value[bracket_end + 1 :]
if rest.startswith(":"):
try:
port = int(rest[1:])
except ValueError:
return None
if not (1 <= port <= 65535):
return None
else:
port = DEFAULT_TCP_PORT
if not host:
return None
return host, port
# For non-bracketed addresses require exactly one colon so that BLE MACs
# (five colons) and bare serial paths (no colon) are rejected.
colon_count = value.count(":")
if colon_count != 1:
return None
host, _, port_str = value.partition(":")
if not host:
return None
try:
port = int(port_str)
except ValueError:
return None
if not (1 <= port <= 65535):
return None
return host, port
def default_serial_targets() -> list[str]:
"""Return candidate serial device paths for auto-discovery.
Globs for common USB serial device paths on Linux and macOS. Always
includes ``/dev/ttyACM0`` as a final fallback so callers have at least
one candidate even on systems without any attached hardware.
Returns:
Ordered list of candidate device paths, deduplicated.
"""
candidates: list[str] = []
seen: set[str] = set()
for pattern in DEFAULT_SERIAL_PATTERNS:
for path in sorted(glob.glob(pattern)):
if path not in seen:
candidates.append(path)
seen.add(path)
if "/dev/ttyACM0" not in seen:
candidates.append("/dev/ttyACM0")
return candidates