mirror of
https://github.com/google/bumble.git
synced 2026-04-16 00:25:31 +00:00
GATT: Support Multiple Requests
This commit is contained in:
129
bumble/att.py
129
bumble/att.py
@@ -29,7 +29,7 @@ import enum
|
||||
import functools
|
||||
import inspect
|
||||
import struct
|
||||
from collections.abc import Awaitable, Callable
|
||||
from collections.abc import Awaitable, Callable, Sequence
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
ClassVar,
|
||||
@@ -72,34 +72,36 @@ ATT_PSM = 0x001F
|
||||
EATT_PSM = 0x0027
|
||||
|
||||
class Opcode(hci.SpecableEnum):
|
||||
ATT_ERROR_RESPONSE = 0x01
|
||||
ATT_EXCHANGE_MTU_REQUEST = 0x02
|
||||
ATT_EXCHANGE_MTU_RESPONSE = 0x03
|
||||
ATT_FIND_INFORMATION_REQUEST = 0x04
|
||||
ATT_FIND_INFORMATION_RESPONSE = 0x05
|
||||
ATT_FIND_BY_TYPE_VALUE_REQUEST = 0x06
|
||||
ATT_FIND_BY_TYPE_VALUE_RESPONSE = 0x07
|
||||
ATT_READ_BY_TYPE_REQUEST = 0x08
|
||||
ATT_READ_BY_TYPE_RESPONSE = 0x09
|
||||
ATT_READ_REQUEST = 0x0A
|
||||
ATT_READ_RESPONSE = 0x0B
|
||||
ATT_READ_BLOB_REQUEST = 0x0C
|
||||
ATT_READ_BLOB_RESPONSE = 0x0D
|
||||
ATT_READ_MULTIPLE_REQUEST = 0x0E
|
||||
ATT_READ_MULTIPLE_RESPONSE = 0x0F
|
||||
ATT_READ_BY_GROUP_TYPE_REQUEST = 0x10
|
||||
ATT_READ_BY_GROUP_TYPE_RESPONSE = 0x11
|
||||
ATT_WRITE_REQUEST = 0x12
|
||||
ATT_WRITE_RESPONSE = 0x13
|
||||
ATT_WRITE_COMMAND = 0x52
|
||||
ATT_SIGNED_WRITE_COMMAND = 0xD2
|
||||
ATT_PREPARE_WRITE_REQUEST = 0x16
|
||||
ATT_PREPARE_WRITE_RESPONSE = 0x17
|
||||
ATT_EXECUTE_WRITE_REQUEST = 0x18
|
||||
ATT_EXECUTE_WRITE_RESPONSE = 0x19
|
||||
ATT_HANDLE_VALUE_NOTIFICATION = 0x1B
|
||||
ATT_HANDLE_VALUE_INDICATION = 0x1D
|
||||
ATT_HANDLE_VALUE_CONFIRMATION = 0x1E
|
||||
ATT_ERROR_RESPONSE = 0x01
|
||||
ATT_EXCHANGE_MTU_REQUEST = 0x02
|
||||
ATT_EXCHANGE_MTU_RESPONSE = 0x03
|
||||
ATT_FIND_INFORMATION_REQUEST = 0x04
|
||||
ATT_FIND_INFORMATION_RESPONSE = 0x05
|
||||
ATT_FIND_BY_TYPE_VALUE_REQUEST = 0x06
|
||||
ATT_FIND_BY_TYPE_VALUE_RESPONSE = 0x07
|
||||
ATT_READ_BY_TYPE_REQUEST = 0x08
|
||||
ATT_READ_BY_TYPE_RESPONSE = 0x09
|
||||
ATT_READ_REQUEST = 0x0A
|
||||
ATT_READ_RESPONSE = 0x0B
|
||||
ATT_READ_BLOB_REQUEST = 0x0C
|
||||
ATT_READ_BLOB_RESPONSE = 0x0D
|
||||
ATT_READ_MULTIPLE_REQUEST = 0x0E
|
||||
ATT_READ_MULTIPLE_RESPONSE = 0x0F
|
||||
ATT_READ_BY_GROUP_TYPE_REQUEST = 0x10
|
||||
ATT_READ_BY_GROUP_TYPE_RESPONSE = 0x11
|
||||
ATT_READ_MULTIPLE_VARIABLE_REQUEST = 0x20
|
||||
ATT_READ_MULTIPLE_VARIABLE_RESPONSE = 0x21
|
||||
ATT_WRITE_REQUEST = 0x12
|
||||
ATT_WRITE_RESPONSE = 0x13
|
||||
ATT_WRITE_COMMAND = 0x52
|
||||
ATT_SIGNED_WRITE_COMMAND = 0xD2
|
||||
ATT_PREPARE_WRITE_REQUEST = 0x16
|
||||
ATT_PREPARE_WRITE_RESPONSE = 0x17
|
||||
ATT_EXECUTE_WRITE_REQUEST = 0x18
|
||||
ATT_EXECUTE_WRITE_RESPONSE = 0x19
|
||||
ATT_HANDLE_VALUE_NOTIFICATION = 0x1B
|
||||
ATT_HANDLE_VALUE_INDICATION = 0x1D
|
||||
ATT_HANDLE_VALUE_CONFIRMATION = 0x1E
|
||||
|
||||
ATT_REQUESTS = [
|
||||
Opcode.ATT_EXCHANGE_MTU_REQUEST,
|
||||
@@ -110,9 +112,10 @@ ATT_REQUESTS = [
|
||||
Opcode.ATT_READ_BLOB_REQUEST,
|
||||
Opcode.ATT_READ_MULTIPLE_REQUEST,
|
||||
Opcode.ATT_READ_BY_GROUP_TYPE_REQUEST,
|
||||
Opcode.ATT_READ_MULTIPLE_VARIABLE_REQUEST,
|
||||
Opcode.ATT_WRITE_REQUEST,
|
||||
Opcode.ATT_PREPARE_WRITE_REQUEST,
|
||||
Opcode.ATT_EXECUTE_WRITE_REQUEST
|
||||
Opcode.ATT_EXECUTE_WRITE_REQUEST,
|
||||
]
|
||||
|
||||
ATT_RESPONSES = [
|
||||
@@ -125,9 +128,10 @@ ATT_RESPONSES = [
|
||||
Opcode.ATT_READ_BLOB_RESPONSE,
|
||||
Opcode.ATT_READ_MULTIPLE_RESPONSE,
|
||||
Opcode.ATT_READ_BY_GROUP_TYPE_RESPONSE,
|
||||
Opcode.ATT_READ_MULTIPLE_VARIABLE_RESPONSE,
|
||||
Opcode.ATT_WRITE_RESPONSE,
|
||||
Opcode.ATT_PREPARE_WRITE_RESPONSE,
|
||||
Opcode.ATT_EXECUTE_WRITE_RESPONSE
|
||||
Opcode.ATT_EXECUTE_WRITE_RESPONSE,
|
||||
]
|
||||
|
||||
class ErrorCode(hci.SpecableEnum):
|
||||
@@ -185,6 +189,18 @@ ATT_INSUFFICIENT_RESOURCES_ERROR = ErrorCode.INSUFFICIENT_RESOURCES
|
||||
ATT_DEFAULT_MTU = 23
|
||||
|
||||
HANDLE_FIELD_SPEC = {'size': 2, 'mapper': lambda x: f'0x{x:04X}'}
|
||||
_SET_OF_HANDLES_METADATA = hci.metadata({
|
||||
'parser': lambda data, offset: (
|
||||
len(data),
|
||||
[
|
||||
struct.unpack_from('<H', data, i)[0]
|
||||
for i in range(offset, len(data), 2)
|
||||
],
|
||||
),
|
||||
'serializer': lambda handles: b''.join(
|
||||
[struct.pack('<H', handle) for handle in handles]
|
||||
),
|
||||
})
|
||||
|
||||
# fmt: on
|
||||
# pylint: enable=line-too-long
|
||||
@@ -554,7 +570,7 @@ class ATT_Read_Multiple_Request(ATT_PDU):
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.7 Read Multiple Request
|
||||
'''
|
||||
|
||||
set_of_handles: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
set_of_handles: Sequence[int] = dataclasses.field(metadata=_SET_OF_HANDLES_METADATA)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -635,6 +651,55 @@ class ATT_Read_By_Group_Type_Response(ATT_PDU):
|
||||
return result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Read_Multiple_Variable_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.11 Read Multiple Variable Request
|
||||
'''
|
||||
|
||||
set_of_handles: Sequence[int] = dataclasses.field(metadata=_SET_OF_HANDLES_METADATA)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Read_Multiple_Variable_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.12 Read Multiple Variable Response
|
||||
'''
|
||||
|
||||
@classmethod
|
||||
def _parse_length_value_tuples(
|
||||
cls, data: bytes, offset: int
|
||||
) -> tuple[int, list[tuple[int, bytes]]]:
|
||||
length_value_tuple_list: list[tuple[int, bytes]] = []
|
||||
while offset < len(data):
|
||||
length = struct.unpack_from('<H', data, offset)[0]
|
||||
length_value_tuple_list.append(
|
||||
(length, data[offset + 2 : offset + 2 + length])
|
||||
)
|
||||
offset += 2 + length
|
||||
return (len(data), length_value_tuple_list)
|
||||
|
||||
length_value_tuple_list: Sequence[tuple[int, bytes]] = dataclasses.field(
|
||||
metadata=hci.metadata(
|
||||
{
|
||||
'parser': lambda data, offset: ATT_Read_Multiple_Variable_Response._parse_length_value_tuples(
|
||||
data, offset
|
||||
),
|
||||
'serializer': lambda length_value_tuple_list: b''.join(
|
||||
[
|
||||
struct.pack('<H', length) + value
|
||||
for length, value in length_value_tuple_list
|
||||
]
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
|
||||
@@ -977,6 +977,94 @@ class Server(utils.EventEmitter):
|
||||
|
||||
self.send_response(bearer, response)
|
||||
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_read_multiple_request(
|
||||
self, bearer: att.Bearer, request: att.ATT_Read_Multiple_Request
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.7 Read Multiple Request.
|
||||
'''
|
||||
response: att.ATT_PDU
|
||||
|
||||
pdu_space_available = bearer.att_mtu - 1
|
||||
values: list[bytes] = []
|
||||
|
||||
for handle in request.set_of_handles:
|
||||
if not (attribute := self.get_attribute(handle)):
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=handle,
|
||||
error_code=att.ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
)
|
||||
self.send_response(bearer, response)
|
||||
return
|
||||
# No need to catch permission errors here, since these attributes
|
||||
# must all be world-readable
|
||||
attribute_value = await attribute.read_value(bearer)
|
||||
# Check the attribute value size
|
||||
max_attribute_size = min(bearer.att_mtu - 1, 251)
|
||||
if len(attribute_value) > max_attribute_size:
|
||||
# We need to truncate
|
||||
attribute_value = attribute_value[:max_attribute_size]
|
||||
|
||||
# Check if there is enough space
|
||||
entry_size = len(attribute_value)
|
||||
if pdu_space_available < entry_size:
|
||||
break
|
||||
|
||||
# Add the attribute to the list
|
||||
values.append(attribute_value)
|
||||
pdu_space_available -= entry_size
|
||||
|
||||
response = att.ATT_Read_Multiple_Response(set_of_values=b''.join(values))
|
||||
self.send_response(bearer, response)
|
||||
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_read_multiple_variable_request(
|
||||
self, bearer: att.Bearer, request: att.ATT_Read_Multiple_Variable_Request
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.11 Read Multiple Variable Request.
|
||||
'''
|
||||
response: att.ATT_PDU
|
||||
|
||||
pdu_space_available = bearer.att_mtu - 1
|
||||
length_value_tuple_list: list[tuple[int, bytes]] = []
|
||||
|
||||
for handle in request.set_of_handles:
|
||||
if not (attribute := self.get_attribute(handle)):
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=handle,
|
||||
error_code=att.ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
)
|
||||
self.send_response(bearer, response)
|
||||
return
|
||||
# No need to catch permission errors here, since these attributes
|
||||
# must all be world-readable
|
||||
attribute_value = await attribute.read_value(bearer)
|
||||
length = len(attribute_value)
|
||||
# Check the attribute value size
|
||||
max_attribute_size = min(bearer.att_mtu - 3, 251)
|
||||
if len(attribute_value) > max_attribute_size:
|
||||
# We need to truncate
|
||||
attribute_value = attribute_value[:max_attribute_size]
|
||||
|
||||
# Check if there is enough space
|
||||
entry_size = 2 + len(attribute_value)
|
||||
|
||||
# Add the attribute to the list
|
||||
length_value_tuple_list.append((length, attribute_value))
|
||||
pdu_space_available -= entry_size
|
||||
|
||||
if pdu_space_available <= 0:
|
||||
break
|
||||
|
||||
response = att.ATT_Read_Multiple_Variable_Response(
|
||||
length_value_tuple_list=length_value_tuple_list
|
||||
)
|
||||
self.send_response(bearer, response)
|
||||
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_write_request(
|
||||
self, bearer: att.Bearer, request: att.ATT_Write_Request
|
||||
|
||||
@@ -28,7 +28,7 @@ from unittest.mock import ANY, AsyncMock, Mock
|
||||
import pytest
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import gatt_client, l2cap
|
||||
from bumble import att, gatt_client, l2cap
|
||||
from bumble.att import (
|
||||
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
ATT_PDU,
|
||||
@@ -1638,6 +1638,104 @@ async def test_eatt_connection_failure():
|
||||
await gatt_client.Client.connect_eatt(devices.connections[0])
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_multiple() -> None:
|
||||
devices = await TwoDevices.create_with_connection()
|
||||
|
||||
characteristic1 = Characteristic(
|
||||
'0001', Characteristic.Properties.READ, Characteristic.READABLE, b'1234'
|
||||
)
|
||||
|
||||
characteristic2 = Characteristic(
|
||||
'0002',
|
||||
Characteristic.Properties.READ,
|
||||
Characteristic.READABLE,
|
||||
b'5678',
|
||||
)
|
||||
|
||||
service = Service('0000', [characteristic1, characteristic2])
|
||||
devices[1].add_service(service)
|
||||
|
||||
client = devices.connections[0].gatt_client
|
||||
server = devices[1].gatt_server
|
||||
|
||||
await client.discover_services()
|
||||
characteristics = await client.discover_characteristics(
|
||||
[characteristic1.uuid, characteristic2.uuid], None
|
||||
)
|
||||
response = await client.send_request(
|
||||
att.ATT_Read_Multiple_Request(
|
||||
set_of_handles=[c.handle for c in characteristics]
|
||||
)
|
||||
)
|
||||
assert isinstance(response, att.ATT_Read_Multiple_Response)
|
||||
assert response.set_of_values == b'12345678'
|
||||
|
||||
response = await client.send_request(
|
||||
att.ATT_Read_Multiple_Request(
|
||||
set_of_handles=[
|
||||
next(
|
||||
handle
|
||||
for handle in range(0x0001, 0xFFFF)
|
||||
if not server.get_attribute(handle)
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
assert isinstance(response, att.ATT_Error_Response)
|
||||
assert response.error_code == att.ATT_ATTRIBUTE_NOT_FOUND_ERROR
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_multiple_variable() -> None:
|
||||
devices = await TwoDevices.create_with_connection()
|
||||
|
||||
characteristic1 = Characteristic(
|
||||
'0001', Characteristic.Properties.READ, Characteristic.READABLE, b'1234'
|
||||
)
|
||||
|
||||
characteristic2 = Characteristic(
|
||||
'0002',
|
||||
Characteristic.Properties.READ,
|
||||
Characteristic.READABLE,
|
||||
b'99',
|
||||
)
|
||||
|
||||
service = Service('0000', [characteristic1, characteristic2])
|
||||
devices[1].add_service(service)
|
||||
|
||||
client = devices.connections[0].gatt_client
|
||||
server = devices[1].gatt_server
|
||||
|
||||
await client.discover_services()
|
||||
characteristics = await client.discover_characteristics(
|
||||
[characteristic1.uuid, characteristic2.uuid], None
|
||||
)
|
||||
response = await client.send_request(
|
||||
att.ATT_Read_Multiple_Variable_Request(
|
||||
set_of_handles=[c.handle for c in characteristics]
|
||||
)
|
||||
)
|
||||
assert isinstance(response, att.ATT_Read_Multiple_Variable_Response)
|
||||
assert response.length_value_tuple_list == [(4, b'1234'), (2, b'99')]
|
||||
|
||||
response = await client.send_request(
|
||||
att.ATT_Read_Multiple_Variable_Request(
|
||||
set_of_handles=[
|
||||
next(
|
||||
handle
|
||||
for handle in range(0x0001, 0xFFFF)
|
||||
if not server.get_attribute(handle)
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
assert isinstance(response, att.ATT_Error_Response)
|
||||
assert response.error_code == att.ATT_ATTRIBUTE_NOT_FOUND_ERROR
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
|
||||
Reference in New Issue
Block a user