Skip to content

Commit

Permalink
Changed Service identification to handles instead of UUID (#449)
Browse files Browse the repository at this point in the history
Changed Service identification to handles for Windows pythonnet backend.
Changed Service identification to handles for BlueZ txdbus backend
Implement service handle for Core Bluetooth
Updated CHANGELOG.rst

Fixes #445
Fixes #362
  • Loading branch information
hbldh committed Mar 1, 2021
1 parent 3e79f4a commit 4a3f862
Show file tree
Hide file tree
Showing 15 changed files with 132 additions and 50 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Added
* Keyword argument ``use_cached`` on .NET backend, to enable uncached reading
of services, characteristics and descriptors in Windows.
* Documentation on troubleshooting OS level caches for services.
* ``handle`` property on ``BleakGATTService`` objects
* ``service_handle`` property on ``BleakGATTCharacteristic`` objects

Fixed
~~~~~
Expand All @@ -32,6 +34,18 @@ Fixed
backend. Merged #401.
* Fixed RSSI missing in discovered devices on macOS backend. Merged #400.
* Fixed a broken check for the correct adapter in ``BleakClientBlueZDBus``.
* Fixed #445 and #362 for Windows.

Changed
~~~~~~~

* Using handles to identify the services. Added `handle` abstract property to `BleakGATTService`
and storing the services by handle instead of UUID.

Removed
~~~~~~~
* Removed all ``__str__`` methods from backend service, characteristic and descriptor implementations
in favour of those in the abstract base classes.


`0.10.0`_ (2020-12-11)
Expand Down
9 changes: 8 additions & 1 deletion bleak/backends/bluezdbus/characteristic.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,14 @@
class BleakGATTCharacteristicBlueZDBus(BleakGATTCharacteristic):
"""GATT Characteristic implementation for the BlueZ DBus backend"""

def __init__(self, obj: dict, object_path: str, service_uuid: str):
def __init__(
self, obj: dict, object_path: str, service_uuid: str, service_handle: int
):
super(BleakGATTCharacteristicBlueZDBus, self).__init__(obj)
self.__descriptors = []
self.__path = object_path
self.__service_uuid = service_uuid
self.__service_handle = service_handle

# D-Bus object path contains handle as last 4 characters of 'charYYYY'
self._handle = int(object_path[-4:], 16)
Expand All @@ -43,6 +46,10 @@ def service_uuid(self) -> str:
"""The uuid of the Service containing this characteristic"""
return self.__service_uuid

@property
def service_handle(self) -> int:
return self.__service_handle

@property
def handle(self) -> int:
"""The handle of this characteristic"""
Expand Down
4 changes: 3 additions & 1 deletion bleak/backends/bluezdbus/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,9 @@ async def get_services(self, **kwargs) -> BleakGATTServiceCollection:
for char, object_path in _chars:
_service = list(filter(lambda x: x.path == char["Service"], self.services))
self.services.add_characteristic(
BleakGATTCharacteristicBlueZDBus(char, object_path, _service[0].uuid)
BleakGATTCharacteristicBlueZDBus(
char, object_path, _service[0].uuid, _service[0].handle
)
)

# D-Bus object path contains handle as last 4 characters of 'charYYYY'
Expand Down
7 changes: 7 additions & 0 deletions bleak/backends/bluezdbus/service.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import List

from bleak.backends.bluezdbus.utils import extract_service_handle_from_path
from bleak.backends.service import BleakGATTService
from bleak.backends.bluezdbus.characteristic import BleakGATTCharacteristicBlueZDBus

Expand All @@ -11,12 +12,18 @@ def __init__(self, obj, path):
super().__init__(obj)
self.__characteristics = []
self.__path = path
self.__handle = extract_service_handle_from_path(path)

@property
def uuid(self) -> str:
"""The UUID to this service"""
return self.obj["UUID"]

@property
def handle(self) -> str:
"""The integer handle of this service"""
return self.__handle

@property
def characteristics(self) -> List[BleakGATTCharacteristicBlueZDBus]:
"""List of characteristics for this service"""
Expand Down
8 changes: 8 additions & 0 deletions bleak/backends/bluezdbus/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import asyncio
import re

from bleak import BleakError
from bleak.uuids import uuidstr_to_str

from bleak.backends.bluezdbus import defs
Expand Down Expand Up @@ -46,3 +47,10 @@ def format_GATT_object(object_path, interfaces):
return "\n{0}\n\t{1}\n\t{2}\n\t{3}".format(
_type, object_path, _uuid, uuidstr_to_str(_uuid)
)


def extract_service_handle_from_path(path):
try:
return int(path[-4:], 16)
except Exception as e:
raise BleakError(f"Could not parse service handle from path: {path}") from e
8 changes: 7 additions & 1 deletion bleak/backends/characteristic.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,20 @@ def __init__(self, obj: Any):
self.obj = obj

def __str__(self):
return "{0}: {1}".format(self.uuid, self.description)
return f"{self.uuid} (Handle: {self.handle}): {self.description}"

@property
@abc.abstractmethod
def service_uuid(self) -> str:
"""The UUID of the Service containing this characteristic"""
raise NotImplementedError()

@property
@abc.abstractmethod
def service_handle(self) -> int:
"""The integer handle of the Service containing this characteristic"""
raise NotImplementedError()

@property
@abc.abstractmethod
def handle(self) -> int:
Expand Down
7 changes: 4 additions & 3 deletions bleak/backends/corebluetooth/characteristic.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,15 @@ def __init__(self, obj: CBCharacteristic):
]
self._uuid = cb_uuid_to_str(self.obj.UUID())

def __str__(self):
return "{0}: {1}".format(self.uuid, self.description)

@property
def service_uuid(self) -> str:
"""The uuid of the Service containing this characteristic"""
return cb_uuid_to_str(self.obj.service().UUID())

@property
def service_handle(self) -> int:
return int(self.obj.service().startHandle())

@property
def handle(self) -> int:
"""Integer handle for this characteristic"""
Expand Down
3 changes: 0 additions & 3 deletions bleak/backends/corebluetooth/descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ def __init__(
self.__characteristic_uuid = characteristic_uuid
self.__characteristic_handle = characteristic_handle

def __str__(self):
return "{0}: (Handle: {1})".format(self.uuid, self.handle)

@property
def characteristic_handle(self) -> int:
"""handle for the characteristic that this descriptor belongs to"""
Expand Down
8 changes: 8 additions & 0 deletions bleak/backends/corebluetooth/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ class BleakGATTServiceCoreBluetooth(BleakGATTService):
def __init__(self, obj: CBService):
super().__init__(obj)
self.__characteristics = []
# N.B. the `startHandle` method of the CBService is an undocumented Core Bluetooth feature,
# which Bleak takes advantage of in order to have a service handle to use.
self.__handle = int(self.obj.startHandle())

@property
def handle(self) -> int:
"""The integer handle of this service"""
return self.__handle

@property
def uuid(self) -> str:
Expand Down
2 changes: 1 addition & 1 deletion bleak/backends/descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def __init__(self, obj: Any):
self.obj = obj

def __str__(self):
return "{0}: {1}".format(self.uuid, self.description)
return f"{self.uuid} (Handle: {self.handle}): {self.description}"

@property
@abc.abstractmethod
Expand Down
8 changes: 5 additions & 3 deletions bleak/backends/dotnet/characteristic.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,16 @@ def __init__(self, obj: GattCharacteristic):
if (self.obj.CharacteristicProperties & v)
]

def __str__(self):
return "[{0}] {1}: {2}".format(self.handle, self.uuid, self.description)

@property
def service_uuid(self) -> str:
"""The uuid of the Service containing this characteristic"""
return self.obj.Service.Uuid.ToString()

@property
def service_handle(self) -> int:
"""The integer handle of the Service containing this characteristic"""
return int(self.obj.Service.AttributeHandle)

@property
def handle(self) -> int:
"""The handle of this characteristic"""
Expand Down
3 changes: 0 additions & 3 deletions bleak/backends/dotnet/descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ def __init__(
self.__characteristic_uuid = characteristic_uuid
self.__characteristic_handle = characteristic_handle

def __str__(self):
return "{0}: (Handle: {1})".format(self.uuid, self.handle)

@property
def characteristic_handle(self) -> int:
"""handle for the characteristic that this descriptor belongs to"""
Expand Down
5 changes: 5 additions & 0 deletions bleak/backends/dotnet/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ def __init__(self, obj: GattDeviceService):
# BleakGATTCharacteristicDotNet(c) for c in obj.GetAllCharacteristics()
]

@property
def handle(self) -> str:
"""The handle of this service"""
return int(self.obj.AttributeHandle)

@property
def uuid(self) -> str:
"""UUID for this service."""
Expand Down
60 changes: 43 additions & 17 deletions bleak/backends/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"""
import abc
from uuid import UUID
from typing import List, Union, Iterator
from typing import Dict, List, Optional, Union, Iterator

from bleak import BleakError
from bleak.uuids import uuidstr_to_str
Expand All @@ -22,7 +22,13 @@ def __init__(self, obj):
self.obj = obj

def __str__(self):
return "{0}: {1}".format(self.uuid, self.description)
return f"{self.uuid} (Handle: {self.handle}): {self.description}"

@property
@abc.abstractmethod
def handle(self) -> str:
"""The handle of this service"""
raise NotImplementedError()

@property
@abc.abstractmethod
Expand Down Expand Up @@ -79,46 +85,66 @@ def __init__(self):

def __getitem__(
self, item: Union[str, int, UUID]
) -> Union[BleakGATTService, BleakGATTCharacteristic, BleakGATTDescriptor]:
) -> Optional[
Union[BleakGATTService, BleakGATTCharacteristic, BleakGATTDescriptor]
]:
"""Get a service, characteristic or descriptor from uuid or handle"""
return self.services.get(
str(item), self.characteristics.get(item, self.descriptors.get(item, None))
return (
self.get_service(item)
or self.get_characteristic(item)
or self.get_descriptor(item)
)

def __iter__(self) -> Iterator[BleakGATTService]:
"""Returns an iterator over all BleakGATTService objects"""
return iter(self.services.values())

@property
def services(self) -> dict:
"""Returns dictionary of UUID strings to BleakGATTService"""
def services(self) -> Dict[int, BleakGATTService]:
"""Returns dictionary of handles mapping to BleakGATTService"""
return self.__services

@property
def characteristics(self) -> dict:
"""Returns dictionary of handles to BleakGATTCharacteristic"""
def characteristics(self) -> Dict[int, BleakGATTCharacteristic]:
"""Returns dictionary of handles mapping to BleakGATTCharacteristic"""
return self.__characteristics

@property
def descriptors(self) -> dict:
"""Returns a dictionary of integer handles to BleakGATTDescriptor"""
def descriptors(self) -> Dict[int, BleakGATTDescriptor]:
"""Returns a dictionary of integer handles mapping to BleakGATTDescriptor"""
return self.__descriptors

def add_service(self, service: BleakGATTService):
"""Add a :py:class:`~BleakGATTService` to the service collection.
Should not be used by end user, but rather by `bleak` itself.
"""
if service.uuid not in self.__services:
self.__services[service.uuid] = service
if service.handle not in self.__services:
self.__services[service.handle] = service
else:
raise BleakError(
"This service is already present in this BleakGATTServiceCollection!"
)

def get_service(self, _uuid: Union[str, UUID]) -> BleakGATTService:
"""Get a service by UUID string"""
return self.services.get(str(_uuid).lower(), None)
def get_service(self, specifier: Union[int, str, UUID]) -> BleakGATTService:
"""Get a service by handle (int) or UUID (str or uuid.UUID)"""
if isinstance(specifier, int):
return self.services.get(specifier, None)
else:
_specifier = str(specifier).lower()
# Assume uuid usage.
x = list(
filter(
lambda x: x.uuid.lower() == _specifier,
self.services.values(),
)
)
if len(x) > 1:
raise BleakError(
"Multiple Services with this UUID, refer to your desired service by the `handle` attribute instead."
)
else:
return x[0] if x else None

def add_characteristic(self, characteristic: BleakGATTCharacteristic):
"""Add a :py:class:`~BleakGATTCharacteristic` to the service collection.
Expand All @@ -127,7 +153,7 @@ def add_characteristic(self, characteristic: BleakGATTCharacteristic):
"""
if characteristic.handle not in self.__characteristics:
self.__characteristics[characteristic.handle] = characteristic
self.__services[characteristic.service_uuid].add_characteristic(
self.__services[characteristic.service_handle].add_characteristic(
characteristic
)
else:
Expand Down
36 changes: 19 additions & 17 deletions examples/service_explorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,35 +26,37 @@ async def run(address, debug=False):
log.addHandler(h)

async with BleakClient(address) as client:
x = await client.is_connected()
log.info("Connected: {0}".format(x))
is_connected = await client.is_connected()
log.info(f"Connected: {is_connected}")

for service in client.services:
log.info("[Service] {0}: {1}".format(service.uuid, service.description))
log.info(f"[Service] {service}")
for char in service.characteristics:
if "read" in char.properties:
try:
value = bytes(await client.read_gatt_char(char.uuid))
log.info(
f"\t[Characteristic] {char} ({','.join(char.properties)}), Value: {value}"
)
except Exception as e:
value = str(e).encode()
log.error(
f"\t[Characteristic] {char} ({','.join(char.properties)}), Value: {e}"
)

else:
value = None
log.info(
"\t[Characteristic] {0}: (Handle: {1}) ({2}) | Name: {3}, Value: {4} ".format(
char.uuid,
char.handle,
",".join(char.properties),
char.description,
value,
log.info(
f"\t[Characteristic] {char} ({','.join(char.properties)}), Value: {value}"
)
)

for descriptor in char.descriptors:
value = await client.read_gatt_descriptor(descriptor.handle)
log.info(
"\t\t[Descriptor] {0}: (Handle: {1}) | Value: {2} ".format(
descriptor.uuid, descriptor.handle, bytes(value)
try:
value = bytes(
await client.read_gatt_descriptor(descriptor.handle)
)
)
log.info(f"\t\t[Descriptor] {descriptor}) | Value: {value}")
except Exception as e:
log.error(f"\t\t[Descriptor] {descriptor}) | Value: {e}")


if __name__ == "__main__":
Expand Down

0 comments on commit 4a3f862

Please sign in to comment.