From f9f694dfcfd7d7801c37f2b08330e04f61c010e3 Mon Sep 17 00:00:00 2001 From: Josh Wu Date: Mon, 8 May 2023 14:56:20 +0800 Subject: [PATCH 1/4] Replace list[] legacy typing --- bumble/device.py | 2 +- bumble/gatt.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bumble/device.py b/bumble/device.py index 72fd755..258a43d 100644 --- a/bumble/device.py +++ b/bumble/device.py @@ -889,7 +889,7 @@ def host_event_handler(function): # List of host event handlers for the Device class. # (we define this list outside the class, because referencing a class in method # decorators is not straightforward) -device_host_event_handlers: list[str] = [] +device_host_event_handlers: List[str] = [] # ----------------------------------------------------------------------------- diff --git a/bumble/gatt.py b/bumble/gatt.py index e57f0a6..9721fd4 100644 --- a/bumble/gatt.py +++ b/bumble/gatt.py @@ -206,7 +206,7 @@ class Service(Attribute): uuid: UUID - def __init__(self, uuid, characteristics: list[Characteristic], primary=True): + def __init__(self, uuid, characteristics: List[Characteristic], primary=True): # Convert the uuid to a UUID object if it isn't already if isinstance(uuid, str): uuid = UUID(uuid) From e9bf5757c4d66d48dfc2ac7d2545a96f608f3487 Mon Sep 17 00:00:00 2001 From: Josh Wu Date: Fri, 5 May 2023 17:48:10 +0800 Subject: [PATCH 2/4] Implement GATT client included service discovery --- bumble/gatt_client.py | 65 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/bumble/gatt_client.py b/bumble/gatt_client.py index 35d8eb7..a33039e 100644 --- a/bumble/gatt_client.py +++ b/bumble/gatt_client.py @@ -63,6 +63,7 @@ from .gatt import ( GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE, GATT_REQUEST_TIMEOUT, GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE, + GATT_INCLUDE_ATTRIBUTE_TYPE, Characteristic, ClientCharacteristicConfigurationBits, ) @@ -109,6 +110,7 @@ class AttributeProxy(EventEmitter): class ServiceProxy(AttributeProxy): uuid: UUID characteristics: List[CharacteristicProxy] + included_services: List[ServiceProxy] @staticmethod def from_client(service_class, client, service_uuid): @@ -502,12 +504,69 @@ class Client: return services - async def discover_included_services(self, _service): + async def discover_included_services( + self, service: ServiceProxy + ) -> List[ServiceProxy]: ''' See Vol 3, Part G - 4.5.1 Find Included Services ''' - # TODO - return [] + + starting_handle = service.handle + ending_handle = service.end_group_handle + + included_services: List[ServiceProxy] = [] + while starting_handle <= ending_handle: + response = await self.send_request( + ATT_Read_By_Type_Request( + starting_handle=starting_handle, + ending_handle=ending_handle, + attribute_type=GATT_INCLUDE_ATTRIBUTE_TYPE, + ) + ) + if response is None: + # TODO raise appropriate exception + return [] + + # Check if we reached the end of the iteration + if response.op_code == ATT_ERROR_RESPONSE: + if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR: + # Unexpected end + logger.warning( + '!!! unexpected error while discovering included services: ' + f'{HCI_Constant.error_name(response.error_code)}' + ) + raise ATT_Error( + error_code=response.error_code, + message='Unexpected error while discovering included services', + ) + break + + # Stop if for some reason the list was empty + if not response.attributes: + break + + # Process all included services returned in this iteration + for attribute_handle, attribute_value in response.attributes: + if attribute_handle < starting_handle: + # Something's not right + logger.warning(f'bogus handle value: {attribute_handle}') + return [] + + group_starting_handle, group_ending_handle = struct.unpack_from( + ' Date: Fri, 5 May 2023 17:53:22 +0800 Subject: [PATCH 3/4] Add self GATT included service tests --- tests/self_test.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/self_test.py b/tests/self_test.py index 9105d2b..1a1a474 100644 --- a/tests/self_test.py +++ b/tests/self_test.py @@ -190,7 +190,9 @@ async def test_self_gatt(): s1 = Service('8140E247-04F0-42C1-BC34-534C344DAFCA', [c1, c2, c3]) s2 = Service('97210A0F-1875-4D05-9E5D-326EB171257A', [c4]) - two_devices.devices[1].add_services([s1, s2]) + s3 = Service('1853', []) + s4 = Service('3A12C182-14E2-4FE0-8C5B-65D7C569F9DB', [], included_services=[s2, s3]) + two_devices.devices[1].add_services([s1, s2, s4]) # Start await two_devices.devices[0].power_on() @@ -225,6 +227,13 @@ async def test_self_gatt(): assert result is not None assert result == c1.value + result = await peer.discover_service(s4.uuid) + assert len(result) == 1 + result = await peer.discover_included_services(result[0]) + assert len(result) == 2 + # Service UUID is only present when the UUID is 16-bit Bluetooth UUID + assert result[1].uuid.to_bytes() == s3.uuid.to_bytes() + # ----------------------------------------------------------------------------- @pytest.mark.asyncio From 8d0969365414a3ba0628736ce7e773d653ccd005 Mon Sep 17 00:00:00 2001 From: Josh Wu Date: Fri, 5 May 2023 17:03:21 +0800 Subject: [PATCH 4/4] Implement GATT server included service declaration --- bumble/gatt.py | 39 +++++++++++++++++++++++++++++++++++++-- bumble/gatt_server.py | 12 +++++++++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/bumble/gatt.py b/bumble/gatt.py index 9721fd4..d99df81 100644 --- a/bumble/gatt.py +++ b/bumble/gatt.py @@ -205,8 +205,16 @@ class Service(Attribute): ''' uuid: UUID + characteristics: List[Characteristic] + included_services: List[Service] - def __init__(self, uuid, characteristics: List[Characteristic], primary=True): + def __init__( + self, + uuid, + characteristics: List[Characteristic], + included_services: List[Service] = [], + primary=True, + ): # Convert the uuid to a UUID object if it isn't already if isinstance(uuid, str): uuid = UUID(uuid) @@ -219,7 +227,7 @@ class Service(Attribute): uuid.to_pdu_bytes(), ) self.uuid = uuid - # self.included_services = [] + self.included_services = included_services[:] self.characteristics = characteristics[:] self.primary = primary @@ -253,6 +261,33 @@ class TemplateService(Service): super().__init__(self.UUID, characteristics, primary) +# ----------------------------------------------------------------------------- +class IncludedServiceDeclaration(Attribute): + ''' + See Vol 3, Part G - 3.2 INCLUDE DEFINITION + ''' + + service: Service + + def __init__(self, service): + declaration_bytes = struct.pack( + '