From da5ff1fb66c2e9eb9cc85c8d37998da555023fa1 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Tue, 26 Jul 2022 10:37:43 -0500 Subject: [PATCH] bluezdbus/manager: call advertisement callbacks from InterfacesAdded We have found that when doing passive scanning, some devices only send one advertisement and then go to sleep for a while. BlueZ triggers InterfacesAdded and InterfacesRemoved signals for these devices since the sleep time is long enough for them to be considered no longer present. This caused advertisements to be missed since we were previously just relying on PropertiesChanged signals with an RSSI change to determine if a device is actually advertising. By calling the callbacks on InterfacesAdded signals as well, we can catch the very first and possibly only advertisement from these sorts of devices. --- CHANGELOG.rst | 1 + bleak/backends/bluezdbus/manager.py | 94 +++++++++++++++++++---------- 2 files changed, 63 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b373d806..508049df 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -38,6 +38,7 @@ Fixed * Documentation fixes. * On empty characteristic description from WinRT, use the lookup table instead of returning empty string. +* Fixed detection of first advertisement in BlueZ backend. Merged #903. `0.14.3`_ (2022-04-29) diff --git a/bleak/backends/bluezdbus/manager.py b/bleak/backends/bluezdbus/manager.py index ab50dcd5..51673c03 100644 --- a/bleak/backends/bluezdbus/manager.py +++ b/bleak/backends/bluezdbus/manager.py @@ -9,7 +9,18 @@ import asyncio import logging import os -from typing import Any, Callable, Coroutine, Dict, List, NamedTuple, Set, Tuple, cast +from typing import ( + Any, + Callable, + Coroutine, + Dict, + Iterable, + List, + NamedTuple, + Set, + Tuple, + cast, +) from dbus_next import BusType, Message, MessageType, Variant from dbus_next.aio.message_bus import MessageBus @@ -484,9 +495,18 @@ def _parse_msg(self, message: Message): obj_path, interfaces_and_props = message.body for interface, props in interfaces_and_props.items(): - self._properties.setdefault(obj_path, {})[interface] = unpack_variants( - props - ) + unpacked_props = unpack_variants(props) + self._properties.setdefault(obj_path, {})[interface] = unpacked_props + + # If this is a device and it has advertising data properties, + # then it should mean that this device just started advertising. + # Previously, we just relied on RSSI updates to determine if + # a device was actually advertising, but we were missing "slow" + # devices that only advertise once and then go to sleep for a while. + if interface == defs.DEVICE_INTERFACE: + self._run_advertisement_callbacks( + obj_path, cast(Device1, unpacked_props), unpacked_props.keys() + ) elif message.member == "InterfacesRemoved": obj_path, interfaces = message.body @@ -509,37 +529,47 @@ def _parse_msg(self, message: Message): else: self_interface.update(unpack_variants(changed)) - if interface == defs.DEVICE_INTERFACE: - for ( - callback, - adapter_path, - seen_devices, - ) in self._advertisement_callbacks: - # filter messages from other adapters - if not message.path.startswith(adapter_path): - continue - - first_time_seen = False - - if message.path not in seen_devices: - first_time_seen = True - seen_devices.add(message.path) - - # Only do advertising data callback if this is the first time the - # device has been seen or if an advertising data property changed. - # Otherwise we get a flood of callbacks from RSSI changing. - if ( - first_time_seen - or not _ADVERTISING_DATA_PROPERTIES.isdisjoint( - changed.keys() - ) - ): - # TODO: this should be deep copy, not shallow - callback(message.path, cast(Device1, self_interface.copy())) - for name in invalidated: del self_interface[name] + if interface == defs.DEVICE_INTERFACE: + self._run_advertisement_callbacks( + message.path, cast(Device1, self_interface), changed.keys() + ) + + def _run_advertisement_callbacks( + self, device_path: str, device: Device1, changed: Iterable[str] + ) -> None: + """ + Runs any registered advertisement callbacks. + + Args: + device_path: The D-Bus object path of the remote device. + device: The current D-Bus properties of the device. + changed: A list of properties that have changed since the last call. + """ + for ( + callback, + adapter_path, + seen_devices, + ) in self._advertisement_callbacks: + # filter messages from other adapters + if not device_path.startswith(adapter_path): + continue + + first_time_seen = False + + if device_path not in seen_devices: + first_time_seen = True + seen_devices.add(device_path) + + # Only do advertising data callback if this is the first time the + # device has been seen or if an advertising data property changed. + # Otherwise we get a flood of callbacks from RSSI changing. + if first_time_seen or not _ADVERTISING_DATA_PROPERTIES.isdisjoint(changed): + # TODO: this should be deep copy, not shallow + callback(device_path, cast(Device1, device.copy())) + async def get_global_bluez_manager() -> BlueZManager: """