Fix event handler rereg, monitor crash, and polling pause ugliness

This commit is contained in:
Jack Kingsman
2026-01-13 01:07:43 -08:00
parent c679f86ebd
commit 40b672d569
4 changed files with 189 additions and 23 deletions
+29
View File
@@ -113,6 +113,8 @@ broadcast_health(radio_connected=True, serial_port="/dev/ttyUSB0")
- Checks connection every 5 seconds
- Broadcasts `health` event on status change
- Attempts automatic reconnection when connection lost
- **Re-registers event handlers after successful auto-reconnect** (critical for message delivery)
- Resilient to transient errors (logs and continues rather than crashing)
- Supports manual reconnection via `POST /api/radio/reconnect`
```python
@@ -126,6 +128,33 @@ await radio_manager.start_connection_monitor()
await radio_manager.stop_connection_monitor()
```
### Message Polling
Periodic message polling serves as a fallback for platforms where push events are unreliable.
Use `pause_polling()` to temporarily suspend polling during operations that need exclusive
radio access (e.g., repeater CLI commands):
```python
from app.radio_sync import pause_polling, is_polling_paused
# Pause polling during sensitive operations (supports nesting)
async with pause_polling():
# Polling is paused here
await do_repeater_operation()
async with pause_polling():
# Still paused (nested)
await do_another_operation()
# Still paused (outer context active)
# Polling resumes when all contexts exit
# Check current state
if is_polling_paused():
print("Polling is currently paused")
```
## Database Schema
```sql
+30 -16
View File
@@ -210,26 +210,40 @@ class RadioManager:
from app.websocket import broadcast_health
while True:
await asyncio.sleep(5) # Check every 5 seconds
try:
await asyncio.sleep(5) # Check every 5 seconds
current_connected = self.is_connected
current_connected = self.is_connected
# Detect status change
if self._last_connected and not current_connected:
# Connection lost
logger.warning("Radio connection lost, broadcasting status change")
broadcast_health(False, self._port)
self._last_connected = False
# Detect status change
if self._last_connected and not current_connected:
# Connection lost
logger.warning("Radio connection lost, broadcasting status change")
broadcast_health(False, self._port)
self._last_connected = False
# Attempt reconnection
await asyncio.sleep(3) # Wait a bit before trying
await self.reconnect()
# Attempt reconnection
await asyncio.sleep(3) # Wait a bit before trying
if await self.reconnect():
# Re-register event handlers after successful reconnect
from app.event_handlers import register_event_handlers
if self._meshcore:
register_event_handlers(self._meshcore)
await self._meshcore.start_auto_message_fetching()
logger.info("Event handlers re-registered after auto-reconnect")
elif not self._last_connected and current_connected:
# Connection restored (might have reconnected automatically)
logger.info("Radio connection restored")
broadcast_health(True, self._port)
self._last_connected = True
elif not self._last_connected and current_connected:
# Connection restored (might have reconnected automatically)
logger.info("Radio connection restored")
broadcast_health(True, self._port)
self._last_connected = True
except asyncio.CancelledError:
# Task is being cancelled, exit cleanly
break
except Exception as e:
# Log error but continue monitoring - don't let the monitor die
logger.exception("Error in connection monitor, continuing: %s", e)
self._reconnect_task = asyncio.create_task(monitor_loop())
logger.info("Radio connection monitor started")
+15 -7
View File
@@ -29,19 +29,27 @@ _message_poll_task: asyncio.Task | None = None
# Message poll interval in seconds
MESSAGE_POLL_INTERVAL = 5
# Flag to pause polling during repeater operations
_polling_paused: bool = False
# Counter to pause polling during repeater operations (supports nested pauses)
_polling_pause_count: int = 0
def is_polling_paused() -> bool:
"""Check if polling is currently paused."""
return _polling_pause_count > 0
@asynccontextmanager
async def pause_polling():
"""Context manager to pause message polling during repeater operations."""
global _polling_paused
_polling_paused = True
"""Context manager to pause message polling during repeater operations.
Supports nested pauses - polling only resumes when all pause contexts have exited.
"""
global _polling_pause_count
_polling_pause_count += 1
try:
yield
finally:
_polling_paused = False
_polling_pause_count -= 1
# Background task handle
_sync_task: asyncio.Task | None = None
@@ -291,7 +299,7 @@ async def _message_poll_loop():
try:
await asyncio.sleep(MESSAGE_POLL_INTERVAL)
if radio_manager.is_connected and not _polling_paused:
if radio_manager.is_connected and not is_polling_paused():
await poll_for_messages()
except asyncio.CancelledError: