Skip to content

Commit

Permalink
BleakScanner: Add async iterator scanning capability
Browse files Browse the repository at this point in the history
Add `devices()` async iterator method to the `BleakScanner` which yields
results of the ongoing scan, together with its class-method counterpart
`scan()`.
  • Loading branch information
bojanpotocnik committed Jul 10, 2023
1 parent 2abe80d commit 4646aea
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 17 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ 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 ``devices()`` async iterator method and ``scan()`` class-method counterpart to ``BleakScanner``.

Fixed
-----
- Fix handling all access denied errors when enumerating characteristics on Windows. Fixes #1291.
- Added support for 32bit UUIDs. Fixes #1314
- Added support for 32bit UUIDs. Fixes #1314.

`0.20.2`_ (2023-04-19)
======================
Expand Down
73 changes: 57 additions & 16 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 @@ -148,12 +149,12 @@ def __init__(
**kwargs,
)

async def __aenter__(self):
await self._backend.start()
async def __aenter__(self) -> BleakScanner:
await self.start()
return self

async def __aexit__(self, exc_type, exc_val, exc_tb):
await self._backend.stop()
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
await self.stop()

def register_detection_callback(
self, callback: Optional[AdvertisementDataCallback]
Expand Down Expand Up @@ -204,6 +205,51 @@ def set_scanning_filter(self, **kwargs):
)
self._backend.set_scanning_filter(**kwargs)

async def devices(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`).
"""
if self._backend.detection_callback:
raise BleakError(
"Cannot use async iterator methods when detection_callback is used"
)

devices = asyncio.Queue()

async def detection_callback(bd, ad):
await devices.put((bd, ad))

self._backend.register_detection_callback(detection_callback)

while True:
try:
yield await devices.get()
except GeneratorExit:
self._backend.register_detection_callback(None)

@classmethod
async def scan(cls, **kwargs) -> AsyncIterator[Tuple[BLEDevice, AdvertisementData]]:
"""
Continuously scans for devices and yields them.
Args:
**kwargs:
Additional arguments to be passed to the :class:`BleakScanner`
constructor.
Returns:
An async iterator that yields tuples (:class:`BLEDevice`, :class:`AdvertisementData`).
"""
async with cls(**kwargs) as scanner:
async for result in scanner.devices():
yield result

@overload
@classmethod
async def discover(
Expand Down Expand Up @@ -360,18 +406,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
5 changes: 5 additions & 0 deletions bleak/backends/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ def __init__(

self.seen_devices = {}

@property
def detection_callback(self) -> Optional[AdvertisementDataCallback]:
"""Get the callback provided to :meth:`__init__` or registered with :meth:`register_detection_callback`"""
return self._callback

def register_detection_callback(
self, callback: Optional[AdvertisementDataCallback]
) -> None:
Expand Down
38 changes: 38 additions & 0 deletions examples/scan_iterator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""
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():
x = 5
print(f"Scanning for {x} devices...")
async for bd, ad in BleakScanner.scan():
print(f"{x}. {bd!r} with {ad!r}")
x -= 1
if x == 0:
break

# Ensure that asyncio loop has time to stop the scanner via the context manager exit
await asyncio.sleep(0.1)

x = 6
print(f"\nScanning for a device with name longer than {x} characters...")
async with BleakScanner() as scanner:
async for bd, ad in scanner.devices():
found = len(bd.name or "") > x or len(ad.local_name or "") > x
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 4646aea

Please sign in to comment.