From 32b30e8bdf7f2311f0a3bebf96b48bccab00538c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bojan=20Poto=C4=8Dnik?= Date: Sat, 8 Jul 2023 01:01:15 +0200 Subject: [PATCH] BleakScanner: Add async iterator scanning capability Add `observe()` async iterator method to the `BleakScanner` which yields results of the ongoing scan. --- CHANGELOG.rst | 1 + bleak/__init__.py | 51 ++++++++++++++++++++++++++++++--------- docs/api/scanner.rst | 6 +++++ examples/scan_iterator.py | 35 +++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 examples/scan_iterator.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 61b23abc..016e91c6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,7 @@ and this project adheres to `Semantic Versioning AsyncIterator[Tuple[BLEDevice, AdvertisementData]]: + """ + Yields devices and their advertising data as they are discovered. + + .. note:: + Ensure that scanning is started before calling this method. + + Returns: + An async iterator that yields tuples (:class:`BLEDevice`, :class:`AdvertisementData`). + """ + # noinspection PyProtectedMember + user_cb = self._backend._callback + + devices = asyncio.Queue() + + def detection_callback(bd, ad): + devices.put_nowait((bd, ad)) + if user_cb: + user_cb(bd, ad) + + self._backend.register_detection_callback(detection_callback) + try: + while True: + yield await devices.get() + finally: + # noinspection PyProtectedMember + if self._backend._callback not in (user_cb, detection_callback): + # This check can be removed when deprecated `register_detection_callback()` method is removed + raise BleakError("Detection callback has been changed while scanning") + self._backend.register_detection_callback(user_cb) + @overload @classmethod async def discover( @@ -360,18 +392,13 @@ async def find_device_by_filter( the timeout. """ - found_device_queue: asyncio.Queue[BLEDevice] = asyncio.Queue() - - def apply_filter(d: BLEDevice, ad: AdvertisementData): - if filterfunc(d, ad): - found_device_queue.put_nowait(d) - - async with cls(detection_callback=apply_filter, **kwargs): - try: - async with async_timeout(timeout): - return await found_device_queue.get() - except asyncio.TimeoutError: - return None + try: + async with async_timeout(timeout): + async for bd, ad in cls.scan(**kwargs): + if filterfunc(bd, ad): + return bd + except asyncio.TimeoutError: + return None class BleakClient: diff --git a/docs/api/scanner.rst b/docs/api/scanner.rst index f9a15787..d79585aa 100644 --- a/docs/api/scanner.rst +++ b/docs/api/scanner.rst @@ -69,6 +69,12 @@ For event-driven programming, you can provide a ``detection_callback`` callback to the :class:`BleakScanner` constructor. This will be called back each time and advertisement is received. +Additionally, you can utilize the asynchronous iterator to iterate over +advertisements as they are received. The method below returns an async iterator +that yields the same tuples as otherwise provided to ``detection_callback``. + +.. automethod:: bleak.BleakScanner.observe + Otherwise, you can use one of the properties below after scanning has stopped. .. autoproperty:: bleak.BleakScanner.discovered_devices diff --git a/examples/scan_iterator.py b/examples/scan_iterator.py new file mode 100644 index 00000000..5ff8f93d --- /dev/null +++ b/examples/scan_iterator.py @@ -0,0 +1,35 @@ +""" +Scan/Discovery Async Iterator +-------------- + +Example showing how to scan for BLE devices using async iterator instead of callback function + +Created on 2023-07-07 by bojanpotocnik + +""" +import asyncio + +from bleak import BleakScanner + + +async def main(): + async with BleakScanner() as scanner: + n = 5 + print(f"Scanning for {n} devices...") + async for bd, ad in scanner.observe(): + print(f"{n}. {bd!r} with {ad!r}") + n -= 1 + if n == 0: + break + + n = 6 + print(f"\nScanning for a device with name longer than {n} characters...") + async for bd, ad in scanner.observe(): + found = len(bd.name or "") > n or len(ad.local_name or "") > n + print(f"Found{' it' if found else ''} {bd!r} with {ad!r}") + if found: + break + + +if __name__ == "__main__": + asyncio.run(main())