route startup and fanout through radio runtime

This commit is contained in:
Jack Kingsman
2026-03-09 23:11:57 -07:00
parent 81bdfe09fa
commit 9388e1f506
7 changed files with 40 additions and 15 deletions

View File

@@ -77,7 +77,7 @@ app/
- `RadioManager.start_connection_monitor()` checks health every 5s.
- `RadioManager.post_connect_setup()` delegates to `services/radio_lifecycle.py`.
- Routers and shared dependencies should reach radio state through `services/radio_runtime.py`, not by importing `app.radio.radio_manager` directly.
- Routers, startup/lifespan code, and fanout helpers should reach radio state through `services/radio_runtime.py`, not by importing `app.radio.radio_manager` directly.
- Shared reconnect/setup helpers in `services/radio_lifecycle.py` are used by startup, the monitor, and manual reconnect/reboot flows before broadcasting healthy state.
- Setup still includes handler registration, key export, time sync, contact/channel sync, polling/advert tasks.

View File

@@ -244,7 +244,7 @@ def _build_radio_info() -> str:
Matches the reference format: ``"freq,bw,sf,cr"`` (comma-separated raw
values). Falls back to ``"0,0,0,0"`` when unavailable.
"""
from app.radio import radio_manager
from app.services.radio_runtime import radio_runtime as radio_manager
try:
if radio_manager.meshcore and radio_manager.meshcore.self_info:
@@ -329,7 +329,7 @@ class CommunityMqttPublisher(BaseMqttPublisher):
def _build_client_kwargs(self, settings: object) -> dict[str, Any]:
s: CommunityMqttSettings = settings # type: ignore[assignment]
from app.keystore import get_private_key, get_public_key
from app.radio import radio_manager
from app.services.radio_runtime import radio_runtime as radio_manager
private_key = get_private_key()
public_key = get_public_key()
@@ -401,7 +401,8 @@ class CommunityMqttPublisher(BaseMqttPublisher):
if self._cached_device_info is not None:
return self._cached_device_info
from app.radio import RadioDisconnectedError, RadioOperationBusyError, radio_manager
from app.radio import RadioDisconnectedError, RadioOperationBusyError
from app.services.radio_runtime import radio_runtime as radio_manager
fallback = {"model": "unknown", "firmware_version": "unknown"}
try:
@@ -448,7 +449,8 @@ class CommunityMqttPublisher(BaseMqttPublisher):
) < _STATS_MIN_CACHE_SECS and self._cached_stats is not None:
return self._cached_stats
from app.radio import RadioDisconnectedError, RadioOperationBusyError, radio_manager
from app.radio import RadioDisconnectedError, RadioOperationBusyError
from app.services.radio_runtime import radio_runtime as radio_manager
try:
async with radio_manager.radio_operation("community_stats_fetch", blocking=False) as mc:
@@ -489,7 +491,7 @@ class CommunityMqttPublisher(BaseMqttPublisher):
) -> None:
"""Build and publish the enriched retained status message."""
from app.keystore import get_public_key
from app.radio import radio_manager
from app.services.radio_runtime import radio_runtime as radio_manager
public_key = get_public_key()
if public_key is None:

View File

@@ -25,7 +25,7 @@ _BACKOFF_MIN = 5
def _broadcast_health() -> None:
"""Push updated health (including MQTT status) to all WS clients."""
from app.radio import radio_manager
from app.services.radio_runtime import radio_runtime as radio_manager
from app.websocket import broadcast_health
broadcast_health(radio_manager.is_connected, radio_manager.connection_info)

View File

@@ -109,7 +109,7 @@ async def _publish_community_packet(
"""Format and publish a raw packet to the community broker."""
try:
from app.keystore import get_public_key
from app.radio import radio_manager
from app.services.radio_runtime import radio_runtime as radio_manager
public_key = get_public_key()
if public_key is None:

View File

@@ -10,7 +10,7 @@ from fastapi.responses import JSONResponse
from app.config import setup_logging
from app.database import db
from app.frontend_static import register_frontend_missing_fallback, register_frontend_static_routes
from app.radio import RadioDisconnectedError, radio_manager
from app.radio import RadioDisconnectedError
from app.radio_sync import (
stop_message_polling,
stop_periodic_advert,
@@ -30,6 +30,7 @@ from app.routers import (
statistics,
ws,
)
from app.services.radio_runtime import radio_runtime as radio_manager
setup_logging()
logger = logging.getLogger(__name__)
@@ -37,13 +38,8 @@ logger = logging.getLogger(__name__)
async def _startup_radio_connect_and_setup() -> None:
"""Connect/setup the radio in the background so HTTP serving can start immediately."""
from app.services.radio_lifecycle import reconnect_and_prepare_radio
try:
connected = await reconnect_and_prepare_radio(
radio_manager,
broadcast_on_success=True,
)
connected = await radio_manager.reconnect_and_prepare(broadcast_on_success=True)
if connected:
logger.info("Connected to radio")
else:

View File

@@ -83,6 +83,15 @@ class RadioRuntime:
async with self.manager.radio_operation(name, **kwargs) as mc:
yield mc
async def start_connection_monitor(self) -> None:
await self.manager.start_connection_monitor()
async def stop_connection_monitor(self) -> None:
await self.manager.stop_connection_monitor()
async def disconnect(self) -> None:
await self.manager.disconnect()
async def prepare_connected(self, *, broadcast_on_success: bool = True) -> None:
from app.services.radio_lifecycle import prepare_connected_radio

View File

@@ -1,4 +1,5 @@
from contextlib import asynccontextmanager
from unittest.mock import AsyncMock
import pytest
from fastapi import HTTPException
@@ -73,3 +74,20 @@ async def test_radio_operation_delegates_to_current_manager():
assert mc == "meshcore"
assert manager.calls == [("sync_contacts", {"pause_polling": True})]
@pytest.mark.asyncio
async def test_lifecycle_passthrough_methods_delegate_to_current_manager():
manager = _Manager(meshcore="meshcore", is_connected=True)
manager.start_connection_monitor = AsyncMock()
manager.stop_connection_monitor = AsyncMock()
manager.disconnect = AsyncMock()
runtime = RadioRuntime(manager)
await runtime.start_connection_monitor()
await runtime.stop_connection_monitor()
await runtime.disconnect()
manager.start_connection_monitor.assert_awaited_once()
manager.stop_connection_monitor.assert_awaited_once()
manager.disconnect.assert_awaited_once()