mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-07-06 01:42:11 +02:00
09ea277a40
* data/meshcore: fix ble and enable tcp * ingestor: address review comments * ingestor: address review comments
164 lines
5.2 KiB
Python
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
|