Skip to content

Commit

Permalink
Merge pull request hbldh#692 from hbldh/macos12-scanning
Browse files Browse the repository at this point in the history
corebluetooth: work around macOS 12 scanner bug
  • Loading branch information
dlech authored Dec 7, 2021
2 parents 05dab6d + afe2b85 commit b644ae2
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 90 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,19 @@ 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. Fixes #230. Works around #635.

Changed
-------

* Changed WinRT backend to use GATT session status instead of actual device
connection status.
* Changed handling of scan response data on WinRT backend. Advertising data
and scan response data is now combined in callbacks like other platforms.

Fixed
-----
Expand Down
18 changes: 14 additions & 4 deletions bleak/backends/bluezdbus/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,18 @@ class BleakScannerBlueZDBus(BaseBleakScanner):
``SetDiscoveryFilter`` method in the `BlueZ docs
<https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/adapter-api.txt?h=5.48&id=0d1e3b9c5754022c779da129025d493a198d49cf>`_
Keyword Args:
adapter (str): Bluetooth adapter to use for discovery.
filters (dict): A dict of filters to be applied on discovery.
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. Specifying this
also enables scanning while the screen is off on Android.
**adapter (str):
Bluetooth adapter to use for discovery.
**filters (dict):
A dict of filters to be applied on discovery.
"""

def __init__(self, **kwargs):
Expand All @@ -72,6 +80,8 @@ def __init__(self, **kwargs):

# Discovery filters
self._filters: Dict[str, Variant] = {}
if self._service_uuids:
self._filters["UUIDs"] = Variant("as", self._service_uuids)
self.set_scanning_filter(**kwargs)

async def start(self):
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
1 change: 1 addition & 0 deletions bleak/backends/p4android/defs.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
BluetoothGattDescriptor = autoclass("android.bluetooth.BluetoothGattDescriptor")
BluetoothProfile = autoclass("android.bluetooth.BluetoothProfile")
PythonActivity = autoclass("org.kivy.android.PythonActivity")
ParcelUuid = autoclass("android.os.ParcelUuid")
activity = cast("android.app.Activity", PythonActivity.mActivity)
context = cast("android.content.Context", activity.getApplicationContext())

Expand Down
36 changes: 23 additions & 13 deletions bleak/backends/p4android/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,21 @@


class BleakScannerP4Android(BaseBleakScanner):
"""
The python-for-android Bleak BLE Scanner.
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. Specifying this
also enables scanning while the screen is off on Android.
"""

__scanner = None

"""The python-for-android Bleak BLE Scanner.
Keyword Args:
filters (dict): A dict of filters to be applied on discovery. [unimplemented]
"""

def __init__(self, **kwargs):
super(BleakScannerP4Android, self).__init__(**kwargs)

Expand All @@ -38,9 +43,6 @@ def __init__(self, **kwargs):
self.__javascanner = None
self.__callback = None

# Discovery filters
self._filters = kwargs.get("filters", {})

def __del__(self):
self.__stop()

Expand Down Expand Up @@ -91,7 +93,13 @@ def handle_permissions(permissions, grantResults):
BleakScannerP4Android.__scanner = self

filters = cast("java.util.List", defs.List())
# filters could be built with defs.ScanFilterBuilder
if self._service_uuids:
for uuid in self._service_uuids:
filters.add(
defs.ScanFilterBuilder()
.setServiceUuid(defs.ParcelUuid.fromString(uuid))
.build()
)

scanfuture = self.__callback.perform_and_wait(
dispatchApi=self.__javascanner.startScan,
Expand Down Expand Up @@ -196,8 +204,10 @@ def __stop(self):
async def stop(self):
self.__stop()

async def set_scanning_filter(self, **kwargs):
self._filters = kwargs.get("filters", {})
def set_scanning_filter(self, **kwargs):
# If we do end up implementing this, this should accept List<ScanFilter>
# and ScanSettings java objects to pass to startScan().
raise NotImplementedError("not implemented in Android backend")

@property
def discovered_devices(self) -> List[BLEDevice]:
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
6 changes: 4 additions & 2 deletions bleak/backends/winrt/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,9 @@ def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs):

# Backend specific. WinRT objects.
if isinstance(address_or_ble_device, BLEDevice):
self._device_info = address_or_ble_device.details.bluetooth_address
self._device_info = (
address_or_ble_device.address.details.adv.bluetooth_address
)
else:
self._device_info = None
self._requester = None
Expand Down Expand Up @@ -166,7 +168,7 @@ async def connect(self, **kwargs) -> bool:
)

if device:
self._device_info = device.details.bluetooth_address
self._device_info = device.details.adv.bluetooth_address
else:
raise BleakError(
"Device with address {0} was not found.".format(self.address)
Expand Down
Loading

0 comments on commit b644ae2

Please sign in to comment.