Skip to content

Commit

Permalink
BleakScanner: Add async iterator scanning capability
Browse files Browse the repository at this point in the history
Add `observe()` async iterator method to the `BleakScanner` which yields
results of the ongoing scan.
  • Loading branch information
bojanpotocnik committed Jul 18, 2023
1 parent 6d603aa commit 32b30e8
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 12 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0
Changed
-------
- Improved error messages when failing to get services in WinRT backend.
- Added ``observe()`` async iterator method to ``BleakScanner``.

Fixed
-----
Expand Down
51 changes: 39 additions & 12 deletions bleak/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
Type,
Union,
overload,
AsyncIterator,
)
from warnings import warn

Expand Down Expand Up @@ -204,6 +205,37 @@ def set_scanning_filter(self, **kwargs):
)
self._backend.set_scanning_filter(**kwargs)

async def observe(self) -> 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(
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions docs/api/scanner.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions examples/scan_iterator.py
Original file line number Diff line number Diff line change
@@ -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 <info@bojanpotocnik.com>
"""
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())

0 comments on commit 32b30e8

Please sign in to comment.