|
4 | 4 | import atexit |
5 | 5 | import logging |
6 | 6 | import struct |
| 7 | +import sys |
7 | 8 | import time |
8 | 9 | import io |
9 | | -from threading import Thread |
| 10 | +from threading import Thread, Event |
10 | 11 | from typing import List, Optional |
11 | 12 |
|
12 | 13 | import google.protobuf |
@@ -258,11 +259,13 @@ class BLEClient: |
258 | 259 | """Client for managing connection to a BLE device""" |
259 | 260 |
|
260 | 261 | def __init__(self, address=None, **kwargs) -> None: |
| 262 | + self._loop_ready = Event() |
261 | 263 | self._eventLoop = asyncio.new_event_loop() |
262 | 264 | self._eventThread = Thread( |
263 | 265 | target=self._run_event_loop, name="BLEClient", daemon=True |
264 | 266 | ) |
265 | 267 | self._eventThread.start() |
| 268 | + self._loop_ready.wait() # Wait for event loop to be running |
266 | 269 |
|
267 | 270 | if not address: |
268 | 271 | logger.debug("No address provided - only discover method will work.") |
@@ -306,13 +309,28 @@ def __exit__(self, _type, _value, _traceback): |
306 | 309 | self.close() |
307 | 310 |
|
308 | 311 | 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 |
310 | 326 |
|
311 | 327 | def async_run(self, coro): # pylint: disable=C0116 |
312 | 328 | return asyncio.run_coroutine_threadsafe(coro, self._eventLoop) |
313 | 329 |
|
314 | 330 | def _run_event_loop(self): |
315 | 331 | try: |
| 332 | + # Signal ready from WITHIN the loop to guarantee it's actually running |
| 333 | + self._eventLoop.call_soon(self._loop_ready.set) |
316 | 334 | self._eventLoop.run_forever() |
317 | 335 | finally: |
318 | 336 | self._eventLoop.close() |
|
0 commit comments