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 functools
|
||||||
import inspect
|
import inspect
|
||||||
import struct
|
import struct
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable, Sequence
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
ClassVar,
|
ClassVar,
|
||||||
@@ -72,34 +72,36 @@ ATT_PSM = 0x001F
|
|||||||
EATT_PSM = 0x0027
|
EATT_PSM = 0x0027
|
||||||
|
|
||||||
class Opcode(hci.SpecableEnum):
|
class Opcode(hci.SpecableEnum):
|
||||||
ATT_ERROR_RESPONSE = 0x01
|
ATT_ERROR_RESPONSE = 0x01
|
||||||
ATT_EXCHANGE_MTU_REQUEST = 0x02
|
ATT_EXCHANGE_MTU_REQUEST = 0x02
|
||||||
ATT_EXCHANGE_MTU_RESPONSE = 0x03
|
ATT_EXCHANGE_MTU_RESPONSE = 0x03
|
||||||
ATT_FIND_INFORMATION_REQUEST = 0x04
|
ATT_FIND_INFORMATION_REQUEST = 0x04
|
||||||
ATT_FIND_INFORMATION_RESPONSE = 0x05
|
ATT_FIND_INFORMATION_RESPONSE = 0x05
|
||||||
ATT_FIND_BY_TYPE_VALUE_REQUEST = 0x06
|
ATT_FIND_BY_TYPE_VALUE_REQUEST = 0x06
|
||||||
ATT_FIND_BY_TYPE_VALUE_RESPONSE = 0x07
|
ATT_FIND_BY_TYPE_VALUE_RESPONSE = 0x07
|
||||||
ATT_READ_BY_TYPE_REQUEST = 0x08
|
ATT_READ_BY_TYPE_REQUEST = 0x08
|
||||||
ATT_READ_BY_TYPE_RESPONSE = 0x09
|
ATT_READ_BY_TYPE_RESPONSE = 0x09
|
||||||
ATT_READ_REQUEST = 0x0A
|
ATT_READ_REQUEST = 0x0A
|
||||||
ATT_READ_RESPONSE = 0x0B
|
ATT_READ_RESPONSE = 0x0B
|
||||||
ATT_READ_BLOB_REQUEST = 0x0C
|
ATT_READ_BLOB_REQUEST = 0x0C
|
||||||
ATT_READ_BLOB_RESPONSE = 0x0D
|
ATT_READ_BLOB_RESPONSE = 0x0D
|
||||||
ATT_READ_MULTIPLE_REQUEST = 0x0E
|
ATT_READ_MULTIPLE_REQUEST = 0x0E
|
||||||
ATT_READ_MULTIPLE_RESPONSE = 0x0F
|
ATT_READ_MULTIPLE_RESPONSE = 0x0F
|
||||||
ATT_READ_BY_GROUP_TYPE_REQUEST = 0x10
|
ATT_READ_BY_GROUP_TYPE_REQUEST = 0x10
|
||||||
ATT_READ_BY_GROUP_TYPE_RESPONSE = 0x11
|
ATT_READ_BY_GROUP_TYPE_RESPONSE = 0x11
|
||||||
ATT_WRITE_REQUEST = 0x12
|
ATT_READ_MULTIPLE_VARIABLE_REQUEST = 0x20
|
||||||
ATT_WRITE_RESPONSE = 0x13
|
ATT_READ_MULTIPLE_VARIABLE_RESPONSE = 0x21
|
||||||
ATT_WRITE_COMMAND = 0x52
|
ATT_WRITE_REQUEST = 0x12
|
||||||
ATT_SIGNED_WRITE_COMMAND = 0xD2
|
ATT_WRITE_RESPONSE = 0x13
|
||||||
ATT_PREPARE_WRITE_REQUEST = 0x16
|
ATT_WRITE_COMMAND = 0x52
|
||||||
ATT_PREPARE_WRITE_RESPONSE = 0x17
|
ATT_SIGNED_WRITE_COMMAND = 0xD2
|
||||||
ATT_EXECUTE_WRITE_REQUEST = 0x18
|
ATT_PREPARE_WRITE_REQUEST = 0x16
|
||||||
ATT_EXECUTE_WRITE_RESPONSE = 0x19
|
ATT_PREPARE_WRITE_RESPONSE = 0x17
|
||||||
ATT_HANDLE_VALUE_NOTIFICATION = 0x1B
|
ATT_EXECUTE_WRITE_REQUEST = 0x18
|
||||||
ATT_HANDLE_VALUE_INDICATION = 0x1D
|
ATT_EXECUTE_WRITE_RESPONSE = 0x19
|
||||||
ATT_HANDLE_VALUE_CONFIRMATION = 0x1E
|
ATT_HANDLE_VALUE_NOTIFICATION = 0x1B
|
||||||
|
ATT_HANDLE_VALUE_INDICATION = 0x1D
|
||||||
|
ATT_HANDLE_VALUE_CONFIRMATION = 0x1E
|
||||||
|
|
||||||
ATT_REQUESTS = [
|
ATT_REQUESTS = [
|
||||||
Opcode.ATT_EXCHANGE_MTU_REQUEST,
|
Opcode.ATT_EXCHANGE_MTU_REQUEST,
|
||||||
@@ -110,9 +112,10 @@ ATT_REQUESTS = [
|
|||||||
Opcode.ATT_READ_BLOB_REQUEST,
|
Opcode.ATT_READ_BLOB_REQUEST,
|
||||||
Opcode.ATT_READ_MULTIPLE_REQUEST,
|
Opcode.ATT_READ_MULTIPLE_REQUEST,
|
||||||
Opcode.ATT_READ_BY_GROUP_TYPE_REQUEST,
|
Opcode.ATT_READ_BY_GROUP_TYPE_REQUEST,
|
||||||
|
Opcode.ATT_READ_MULTIPLE_VARIABLE_REQUEST,
|
||||||
Opcode.ATT_WRITE_REQUEST,
|
Opcode.ATT_WRITE_REQUEST,
|
||||||
Opcode.ATT_PREPARE_WRITE_REQUEST,
|
Opcode.ATT_PREPARE_WRITE_REQUEST,
|
||||||
Opcode.ATT_EXECUTE_WRITE_REQUEST
|
Opcode.ATT_EXECUTE_WRITE_REQUEST,
|
||||||
]
|
]
|
||||||
|
|
||||||
ATT_RESPONSES = [
|
ATT_RESPONSES = [
|
||||||
@@ -125,9 +128,10 @@ ATT_RESPONSES = [
|
|||||||
Opcode.ATT_READ_BLOB_RESPONSE,
|
Opcode.ATT_READ_BLOB_RESPONSE,
|
||||||
Opcode.ATT_READ_MULTIPLE_RESPONSE,
|
Opcode.ATT_READ_MULTIPLE_RESPONSE,
|
||||||
Opcode.ATT_READ_BY_GROUP_TYPE_RESPONSE,
|
Opcode.ATT_READ_BY_GROUP_TYPE_RESPONSE,
|
||||||
|
Opcode.ATT_READ_MULTIPLE_VARIABLE_RESPONSE,
|
||||||
Opcode.ATT_WRITE_RESPONSE,
|
Opcode.ATT_WRITE_RESPONSE,
|
||||||
Opcode.ATT_PREPARE_WRITE_RESPONSE,
|
Opcode.ATT_PREPARE_WRITE_RESPONSE,
|
||||||
Opcode.ATT_EXECUTE_WRITE_RESPONSE
|
Opcode.ATT_EXECUTE_WRITE_RESPONSE,
|
||||||
]
|
]
|
||||||
|
|
||||||
class ErrorCode(hci.SpecableEnum):
|
class ErrorCode(hci.SpecableEnum):
|
||||||
@@ -185,6 +189,18 @@ ATT_INSUFFICIENT_RESOURCES_ERROR = ErrorCode.INSUFFICIENT_RESOURCES
|
|||||||
ATT_DEFAULT_MTU = 23
|
ATT_DEFAULT_MTU = 23
|
||||||
|
|
||||||
HANDLE_FIELD_SPEC = {'size': 2, 'mapper': lambda x: f'0x{x:04X}'}
|
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
|
# fmt: on
|
||||||
# pylint: enable=line-too-long
|
# 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
|
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
|
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
|
@ATT_PDU.subclass
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
|
|||||||
@@ -977,6 +977,94 @@ class Server(utils.EventEmitter):
|
|||||||
|
|
||||||
self.send_response(bearer, response)
|
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()
|
@utils.AsyncRunner.run_in_task()
|
||||||
async def on_att_write_request(
|
async def on_att_write_request(
|
||||||
self, bearer: att.Bearer, request: att.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
|
import pytest
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
from bumble import gatt_client, l2cap
|
from bumble import att, gatt_client, l2cap
|
||||||
from bumble.att import (
|
from bumble.att import (
|
||||||
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||||
ATT_PDU,
|
ATT_PDU,
|
||||||
@@ -1638,6 +1638,104 @@ async def test_eatt_connection_failure():
|
|||||||
await gatt_client.Client.connect_eatt(devices.connections[0])
|
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__':
|
if __name__ == '__main__':
|
||||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||||
|
|||||||
Reference in New Issue
Block a user