From 2757f25eb97e43f6fac64583672916e0e9a2f2b3 Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Tue, 24 Feb 2026 20:26:18 -0800 Subject: [PATCH] Use radio lock after setup --- app/radio.py | 62 ++++++++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/app/radio.py b/app/radio.py index cf00b35..bb5fbe3 100644 --- a/app/radio.py +++ b/app/radio.py @@ -245,40 +245,46 @@ class RadioManager: self._setup_complete = False mc = self._meshcore try: + # Register event handlers (no radio I/O, just callback setup) register_event_handlers(mc) - await export_and_store_private_key(mc) - # Sync radio clock with system time - await sync_radio_time(mc) + # Hold the operation lock for all radio I/O during setup. + # This prevents user-initiated operations (send message, etc.) + # from interleaving commands on the serial link. + await self._acquire_operation_lock("post_connect_setup", blocking=True) + try: + await export_and_store_private_key(mc) - # Sync contacts/channels from radio to DB and clear radio - logger.info("Syncing and offloading radio data...") - result = await sync_and_offload_all(mc) - logger.info("Sync complete: %s", result) + # Sync radio clock with system time + await sync_radio_time(mc) - # Start periodic sync (idempotent) + # Sync contacts/channels from radio to DB and clear radio + logger.info("Syncing and offloading radio data...") + result = await sync_and_offload_all(mc) + logger.info("Sync complete: %s", result) + + # Send advertisement to announce our presence (if enabled and not throttled) + if await send_advertisement(mc): + logger.info("Advertisement sent") + else: + logger.debug("Advertisement skipped (disabled or throttled)") + + # Drain any messages that were queued before we connected. + # This must happen BEFORE starting auto-fetch, otherwise both + # compete on get_msg() with interleaved radio I/O. + drained = await drain_pending_messages(mc) + if drained > 0: + logger.info("Drained %d pending message(s)", drained) + + await mc.start_auto_message_fetching() + logger.info("Auto message fetching started") + finally: + self._release_operation_lock("post_connect_setup") + + # Start background tasks AFTER releasing the operation lock. + # These tasks acquire their own locks when they need radio access. start_periodic_sync() - - # Send advertisement to announce our presence (if enabled and not throttled) - if await send_advertisement(mc): - logger.info("Advertisement sent") - else: - logger.debug("Advertisement skipped (disabled or throttled)") - - # Start periodic advertisement (idempotent) start_periodic_advert() - - # Drain any messages that were queued before we connected. - # This must happen BEFORE starting auto-fetch, otherwise both - # compete on get_msg() with interleaved radio I/O. - drained = await drain_pending_messages(mc) - if drained > 0: - logger.info("Drained %d pending message(s)", drained) - - await mc.start_auto_message_fetching() - logger.info("Auto message fetching started") - - # Start periodic message polling as fallback (idempotent) start_message_polling() self._setup_complete = True