Skip to content

Commit

Permalink
Merge pull request #186 from zxzxwu/gatt
Browse files Browse the repository at this point in the history
GATT included service declaration & discovery
  • Loading branch information
uael committed May 9, 2023
2 parents 022c235 + 8d09693 commit 4bd8c24
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 8 deletions.
2 changes: 1 addition & 1 deletion bumble/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = []


# -----------------------------------------------------------------------------
Expand Down
39 changes: 37 additions & 2 deletions bumble/gatt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -219,7 +227,7 @@ def __init__(self, uuid, characteristics: list[Characteristic], primary=True):
uuid.to_pdu_bytes(),
)
self.uuid = uuid
# self.included_services = []
self.included_services = included_services[:]
self.characteristics = characteristics[:]
self.primary = primary

Expand Down Expand Up @@ -253,6 +261,33 @@ def __init__(self, characteristics, primary=True):
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(
'<HH2s', service.handle, service.end_group_handle, service.uuid.to_bytes()
)
super().__init__(
GATT_INCLUDE_ATTRIBUTE_TYPE, Attribute.READABLE, declaration_bytes
)
self.service = service

def __str__(self):
return (
f'IncludedServiceDefinition(handle=0x{self.handle:04X}, '
f'group_starting_handle=0x{self.service.handle:04X}, '
f'group_ending_handle=0x{self.service.end_group_handle:04X}, '
f'uuid={self.service.uuid}, '
f'{self.service.properties!s})'
)


# -----------------------------------------------------------------------------
class Characteristic(Attribute):
'''
Expand Down
65 changes: 62 additions & 3 deletions bumble/gatt_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
GATT_REQUEST_TIMEOUT,
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
GATT_INCLUDE_ATTRIBUTE_TYPE,
Characteristic,
ClientCharacteristicConfigurationBits,
)
Expand Down Expand Up @@ -109,6 +110,7 @@ def __str__(self):
class ServiceProxy(AttributeProxy):
uuid: UUID
characteristics: List[CharacteristicProxy]
included_services: List[ServiceProxy]

@staticmethod
def from_client(service_class, client, service_uuid):
Expand Down Expand Up @@ -502,12 +504,69 @@ async def discover_service(self, uuid):

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(
'<HH', attribute_value
)
service_uuid = UUID.from_bytes(attribute_value[4:])
included_service = ServiceProxy(
self, group_starting_handle, group_ending_handle, service_uuid, True
)

included_services.append(included_service)

# Move on to the next included services
starting_handle = response.attributes[-1][0] + 1

service.included_services = included_services
return included_services

async def discover_characteristics(
self, uuids, service: Optional[ServiceProxy]
Expand Down
12 changes: 11 additions & 1 deletion bumble/gatt_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
Characteristic,
CharacteristicDeclaration,
CharacteristicValue,
IncludedServiceDeclaration,
Descriptor,
Service,
)
Expand All @@ -94,6 +95,7 @@ class Server(EventEmitter):
def __init__(self, device):
super().__init__()
self.device = device
self.services = []
self.attributes = [] # Attributes, ordered by increasing handle values
self.attributes_by_handle = {} # Map for fast attribute access by handle
self.max_mtu = (
Expand Down Expand Up @@ -222,7 +224,14 @@ def add_service(self, service: Service):
# Add the service attribute to the DB
self.add_attribute(service)

# TODO: add included services
# Add all included service
for included_service in service.included_services:
# Not registered yet, register the included service first.
if included_service not in self.services:
self.add_service(included_service)
# TODO: Handle circular service reference
include_declaration = IncludedServiceDeclaration(included_service)
self.add_attribute(include_declaration)

# Add all characteristics
for characteristic in service.characteristics:
Expand Down Expand Up @@ -274,6 +283,7 @@ def add_service(self, service: Service):

# Update the service group end
service.end_group_handle = self.attributes[-1].handle
self.services.append(service)

def add_services(self, services):
for service in services:
Expand Down
11 changes: 10 additions & 1 deletion tests/self_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 4bd8c24

Please sign in to comment.