Skip to content

Commit

Permalink
backends/scanner: improve BLEDevice creation performance
Browse files Browse the repository at this point in the history
This changes all scanner backends so that they only create a BLEDevice
object once per scan. For example, in several backends, a new BLEDevice
was created for each discovered device each time the discovered_devices
property was called, which could be significant if many (~100) devices
were discovered.

This also caches the last advertisement data along with the BLEDevice
which is expected to be used as an alternative to BLEDevice.metadata
in the future.
  • Loading branch information
dlech committed Oct 3, 2022
1 parent a3c4dd5 commit 0b9a210
Show file tree
Hide file tree
Showing 11 changed files with 199 additions and 235 deletions.
2 changes: 1 addition & 1 deletion bleak/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ def discovered_devices(self) -> List[BLEDevice]:
Returns:
A list of the devices that the scanner has discovered during the scanning.
"""
return self._backend.discovered_devices
return [d for d, _ in self._backend.seen_devices.values()]

async def get_discovered_devices(self) -> List[BLEDevice]:
"""Gets the devices registered by the BleakScanner.
Expand Down
61 changes: 25 additions & 36 deletions bleak/backends/bluezdbus/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from .advertisement_monitor import OrPatternLike
from .defs import Device1
from .manager import get_global_bluez_manager
from .utils import bdaddr_from_device_path

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -89,9 +90,6 @@ def __init__(
# kwarg "device" is for backwards compatibility
self._adapter: Optional[str] = kwargs.get("adapter", kwargs.get("device"))

# map of d-bus object path to d-bus object properties
self._devices: Dict[str, Device1] = {}

# callback from manager for stopping scanning if it has been started
self._stop: Optional[Callable[[], Coroutine]] = None

Expand Down Expand Up @@ -137,7 +135,7 @@ async def start(self):
else:
adapter_path = manager.get_default_adapter()

self._devices.clear()
self.seen_devices = {}

if self._scanning_mode == "passive":
self._stop = await manager.passive_scan(
Expand Down Expand Up @@ -192,25 +190,6 @@ def set_scanning_filter(self, **kwargs):
else:
logger.warning("Filter '%s' is not currently supported." % k)

@property
def discovered_devices(self) -> List[BLEDevice]:
discovered_devices = []

for path, props in self._devices.items():
uuids = props.get("UUIDs", [])
manufacturer_data = props.get("ManufacturerData", {})
discovered_devices.append(
BLEDevice(
props["Address"],
props["Alias"],
{"path": path, "props": props},
props.get("RSSI", 0),
uuids=uuids,
manufacturer_data=manufacturer_data,
)
)
return discovered_devices

# Helper methods

def _handle_advertising_data(self, path: str, props: Device1) -> None:
Expand All @@ -222,11 +201,6 @@ def _handle_advertising_data(self, path: str, props: Device1) -> None:
props: The D-Bus object properties of the device.
"""

self._devices[path] = props

if self._callback is None:
return

# Get all the information wanted to pack in the advertisement data
_local_name = props.get("Name")
_manufacturer_data = {
Expand All @@ -244,29 +218,44 @@ def _handle_advertising_data(self, path: str, props: Device1) -> None:
manufacturer_data=_manufacturer_data,
service_data=_service_data,
service_uuids=_service_uuids,
platform_data=props,
platform_data=(path, props),
tx_power=tx_power,
)

device = BLEDevice(
props["Address"],
props["Alias"],
{"path": path, "props": props},
props.get("RSSI", 0),
metadata = dict(
uuids=_service_uuids,
manufacturer_data=_manufacturer_data,
)

try:
device, _ = self.seen_devices[props["Address"]]

device.metadata = metadata
except KeyError:
device = BLEDevice(
props["Address"],
props["Alias"],
{"path": path, "props": props},
props.get("RSSI", 0),
**metadata,
)

self.seen_devices[props["Address"]] = (device, advertisement_data)

if self._callback is None:
return

self._callback(device, advertisement_data)

def _handle_device_removed(self, device_path: str) -> None:
"""
Handles a device being removed from BlueZ.
"""
try:
del self._devices[device_path]
bdaddr = bdaddr_from_device_path(device_path)
del self.seen_devices[bdaddr]
except KeyError:
# The device will not have been added to self._devices if no
# The device will not have been added to self.seen_devices if no
# advertising data was received, so this is expected to happen
# occasionally.
pass
13 changes: 13 additions & 0 deletions bleak/backends/bluezdbus/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,16 @@ def extract_service_handle_from_path(path):
return int(path[-4:], 16)
except Exception as e:
raise BleakError(f"Could not parse service handle from path: {path}") from e


def bdaddr_from_device_path(device_path: str) -> str:
"""
Scrape the Bluetooth address from a D-Bus device path.
Args:
device_path: The D-Bus object path of the device.
Returns:
A Bluetooth address as a string.
"""
return ":".join(device_path[-17:].split("_"))
7 changes: 0 additions & 7 deletions bleak/backends/corebluetooth/CentralManagerDelegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ def init(self) -> Optional["CentralManagerDelegate"]:
self.event_loop = asyncio.get_running_loop()
self._connect_futures: Dict[NSUUID, asyncio.Future] = {}

self.last_rssi: Dict[str, int] = {}

self.callbacks: Dict[
int, Callable[[CBPeripheral, Dict[str, Any], int], None]
] = {}
Expand Down Expand Up @@ -108,9 +106,6 @@ def __del__(self):

@objc.python_method
async def start_scan(self, service_uuids) -> None:
# remove old
self.last_rssi.clear()

service_uuids = (
NSArray.alloc().initWithArray_(
list(map(CBUUID.UUIDWithString_, service_uuids))
Expand Down Expand Up @@ -254,8 +249,6 @@ def did_discover_peripheral(

uuid_string = peripheral.identifier().UUIDString()

self.last_rssi[uuid_string] = RSSI

for callback in self.callbacks.values():
if callback:
callback(peripheral, advertisementData, RSSI)
Expand Down
9 changes: 5 additions & 4 deletions bleak/backends/corebluetooth/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs):
self._central_manager_delegate: Optional[CentralManagerDelegate] = None

if isinstance(address_or_ble_device, BLEDevice):
self._peripheral = address_or_ble_device.details
self._central_manager_delegate = address_or_ble_device.metadata["delegate"]
(
self._peripheral,
self._central_manager_delegate,
) = address_or_ble_device.details

self._services: Optional[NSArray] = None

Expand All @@ -77,8 +79,7 @@ async def connect(self, **kwargs) -> bool:
)

if device:
self._peripheral = device.details
self._central_manager_delegate = device.metadata["delegate"]
self._peripheral, self._central_manager_delegate = device.details
else:
raise BleakDeviceNotFoundError(
self.address, f"Device with address {self.address} was not found"
Expand Down
85 changes: 20 additions & 65 deletions bleak/backends/corebluetooth/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

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

from ...exc import BleakError
from ..device import BLEDevice
Expand Down Expand Up @@ -62,7 +62,6 @@ def __init__(
if scanning_mode == "passive":
raise BleakError("macOS does not support passive scanning")

self._identifiers: Optional[Dict[NSUUID, Dict[str, Any]]] = None
self._manager = CentralManagerDelegate.alloc().init()
self._timeout: float = kwargs.get("timeout", 5.0)
if (
Expand All @@ -77,14 +76,9 @@ def __init__(
)

async def start(self):
self._identifiers = {}
self.seen_devices = {}

def callback(p: CBPeripheral, a: Dict[str, Any], r: int) -> None:
# update identifiers for scanned device
self._identifiers.setdefault(p.identifier(), {}).update(a)

if not self._callback:
return

# Process service data
service_data_dict_raw = a.get("kCBAdvDataServiceData", {})
Expand Down Expand Up @@ -118,17 +112,28 @@ def callback(p: CBPeripheral, a: Dict[str, Any], r: int) -> None:
tx_power=tx_power,
)

device = BLEDevice(
p.identifier().UUIDString(),
p.name(),
p,
r,
metadata = dict(
uuids=service_uuids,
manufacturer_data=manufacturer_data,
service_data=service_data,
delegate=self._manager.central_manager.delegate(),
)

try:
device, _ = self.seen_devices[p.identifier().UUIDString()]
device.metadata = metadata
except KeyError:
device = BLEDevice(
p.identifier().UUIDString(),
p.name(),
(p, self._manager.central_manager.delegate()),
r,
**metadata
)

self.seen_devices[device.address] = (device, advertisement_data)

if not self._callback:
return

self._callback(device, advertisement_data)

self._manager.callbacks[id(self)] = callback
Expand All @@ -154,56 +159,6 @@ def set_scanning_filter(self, **kwargs):
"Need to evaluate which macOS versions to support first..."
)

@property
def discovered_devices(self) -> List[BLEDevice]:
found = []
peripherals = self._manager.central_manager.retrievePeripheralsWithIdentifiers_(
NSArray(self._identifiers.keys()),
)

for peripheral in peripherals:
address = peripheral.identifier().UUIDString()
name = peripheral.name() or "Unknown"
details = peripheral
rssi = self._manager.last_rssi[address]

advertisementData = self._identifiers[peripheral.identifier()]
manufacturer_binary_data = advertisementData.get(
"kCBAdvDataManufacturerData"
)
manufacturer_data = {}
if manufacturer_binary_data:
manufacturer_id = int.from_bytes(
manufacturer_binary_data[0:2], byteorder="little"
)
manufacturer_value = bytes(manufacturer_binary_data[2:])
manufacturer_data = {manufacturer_id: manufacturer_value}

uuids = [
cb_uuid_to_str(u)
for u in advertisementData.get("kCBAdvDataServiceUUIDs", [])
]

service_data = {}
adv_service_data = advertisementData.get("kCBAdvDataServiceData", [])
for u in adv_service_data:
service_data[cb_uuid_to_str(u)] = bytes(adv_service_data[u])

found.append(
BLEDevice(
address,
name,
details,
rssi=rssi,
uuids=uuids,
manufacturer_data=manufacturer_data,
service_data=service_data,
delegate=self._manager.central_manager.delegate(),
)
)

return found

# macOS specific methods

@property
Expand Down
14 changes: 2 additions & 12 deletions bleak/backends/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,7 @@ def __init__(self, address, name=None, details=None, rssi=0, **kwargs):
self.metadata = kwargs

def __str__(self):
if not self.name:
if "manufacturer_data" in self.metadata:
ks = list(self.metadata["manufacturer_data"].keys())
if len(ks):
mf = MANUFACTURERS.get(ks[0], MANUFACTURERS.get(0xFFFF))
value = self.metadata["manufacturer_data"].get(
ks[0], MANUFACTURERS.get(0xFFFF)
)
# TODO: Evaluate how to interpret the value of the company identifier...
return "{0}: {1} ({2})".format(self.address, mf, value)
return "{0}: {1}".format(self.address, self.name or "Unknown")
return f"{self.address}: {self.name}"

def __repr__(self):
return str(self)
return f"BLEDevice({self.address}, {self.name})"
Loading

0 comments on commit 0b9a210

Please sign in to comment.