Skip to content

Commit cdf893e

Browse files
authored
Merge pull request #893 from lexfrei/fix/ble-race-condition
fix(ble): resolve BLE connection hangs on macOS without --debug flag
2 parents b26d80f + 9b9df9e commit cdf893e

File tree

1 file changed

+20
-2
lines changed

1 file changed

+20
-2
lines changed

meshtastic/ble_interface.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
import atexit
55
import logging
66
import struct
7+
import sys
78
import time
89
import io
9-
from threading import Thread
10+
from threading import Thread, Event
1011
from typing import List, Optional
1112

1213
import google.protobuf
@@ -258,11 +259,13 @@ class BLEClient:
258259
"""Client for managing connection to a BLE device"""
259260

260261
def __init__(self, address=None, **kwargs) -> None:
262+
self._loop_ready = Event()
261263
self._eventLoop = asyncio.new_event_loop()
262264
self._eventThread = Thread(
263265
target=self._run_event_loop, name="BLEClient", daemon=True
264266
)
265267
self._eventThread.start()
268+
self._loop_ready.wait() # Wait for event loop to be running
266269

267270
if not address:
268271
logger.debug("No address provided - only discover method will work.")
@@ -306,13 +309,28 @@ def __exit__(self, _type, _value, _traceback):
306309
self.close()
307310

308311
def async_await(self, coro, timeout=None): # pylint: disable=C0116
309-
return self.async_run(coro).result(timeout)
312+
"""Wait for async operation to complete.
313+
314+
On macOS, CoreBluetooth requires occasional I/O operations for
315+
callbacks to be properly delivered. The debug logging provides this
316+
I/O when enabled, allowing the system to process pending callbacks.
317+
"""
318+
logger.debug(f"async_await: waiting for {coro}")
319+
future = self.async_run(coro)
320+
# On macOS without debug logging, callbacks may not be delivered
321+
# unless we trigger some I/O. This is a known quirk of CoreBluetooth.
322+
sys.stdout.flush()
323+
result = future.result(timeout)
324+
logger.debug("async_await: complete")
325+
return result
310326

311327
def async_run(self, coro): # pylint: disable=C0116
312328
return asyncio.run_coroutine_threadsafe(coro, self._eventLoop)
313329

314330
def _run_event_loop(self):
315331
try:
332+
# Signal ready from WITHIN the loop to guarantee it's actually running
333+
self._eventLoop.call_soon(self._loop_ready.set)
316334
self._eventLoop.run_forever()
317335
finally:
318336
self._eventLoop.close()

0 commit comments

Comments
 (0)