Skip to content

Commit

Permalink
Cache service resolution on reconnect when using BlueZ/Dbus backend w…
Browse files Browse the repository at this point in the history
…hen requested

If the device was previously connected and we have the
the service collection already built, it can
be reused for the next connection if the device has not
since been removed from the bus when the dangerous_use_bleak_cache
flag is passed.

For devices that connect and disconnect frequently this
can make operations up to an magnitude faster once
the first connection has been made and the device
has not been removed from the bus since the last
connection.
  • Loading branch information
bdraco committed Aug 6, 2022
1 parent 88c7e6b commit f8e524c
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 5 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ Fixed
Changed
-------
* Switch to using async_timeout instead of asyncio.wait_for for performance.
* The BlueZ D-Bus backend implements a services cache between connections to significancy improve reconnect performance.
To use the cache, call ``connect`` and ``get_services`` with the ``dangerous_use_bleak_cache``
argument to avoid services being resolved again.

`0.15.1`_ (2022-08-03)
======================
Expand Down
19 changes: 15 additions & 4 deletions bleak/backends/bluezdbus/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs):

# Connectivity methods

async def connect(self, **kwargs) -> bool:
async def connect(self, dangerous_use_bleak_cache: bool = False, **kwargs) -> bool:
"""Connect to the specified GATT server.
Keyword Args:
Expand Down Expand Up @@ -202,7 +202,11 @@ def on_value_changed(char_path: str, value: bytes) -> None:
asyncio.ensure_future(self._disconnect_monitor())

# Get all services. This means making the actual connection.
await self.get_services()
#
# We will try to use the cache if it exists and `dangerous_use_bleak_cache`
# is True.
#
await self.get_services(dangerous_use_bleak_cache=dangerous_use_bleak_cache)

return True
except BaseException:
Expand Down Expand Up @@ -469,9 +473,14 @@ def mtu_size(self) -> int:

# GATT services methods

async def get_services(self, **kwargs) -> BleakGATTServiceCollection:
async def get_services(
self, dangerous_use_bleak_cache: bool = False, **kwargs
) -> BleakGATTServiceCollection:
"""Get all services registered for this GATT server.
Args:
dangerous_use_bleak_cache (bool): Use cached services if available.
Returns:
A :py:class:`bleak.backends.service.BleakGATTServiceCollection` with this device's services tree.
Expand All @@ -484,7 +493,9 @@ async def get_services(self, **kwargs) -> BleakGATTServiceCollection:

manager = await get_global_bluez_manager()

self.services = await manager.get_services(self._device_path)
self.services = await manager.get_services(
self._device_path, dangerous_use_bleak_cache
)
self._services_resolved = True

return self.services
Expand Down
18 changes: 17 additions & 1 deletion bleak/backends/bluezdbus/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ def __init__(self):
self._advertisement_callbacks: List[CallbackAndState] = []
self._device_watchers: Set[DeviceWatcher] = set()
self._condition_callbacks: Set[Callable] = set()
self._services_cache: Dict[str, BleakGATTServiceCollection] = {}

async def async_init(self):
"""
Expand All @@ -288,6 +289,8 @@ async def async_init(self):
if self._bus and self._bus.connected:
return

self._services_cache = {}

# We need to create a new MessageBus each time as
# dbus-next will destory the underlying file descriptors
# when the previous one is closed in its finalizer.
Expand Down Expand Up @@ -561,7 +564,9 @@ def remove_device_watcher(self, watcher: DeviceWatcher) -> None:
"""
self._device_watchers.remove(watcher)

async def get_services(self, device_path: str) -> BleakGATTServiceCollection:
async def get_services(
self, device_path: str, use_cached: bool
) -> BleakGATTServiceCollection:
"""
Builds a new :class:`BleakGATTServiceCollection` from the current state.
Expand All @@ -571,6 +576,12 @@ async def get_services(self, device_path: str) -> BleakGATTServiceCollection:
Returns:
A new :class:`BleakGATTServiceCollection`.
"""
if use_cached:
services = self._services_cache.get(device_path)
if services is not None:
logger.debug("Using cached services for %s", device_path)
return services

await self._wait_condition(device_path, "ServicesResolved", True)

services = BleakGATTServiceCollection()
Expand Down Expand Up @@ -620,6 +631,8 @@ async def get_services(self, device_path: str) -> BleakGATTServiceCollection:

services.add_descriptor(desc)

self._services_cache[device_path] = services

return services

def get_device_name(self, device_path: str) -> str:
Expand Down Expand Up @@ -713,6 +726,9 @@ def _parse_msg(self, message: Message):

for interface in interfaces:
del self._properties[obj_path][interface]

if interface == defs.DEVICE_INTERFACE:
self._services_cache.pop(obj_path, None)
elif message.member == "PropertiesChanged":
assert message.path is not None

Expand Down
7 changes: 7 additions & 0 deletions docs/backends/linux.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,10 @@ Before that commit, ``Characteristic.WriteValue`` was only "Write with response"
`Bluez 5.46 <https://git.kernel.org/pub/scm/bluetooth/bluez.git/commit/doc/gatt-api.txt?id=f59f3dedb2c79a75e51a3a0d27e2ae06fefc603e>`_
which can be used to "Write without response", but for older versions of Bluez (5.43, 5.44, 5.45), it is not possible to "Write without response".


Resolving services with ``get_services``
----------------------------------------

When reconnecting to a device that has been connected before, and is still present on the bus, the ``.services`` property of the device will be cached. While its rare that devices change their services between connections, calling ``get_services`` will wait for the services to be resolved and update the ``.services`` property.

By default, calling ``get_services`` will wait for services to be resolved before returning the ``BleakGATTServiceCollection``. If a previous connection to the device was made, passing the ``dangerous_use_bleak_cache`` argument will return the cached services without waiting for them to be resolved again. This is useful when you know services have not changed, and you want to use the services immediately, but don't want to wait for them to be resolved again.

0 comments on commit f8e524c

Please sign in to comment.