From 9b9df9e585eb11663c501642e2ec9a331b2c807d Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Sun, 25 Jan 2026 03:06:58 +0300 Subject: [PATCH] fix(ble): resolve BLE hangs on macOS without --debug flag This fixes two issues that caused BLE connections to hang on macOS when not using the --debug flag: 1. Race condition in BLEClient event loop initialization - The event loop thread was started but asyncio operations were submitted before the loop was actually running - Added threading.Event synchronization to ensure the event loop is running before any operations are submitted - The ready signal is sent from within the loop via call_soon() to guarantee the loop is truly active 2. CoreBluetooth callback delivery on macOS - On macOS, CoreBluetooth requires occasional I/O operations for callbacks to be properly delivered to the main thread - Without --debug, no I/O was happening, causing callbacks to never be processed and operations to hang indefinitely - Added sys.stdout.flush() call before waiting for async results to trigger the necessary I/O The --debug flag masked these issues because: - Debug logging introduces timing delays that let the event loop start - Logger I/O triggers the necessary callback delivery mechanism Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- meshtastic/ble_interface.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/meshtastic/ble_interface.py b/meshtastic/ble_interface.py index 19d9a275..0237672f 100644 --- a/meshtastic/ble_interface.py +++ b/meshtastic/ble_interface.py @@ -4,9 +4,10 @@ import atexit import logging import struct +import sys import time import io -from threading import Thread +from threading import Thread, Event from typing import List, Optional import google.protobuf @@ -258,11 +259,13 @@ class BLEClient: """Client for managing connection to a BLE device""" def __init__(self, address=None, **kwargs) -> None: + self._loop_ready = Event() self._eventLoop = asyncio.new_event_loop() self._eventThread = Thread( target=self._run_event_loop, name="BLEClient", daemon=True ) self._eventThread.start() + self._loop_ready.wait() # Wait for event loop to be running if not address: logger.debug("No address provided - only discover method will work.") @@ -306,13 +309,28 @@ def __exit__(self, _type, _value, _traceback): self.close() def async_await(self, coro, timeout=None): # pylint: disable=C0116 - return self.async_run(coro).result(timeout) + """Wait for async operation to complete. + + On macOS, CoreBluetooth requires occasional I/O operations for + callbacks to be properly delivered. The debug logging provides this + I/O when enabled, allowing the system to process pending callbacks. + """ + logger.debug(f"async_await: waiting for {coro}") + future = self.async_run(coro) + # On macOS without debug logging, callbacks may not be delivered + # unless we trigger some I/O. This is a known quirk of CoreBluetooth. + sys.stdout.flush() + result = future.result(timeout) + logger.debug("async_await: complete") + return result def async_run(self, coro): # pylint: disable=C0116 return asyncio.run_coroutine_threadsafe(coro, self._eventLoop) def _run_event_loop(self): try: + # Signal ready from WITHIN the loop to guarantee it's actually running + self._eventLoop.call_soon(self._loop_ready.set) self._eventLoop.run_forever() finally: self._eventLoop.close()