mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-06 08:43:36 +02:00
Move to multi-connection modality
This commit is contained in:
+7
-4
@@ -62,7 +62,7 @@ await AppSettingsRepository.add_favorite("contact", public_key)
|
||||
|
||||
### Radio Connection
|
||||
|
||||
`RadioManager` in `radio.py` handles serial connection:
|
||||
`RadioManager` in `radio.py` handles radio connection over Serial, TCP, or BLE:
|
||||
|
||||
```python
|
||||
from app.radio import radio_manager
|
||||
@@ -70,9 +70,12 @@ from app.radio import radio_manager
|
||||
# Access meshcore instance
|
||||
if radio_manager.meshcore:
|
||||
await radio_manager.meshcore.commands.send_msg(dst, msg)
|
||||
|
||||
# Check connection info (e.g. "Serial: /dev/ttyUSB0", "TCP: 192.168.1.1:4000", "BLE: AA:BB:CC:DD:EE:FF")
|
||||
print(radio_manager.connection_info)
|
||||
```
|
||||
|
||||
Auto-detection scans common serial ports when `MESHCORE_SERIAL_PORT` is not set.
|
||||
Transport is configured via env vars (see root AGENTS.md). When no transport env vars are set, serial auto-detection is used.
|
||||
|
||||
### Event-Driven Architecture
|
||||
|
||||
@@ -116,7 +119,7 @@ from app.websocket import broadcast_error, broadcast_health
|
||||
broadcast_error("Operation failed", "Additional details")
|
||||
|
||||
# Notify clients of connection status change
|
||||
broadcast_health(radio_connected=True, serial_port="/dev/ttyUSB0")
|
||||
broadcast_health(radio_connected=True, connection_info="Serial: /dev/ttyUSB0")
|
||||
```
|
||||
|
||||
### Connection Monitoring
|
||||
@@ -464,7 +467,7 @@ result = await sync_recent_contacts_to_radio(force=True)
|
||||
All endpoints are prefixed with `/api`.
|
||||
|
||||
### Health
|
||||
- `GET /api/health` - Connection status, serial port
|
||||
- `GET /api/health` - Connection status, connection info
|
||||
|
||||
### Radio
|
||||
- `GET /api/radio/config` - Read config (public key, name, radio params)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import model_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
@@ -9,9 +10,39 @@ class Settings(BaseSettings):
|
||||
|
||||
serial_port: str = "" # Empty string triggers auto-detection
|
||||
serial_baudrate: int = 115200
|
||||
tcp_host: str = ""
|
||||
tcp_port: int = 4000
|
||||
ble_address: str = ""
|
||||
ble_pin: str = ""
|
||||
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
|
||||
database_path: str = "data/meshcore.db"
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_transport_exclusivity(self) -> "Settings":
|
||||
transports_set = sum(
|
||||
[
|
||||
bool(self.serial_port),
|
||||
bool(self.tcp_host),
|
||||
bool(self.ble_address),
|
||||
]
|
||||
)
|
||||
if transports_set > 1:
|
||||
raise ValueError(
|
||||
"Only one transport may be configured at a time. "
|
||||
"Set exactly one of MESHCORE_SERIAL_PORT, MESHCORE_TCP_HOST, or MESHCORE_BLE_ADDRESS."
|
||||
)
|
||||
if self.ble_address and not self.ble_pin:
|
||||
raise ValueError("MESHCORE_BLE_PIN is required when MESHCORE_BLE_ADDRESS is set.")
|
||||
return self
|
||||
|
||||
@property
|
||||
def connection_type(self) -> Literal["serial", "tcp", "ble"]:
|
||||
if self.tcp_host:
|
||||
return "tcp"
|
||||
if self.ble_address:
|
||||
return "ble"
|
||||
return "serial"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
|
||||
+51
-9
@@ -102,7 +102,7 @@ class RadioManager:
|
||||
|
||||
def __init__(self):
|
||||
self._meshcore: MeshCore | None = None
|
||||
self._port: str | None = None
|
||||
self._connection_info: str | None = None
|
||||
self._reconnect_task: asyncio.Task | None = None
|
||||
self._last_connected: bool = False
|
||||
self._reconnect_lock: asyncio.Lock | None = None
|
||||
@@ -169,8 +169,8 @@ class RadioManager:
|
||||
return self._meshcore
|
||||
|
||||
@property
|
||||
def port(self) -> str | None:
|
||||
return self._port
|
||||
def connection_info(self) -> str | None:
|
||||
return self._connection_info
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
@@ -181,10 +181,20 @@ class RadioManager:
|
||||
return self._reconnect_lock is not None and self._reconnect_lock.locked()
|
||||
|
||||
async def connect(self) -> None:
|
||||
"""Connect to the radio over serial."""
|
||||
"""Connect to the radio using the configured transport."""
|
||||
if self._meshcore is not None:
|
||||
await self.disconnect()
|
||||
|
||||
connection_type = settings.connection_type
|
||||
if connection_type == "tcp":
|
||||
await self._connect_tcp()
|
||||
elif connection_type == "ble":
|
||||
await self._connect_ble()
|
||||
else:
|
||||
await self._connect_serial()
|
||||
|
||||
async def _connect_serial(self) -> None:
|
||||
"""Connect to the radio over serial."""
|
||||
port = settings.serial_port
|
||||
|
||||
# Auto-detect if no port specified
|
||||
@@ -205,10 +215,42 @@ class RadioManager:
|
||||
auto_reconnect=True,
|
||||
max_reconnect_attempts=10,
|
||||
)
|
||||
self._port = port
|
||||
self._connection_info = f"Serial: {port}"
|
||||
self._last_connected = True
|
||||
logger.debug("Serial connection established")
|
||||
|
||||
async def _connect_tcp(self) -> None:
|
||||
"""Connect to the radio over TCP."""
|
||||
host = settings.tcp_host
|
||||
port = settings.tcp_port
|
||||
|
||||
logger.debug("Connecting to radio at %s:%d (TCP)", host, port)
|
||||
self._meshcore = await MeshCore.create_tcp(
|
||||
host=host,
|
||||
port=port,
|
||||
auto_reconnect=True,
|
||||
max_reconnect_attempts=10,
|
||||
)
|
||||
self._connection_info = f"TCP: {host}:{port}"
|
||||
self._last_connected = True
|
||||
logger.debug("TCP connection established")
|
||||
|
||||
async def _connect_ble(self) -> None:
|
||||
"""Connect to the radio over BLE."""
|
||||
address = settings.ble_address
|
||||
pin = settings.ble_pin
|
||||
|
||||
logger.debug("Connecting to radio at %s (BLE)", address)
|
||||
self._meshcore = await MeshCore.create_ble(
|
||||
address=address,
|
||||
pin=pin,
|
||||
auto_reconnect=True,
|
||||
max_reconnect_attempts=15,
|
||||
)
|
||||
self._connection_info = f"BLE: {address}"
|
||||
self._last_connected = True
|
||||
logger.debug("BLE connection established")
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
"""Disconnect from the radio."""
|
||||
if self._meshcore is not None:
|
||||
@@ -250,8 +292,8 @@ class RadioManager:
|
||||
await self.connect()
|
||||
|
||||
if self.is_connected:
|
||||
logger.info("Radio reconnected successfully at %s", self._port)
|
||||
broadcast_health(True, self._port)
|
||||
logger.info("Radio reconnected successfully at %s", self._connection_info)
|
||||
broadcast_health(True, self._connection_info)
|
||||
return True
|
||||
else:
|
||||
logger.warning("Reconnection failed: not connected after connect()")
|
||||
@@ -280,7 +322,7 @@ class RadioManager:
|
||||
if self._last_connected and not current_connected:
|
||||
# Connection lost
|
||||
logger.warning("Radio connection lost, broadcasting status change")
|
||||
broadcast_health(False, self._port)
|
||||
broadcast_health(False, self._connection_info)
|
||||
self._last_connected = False
|
||||
|
||||
# Attempt reconnection
|
||||
@@ -291,7 +333,7 @@ class RadioManager:
|
||||
elif not self._last_connected and current_connected:
|
||||
# Connection restored (might have reconnected automatically)
|
||||
logger.info("Radio connection restored")
|
||||
broadcast_health(True, self._port)
|
||||
broadcast_health(True, self._connection_info)
|
||||
self._last_connected = True
|
||||
|
||||
except asyncio.CancelledError:
|
||||
|
||||
@@ -13,12 +13,12 @@ router = APIRouter(tags=["health"])
|
||||
class HealthResponse(BaseModel):
|
||||
status: str
|
||||
radio_connected: bool
|
||||
serial_port: str | None
|
||||
connection_info: str | None
|
||||
database_size_mb: float
|
||||
oldest_undecrypted_timestamp: int | None
|
||||
|
||||
|
||||
async def build_health_data(radio_connected: bool, serial_port: str | None) -> dict:
|
||||
async def build_health_data(radio_connected: bool, connection_info: str | None) -> dict:
|
||||
"""Build the health status payload used by REST endpoint and WebSocket broadcasts."""
|
||||
db_size_mb = 0.0
|
||||
try:
|
||||
@@ -36,7 +36,7 @@ async def build_health_data(radio_connected: bool, serial_port: str | None) -> d
|
||||
return {
|
||||
"status": "ok" if radio_connected else "degraded",
|
||||
"radio_connected": radio_connected,
|
||||
"serial_port": serial_port,
|
||||
"connection_info": connection_info,
|
||||
"database_size_mb": db_size_mb,
|
||||
"oldest_undecrypted_timestamp": oldest_ts,
|
||||
}
|
||||
@@ -45,5 +45,5 @@ async def build_health_data(radio_connected: bool, serial_port: str | None) -> d
|
||||
@router.get("/health", response_model=HealthResponse)
|
||||
async def healthcheck() -> HealthResponse:
|
||||
"""Check if the API is running and if the radio is connected."""
|
||||
data = await build_health_data(radio_manager.is_connected, radio_manager.port)
|
||||
data = await build_health_data(radio_manager.is_connected, radio_manager.connection_info)
|
||||
return HealthResponse(**data)
|
||||
|
||||
+3
-1
@@ -23,7 +23,9 @@ async def websocket_endpoint(websocket: WebSocket) -> None:
|
||||
|
||||
# Send initial health status
|
||||
try:
|
||||
health_data = await build_health_data(radio_manager.is_connected, radio_manager.port)
|
||||
health_data = await build_health_data(
|
||||
radio_manager.is_connected, radio_manager.connection_info
|
||||
)
|
||||
await ws_manager.send_personal(websocket, "health", health_data)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
+2
-2
@@ -123,13 +123,13 @@ def broadcast_success(message: str, details: str | None = None) -> None:
|
||||
asyncio.create_task(ws_manager.broadcast("success", data))
|
||||
|
||||
|
||||
def broadcast_health(radio_connected: bool, serial_port: str | None = None) -> None:
|
||||
def broadcast_health(radio_connected: bool, connection_info: str | None = None) -> None:
|
||||
"""Broadcast health status change to all connected clients."""
|
||||
|
||||
async def _broadcast():
|
||||
from app.routers.health import build_health_data
|
||||
|
||||
data = await build_health_data(radio_connected, serial_port)
|
||||
data = await build_health_data(radio_connected, connection_info)
|
||||
await ws_manager.broadcast("health", data)
|
||||
|
||||
asyncio.create_task(_broadcast())
|
||||
|
||||
Reference in New Issue
Block a user