Skip to content

Commit

Permalink
corebluetooth: work around macOS 12 scanner bug
Browse files Browse the repository at this point in the history
macOS has a bug (feature?) where advertising data is no longer received unless one
or more service UUIDs are given in scanForPeripheralsWithServices:options:.

This implements a new kwarg on `BleakScanner` to allow users to provide
such a list of UUIDs. This commit only implements it in the CoreBluetooth
backend with other backends to follow.

Issue #635.
  • Loading branch information
dlech committed Dec 6, 2021
1 parent 05dab6d commit 5e1a9b3
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 17 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0
`Unreleased`_
=============

Added
-----

* Added ``service_uuids`` kwarg to ``BleakScanner``. This can be used to work
around issue of scanning not working on macOS 12. Issue #635.

Changed
-------

Expand Down
14 changes: 8 additions & 6 deletions bleak/backends/corebluetooth/CentralManagerDelegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,17 @@ def __del__(self):
# User defined functions

@objc.python_method
async def start_scan(self, scan_options) -> None:
async def start_scan(self, service_uuids) -> None:
# remove old
self.devices = {}
service_uuids = None
if "service_uuids" in scan_options:
service_uuids_str = scan_options["service_uuids"]
service_uuids = NSArray.alloc().initWithArray_(
list(map(CBUUID.UUIDWithString_, service_uuids_str))

service_uuids = (
NSArray.alloc().initWithArray_(
list(map(CBUUID.UUIDWithString_, service_uuids))
)
if service_uuids
else None
)

self.central_manager.scanForPeripheralsWithServices_options_(
service_uuids, None
Expand Down
19 changes: 15 additions & 4 deletions bleak/backends/corebluetooth/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import pathlib
from typing import Any, Dict, List, Optional

import objc
from Foundation import NSArray, NSUUID
from CoreBluetooth import CBPeripheral

Expand All @@ -25,17 +26,27 @@ class BleakScannerCoreBluetooth(BaseBleakScanner):
with this, CoreBluetooth utilizes UUIDs for each peripheral. Bleak uses
this for the BLEDevice address on macOS.
Keyword Args:
timeout (double): The scanning timeout to be used, in case of missing
Args:
**timeout (double): The scanning timeout to be used, in case of missing
``stopScan_`` method.
**detection_callback (callable or coroutine):
Optional function that will be called each time a device is
discovered or advertising data has changed.
**service_uuids (List[str]):
Optional list of service UUIDs to filter on. Only advertisements
containing this advertising data will be received. Required on
macOS 12 and later.
"""

def __init__(self, **kwargs):
super(BleakScannerCoreBluetooth, self).__init__(**kwargs)
self._identifiers: Optional[Dict[NSUUID, Dict[str, Any]]] = None
self._manager = CentralManagerDelegate.alloc().init()
self._timeout: float = kwargs.get("timeout", 5.0)
if objc.macos_available(12, 0) and not self._service_uuids:
logging.error(
"macOS 12 requires non-empty service_uuids kwarg, otherwise no advertisement data will be received"
)

async def start(self):
self._identifiers = {}
Expand Down Expand Up @@ -89,7 +100,7 @@ def callback(p: CBPeripheral, a: Dict[str, Any], r: int) -> None:
self._callback(device, advertisement_data)

self._manager.callbacks[id(self)] = callback
await self._manager.start_scan({})
await self._manager.start_scan(self._service_uuids)

async def stop(self):
await self._manager.stop_scan()
Expand Down
18 changes: 17 additions & 1 deletion bleak/backends/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,28 @@ def __repr__(self) -> str:


class BaseBleakScanner(abc.ABC):
"""Interface for Bleak Bluetooth LE Scanners"""
"""
Interface for Bleak Bluetooth LE Scanners
Args:
**detection_callback (callable or coroutine):
Optional function that will be called each time a device is
discovered or advertising data has changed.
**service_uuids (List[str]):
Optional list of service UUIDs to filter on. Only advertisements
containing this advertising data will be received. Required on
macOS 12 and later.
"""

def __init__(self, *args, **kwargs):
super(BaseBleakScanner, self).__init__()
self._callback: Optional[AdvertisementDataCallback] = None
self.register_detection_callback(kwargs.get("detection_callback"))
self._service_uuids: Optional[List[str]] = (
[u.lower() for u in kwargs["service_uuids"]]
if "service_uuids" in kwargs
else None
)

async def __aenter__(self):
await self.start()
Expand Down
19 changes: 13 additions & 6 deletions examples/detection_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,22 @@
"""

import asyncio
import logging
import sys

from bleak import BleakScanner
from bleak.backends.device import BLEDevice
from bleak.backends.scanner import AdvertisementData
import logging

logging.basicConfig()
logger = logging.getLogger(__name__)


def simple_callback(device: BLEDevice, advertisement_data: AdvertisementData):
print(device.address, "RSSI:", device.rssi, advertisement_data)
logger.info(f"{device.address} RSSI: {device.rssi}, {advertisement_data}")


async def main():
scanner = BleakScanner()
async def main(service_uuids):
scanner = BleakScanner(service_uuids=service_uuids)
scanner.register_detection_callback(simple_callback)

while True:
Expand All @@ -32,4 +34,9 @@ async def main():


if __name__ == "__main__":
asyncio.run(main())
logging.basicConfig(
level=logging.INFO,
format="%(asctime)-15s %(name)-8s %(levelname)s: %(message)s",
)
service_uuids = sys.argv[1:]
asyncio.run(main(service_uuids))

0 comments on commit 5e1a9b3

Please sign in to comment.