mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-13 23:21:19 +10:00
* Start of MCP server and test suite * Add MCP server for interacting with meshtastic devices and testing framework / TUI * Update mcp-server/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix mcp-server review feedback from thread Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/91dc128a-ed50-4d07-8bb2-3dc6623a05f7 Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * Enhance StreamAPI and PhoneAPI for improved log record handling and concurrency control * Semgrep fixes * Trunk and semgrep fixes * optimize pio streaming tee file writes Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/04e26c6b-6a2b-45be-bbeb-79ae4d0be633 Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * chore: remove redundant log handle assignment Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/04e26c6b-6a2b-45be-bbeb-79ae4d0be633 Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * Consolidate type imports and remove placeholder test files * Add tests for config persistence and more exchange messages * Refactor position test to validate on-demand request/reply behavior * Remove position request/reply test and update README for telemetry behavior * Fix transmit history file to get removed on factory reset --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
85 lines
2.6 KiB
Python
85 lines
2.6 KiB
Python
"""Context manager for meshtastic.SerialInterface connections.
|
|
|
|
Every info/admin tool goes through `connect(port)` so we have a single place
|
|
that:
|
|
- auto-selects the port when one likely_meshtastic device is present,
|
|
- fails fast if a serial_session is already holding the port,
|
|
- guarantees `.close()` is called, even on exception.
|
|
|
|
The `SerialInterface` blocks on construction waiting for the node database;
|
|
that's fine for v1 since every tool is a short-lived request.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from contextlib import contextmanager
|
|
from typing import Iterator
|
|
|
|
from . import devices, registry
|
|
|
|
|
|
class ConnectionError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def resolve_port(port: str | None) -> str:
|
|
"""Pick a port: explicit > sole likely_meshtastic candidate > error."""
|
|
if port:
|
|
return port
|
|
candidates = [d for d in devices.list_devices() if d["likely_meshtastic"]]
|
|
if not candidates:
|
|
raise ConnectionError(
|
|
"No Meshtastic devices detected. Plug one in or pass `port` explicitly. "
|
|
"Run `list_devices` with include_unknown=True to see all serial ports."
|
|
)
|
|
if len(candidates) > 1:
|
|
ports = ", ".join(c["port"] for c in candidates)
|
|
raise ConnectionError(
|
|
f"Multiple Meshtastic devices detected ({ports}). "
|
|
"Specify `port` explicitly."
|
|
)
|
|
return candidates[0]["port"]
|
|
|
|
|
|
@contextmanager
|
|
def connect(port: str | None = None, timeout_s: float = 8.0) -> Iterator:
|
|
"""Open a `meshtastic.SerialInterface` and always close it.
|
|
|
|
Raises `ConnectionError` immediately if another serial session holds the
|
|
port (a `pio device monitor` in `serial_sessions/`, for instance).
|
|
"""
|
|
from meshtastic.serial_interface import (
|
|
SerialInterface, # type: ignore[import-untyped]
|
|
)
|
|
|
|
resolved = resolve_port(port)
|
|
|
|
active = registry.active_session_for_port(resolved)
|
|
if active is not None:
|
|
raise ConnectionError(
|
|
f"Port {resolved} is held by serial session {active.id}. "
|
|
"Call `serial_close` first."
|
|
)
|
|
|
|
lock = registry.port_lock(resolved)
|
|
if not lock.acquire(blocking=False):
|
|
raise ConnectionError(
|
|
f"Port {resolved} is busy — another device operation is in flight. "
|
|
"Retry shortly."
|
|
)
|
|
|
|
iface = None
|
|
try:
|
|
iface = SerialInterface(devPath=resolved, connectNow=True, noProto=False)
|
|
yield iface
|
|
finally:
|
|
if iface is not None:
|
|
try:
|
|
iface.close()
|
|
except Exception:
|
|
pass
|
|
try:
|
|
lock.release()
|
|
except RuntimeError:
|
|
pass
|