mirror of
https://github.com/google/bumble.git
synced 2026-04-17 00:35:31 +00:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c619f1f21b | ||
|
|
d4b0da9265 | ||
|
|
f1058e4d4e | ||
|
|
454d477d7e | ||
|
|
6966228d74 | ||
|
|
f4271a5646 | ||
|
|
534209f0af | ||
|
|
549b82999a | ||
|
|
551f577b2a | ||
|
|
84a6453dda | ||
|
|
591ed61686 | ||
|
|
3d3acbb374 | ||
|
|
671f306a27 | ||
|
|
f7364db992 | ||
|
|
0fb2b3bd66 | ||
|
|
9e270d4d62 | ||
|
|
cf60b5ffbb | ||
|
|
aa4c57d105 | ||
|
|
61a601e6e2 | ||
|
|
05fd4fbfc6 | ||
|
|
2cad743f8c | ||
|
|
6aa9e0bdf7 | ||
|
|
255414f315 | ||
|
|
d2df76f6f4 | ||
|
|
884b1c20e4 | ||
|
|
91a2b4f676 | ||
|
|
5831f79d62 | ||
|
|
36f81b798c | ||
|
|
985183001f | ||
|
|
b153d0fcde | ||
|
|
30d912d66e | ||
|
|
054dc70f3f |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -17,3 +17,6 @@ venv/
|
||||
.venv/
|
||||
# snoop logs
|
||||
out/
|
||||
# macOS
|
||||
.DS_Store
|
||||
._*
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
import usb1
|
||||
|
||||
@@ -166,13 +168,16 @@ def is_bluetooth_hci(device):
|
||||
# -----------------------------------------------------------------------------
|
||||
@click.command()
|
||||
@click.option('--verbose', is_flag=True, default=False, help='Print more details')
|
||||
def main(verbose):
|
||||
@click.option('--hci-only', is_flag=True, default=False, help='only show HCI device')
|
||||
@click.option('--manufacturer', help='filter by manufacturer')
|
||||
@click.option('--product', help='filter by product')
|
||||
def main(verbose: bool, manufacturer: str, product: str, hci_only: bool):
|
||||
bumble.logging.setup_basic_logging('WARNING')
|
||||
|
||||
load_libusb()
|
||||
with usb1.USBContext() as context:
|
||||
bluetooth_device_count = 0
|
||||
devices = {}
|
||||
devices: dict[tuple[Any, Any], list[str | None]] = {}
|
||||
|
||||
for device in context.getDeviceIterator(skip_on_error=True):
|
||||
device_class = device.getDeviceClass()
|
||||
@@ -234,6 +239,14 @@ def main(verbose):
|
||||
f'{basic_transport_name}/{device_serial_number}'
|
||||
)
|
||||
|
||||
# Filter
|
||||
if product and device_product != product:
|
||||
continue
|
||||
if manufacturer and device_manufacturer != manufacturer:
|
||||
continue
|
||||
if not is_bluetooth_hci(device) and hci_only:
|
||||
continue
|
||||
|
||||
# Print the results
|
||||
print(
|
||||
color(
|
||||
|
||||
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
|
||||
|
||||
@@ -235,7 +235,7 @@ class Protocol:
|
||||
)
|
||||
+ payload
|
||||
)
|
||||
self.l2cap_channel.send_pdu(pdu)
|
||||
self.l2cap_channel.write(pdu)
|
||||
|
||||
def send_command(self, transaction_label: int, pid: int, payload: bytes) -> None:
|
||||
logger.debug(
|
||||
|
||||
@@ -268,7 +268,7 @@ class MediaPacketPump:
|
||||
await self.clock.sleep(delay)
|
||||
|
||||
# Emit
|
||||
rtp_channel.send_pdu(bytes(packet))
|
||||
rtp_channel.write(bytes(packet))
|
||||
logger.debug(
|
||||
f'{color(">>> sending RTP packet:", "green")} {packet}'
|
||||
)
|
||||
@@ -1519,7 +1519,7 @@ class Protocol(utils.EventEmitter):
|
||||
header = bytes([first_header_byte])
|
||||
|
||||
# Send one packet
|
||||
self.l2cap_channel.send_pdu(header + payload[:max_fragment_size])
|
||||
self.l2cap_channel.write(header + payload[:max_fragment_size])
|
||||
|
||||
# Prepare for the next packet
|
||||
payload = payload[max_fragment_size:]
|
||||
@@ -1829,7 +1829,7 @@ class Stream:
|
||||
|
||||
def send_media_packet(self, packet: MediaPacket) -> None:
|
||||
assert self.rtp_channel
|
||||
self.rtp_channel.send_pdu(bytes(packet))
|
||||
self.rtp_channel.write(bytes(packet))
|
||||
|
||||
async def configure(self) -> None:
|
||||
if self.state != State.IDLE:
|
||||
|
||||
@@ -1383,10 +1383,7 @@ class Peer:
|
||||
def create_service_proxy(
|
||||
self, proxy_class: type[_PROXY_CLASS]
|
||||
) -> _PROXY_CLASS | None:
|
||||
if proxy := proxy_class.from_client(self.gatt_client):
|
||||
return cast(_PROXY_CLASS, proxy)
|
||||
|
||||
return None
|
||||
return proxy_class.from_client(self.gatt_client)
|
||||
|
||||
async def discover_service_and_create_proxy(
|
||||
self, proxy_class: type[_PROXY_CLASS]
|
||||
@@ -1406,7 +1403,7 @@ class Peer:
|
||||
async def request_name(self) -> str:
|
||||
return await self.connection.request_remote_name()
|
||||
|
||||
async def __aenter__(self):
|
||||
async def __aenter__(self) -> Self:
|
||||
await self.discover_services()
|
||||
for service in self.services:
|
||||
await service.discover_characteristics()
|
||||
|
||||
@@ -77,6 +77,7 @@ class RtlProjectId(enum.IntEnum):
|
||||
PROJECT_ID_8852A = 18
|
||||
PROJECT_ID_8852B = 20
|
||||
PROJECT_ID_8852C = 25
|
||||
PROJECT_ID_8761C = 51
|
||||
|
||||
|
||||
RTK_PROJECT_ID_TO_ROM = {
|
||||
@@ -92,6 +93,7 @@ RTK_PROJECT_ID_TO_ROM = {
|
||||
18: RTK_ROM_LMP_8852A,
|
||||
20: RTK_ROM_LMP_8852A,
|
||||
25: RTK_ROM_LMP_8852A,
|
||||
51: RTK_ROM_LMP_8761A,
|
||||
}
|
||||
|
||||
# List of USB (VendorID, ProductID) for Realtek-based devices.
|
||||
@@ -123,6 +125,10 @@ RTK_USB_PRODUCTS = {
|
||||
(0x2550, 0x8761),
|
||||
(0x2B89, 0x8761),
|
||||
(0x7392, 0xC611),
|
||||
# Realtek 8761CUV
|
||||
(0x0B05, 0x1BF6),
|
||||
(0x0BDA, 0xC761),
|
||||
(0x7392, 0xF611),
|
||||
# Realtek 8821AE
|
||||
(0x0B05, 0x17DC),
|
||||
(0x13D3, 0x3414),
|
||||
@@ -363,6 +369,15 @@ class Driver(common.Driver):
|
||||
fw_name="rtl8761bu_fw.bin",
|
||||
config_name="rtl8761bu_config.bin",
|
||||
),
|
||||
# 8761CU
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8761A,
|
||||
hci=(0x0E, 0x00),
|
||||
config_needed=False,
|
||||
has_rom_version=True,
|
||||
fw_name="rtl8761cu_fw.bin",
|
||||
config_name="rtl8761cu_config.bin",
|
||||
),
|
||||
# 8822C
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8822B,
|
||||
@@ -420,9 +435,17 @@ class Driver(common.Driver):
|
||||
@staticmethod
|
||||
def find_driver_info(hci_version, hci_subversion, lmp_subversion):
|
||||
for driver_info in Driver.DRIVER_INFOS:
|
||||
if driver_info.rom == lmp_subversion and driver_info.hci == (
|
||||
hci_subversion,
|
||||
hci_version,
|
||||
if driver_info.rom == lmp_subversion and (
|
||||
driver_info.hci
|
||||
== (
|
||||
hci_subversion,
|
||||
hci_version,
|
||||
)
|
||||
or driver_info.hci
|
||||
== (
|
||||
hci_subversion,
|
||||
0x0,
|
||||
)
|
||||
):
|
||||
return driver_info
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import functools
|
||||
import logging
|
||||
import struct
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import TypeVar
|
||||
from typing import ClassVar, TypeVar
|
||||
|
||||
from bumble.att import Attribute, AttributeValue, AttributeValueV2
|
||||
from bumble.colors import color
|
||||
@@ -403,7 +403,7 @@ class TemplateService(Service):
|
||||
to expose their UUID as a class property
|
||||
'''
|
||||
|
||||
UUID: UUID
|
||||
UUID: ClassVar[UUID]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -34,11 +34,14 @@ from datetime import datetime
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
ClassVar,
|
||||
Generic,
|
||||
TypeVar,
|
||||
overload,
|
||||
)
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import att, core, l2cap, utils
|
||||
from bumble.colors import color
|
||||
from bumble.core import UUID, InvalidStateError
|
||||
@@ -249,10 +252,10 @@ class ProfileServiceProxy:
|
||||
Base class for profile-specific service proxies
|
||||
'''
|
||||
|
||||
SERVICE_CLASS: type[TemplateService]
|
||||
SERVICE_CLASS: ClassVar[type[TemplateService]]
|
||||
|
||||
@classmethod
|
||||
def from_client(cls, client: Client) -> ProfileServiceProxy | None:
|
||||
def from_client(cls, client: Client) -> Self | None:
|
||||
return ServiceProxy.from_client(cls, client, cls.SERVICE_CLASS.UUID)
|
||||
|
||||
|
||||
@@ -285,8 +288,6 @@ class Client:
|
||||
self._bearer_id = (
|
||||
f'[0x{bearer.connection.handle:04X}|CID=0x{bearer.source_cid:04X}]'
|
||||
)
|
||||
# Fill the mtu.
|
||||
bearer.on_att_mtu_update(att.ATT_DEFAULT_MTU)
|
||||
self.connection = bearer.connection
|
||||
else:
|
||||
bearer.on(bearer.EVENT_DISCONNECTION, self.on_disconnection)
|
||||
|
||||
@@ -115,7 +115,6 @@ class Server(utils.EventEmitter):
|
||||
channel.connection.handle,
|
||||
channel.source_cid,
|
||||
)
|
||||
channel.att_mtu = att.ATT_DEFAULT_MTU
|
||||
channel.sink = lambda pdu: self.on_gatt_pdu(
|
||||
channel, att.ATT_PDU.from_bytes(pdu)
|
||||
)
|
||||
@@ -777,6 +776,18 @@ class Server(utils.EventEmitter):
|
||||
error_code=att.ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
)
|
||||
|
||||
if (
|
||||
request.starting_handle == 0x0000
|
||||
or request.starting_handle > request.ending_handle
|
||||
):
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.starting_handle,
|
||||
error_code=att.ATT_INVALID_HANDLE_ERROR,
|
||||
)
|
||||
self.send_response(bearer, response)
|
||||
return
|
||||
|
||||
attributes: list[tuple[int, bytes]] = []
|
||||
for attribute in (
|
||||
attribute
|
||||
@@ -977,6 +988,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
|
||||
|
||||
101
bumble/hci.py
101
bumble/hci.py
@@ -2090,9 +2090,9 @@ class Address:
|
||||
RANDOM_IDENTITY_ADDRESS = AddressType.RANDOM_IDENTITY
|
||||
|
||||
# Type declarations
|
||||
NIL: Address
|
||||
ANY: Address
|
||||
ANY_RANDOM: Address
|
||||
NIL: ClassVar[Address]
|
||||
ANY: ClassVar[Address]
|
||||
ANY_RANDOM: ClassVar[Address]
|
||||
|
||||
# pylint: disable-next=unnecessary-lambda
|
||||
ADDRESS_TYPE_SPEC = {'size': 1, 'mapper': lambda x: Address.address_type_name(x)}
|
||||
@@ -2204,38 +2204,38 @@ class Address:
|
||||
|
||||
self.address_type = address_type
|
||||
|
||||
def clone(self):
|
||||
def clone(self) -> Address:
|
||||
return Address(self.address_bytes, self.address_type)
|
||||
|
||||
@property
|
||||
def is_public(self):
|
||||
def is_public(self) -> bool:
|
||||
return self.address_type in (
|
||||
self.PUBLIC_DEVICE_ADDRESS,
|
||||
self.PUBLIC_IDENTITY_ADDRESS,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_random(self):
|
||||
def is_random(self) -> bool:
|
||||
return not self.is_public
|
||||
|
||||
@property
|
||||
def is_resolved(self):
|
||||
def is_resolved(self) -> bool:
|
||||
return self.address_type in (
|
||||
self.PUBLIC_IDENTITY_ADDRESS,
|
||||
self.RANDOM_IDENTITY_ADDRESS,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_resolvable(self):
|
||||
def is_resolvable(self) -> bool:
|
||||
return self.address_type == self.RANDOM_DEVICE_ADDRESS and (
|
||||
self.address_bytes[5] >> 6 == 1
|
||||
)
|
||||
|
||||
@property
|
||||
def is_static(self):
|
||||
def is_static(self) -> bool:
|
||||
return self.is_random and (self.address_bytes[5] >> 6 == 3)
|
||||
|
||||
def to_string(self, with_type_qualifier=True):
|
||||
def to_string(self, with_type_qualifier: bool = True) -> str:
|
||||
'''
|
||||
String representation of the address, MSB first, with an optional type
|
||||
qualifier.
|
||||
@@ -2245,23 +2245,23 @@ class Address:
|
||||
return result
|
||||
return result + '/P'
|
||||
|
||||
def __bytes__(self):
|
||||
def __bytes__(self) -> bytes:
|
||||
return self.address_bytes
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.address_bytes)
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
isinstance(other, Address)
|
||||
and self.address_bytes == other.address_bytes
|
||||
and self.is_public == other.is_public
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.to_string()
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f'Address({self.to_string(False)}/{self.address_type_name(self.address_type)})'
|
||||
|
||||
|
||||
@@ -2300,10 +2300,10 @@ class HCI_Packet:
|
||||
Abstract Base class for HCI packets
|
||||
'''
|
||||
|
||||
hci_packet_type: ClassVar[int]
|
||||
hci_packet_type: int
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(packet: bytes) -> HCI_Packet:
|
||||
@classmethod
|
||||
def from_bytes(cls, packet: bytes) -> HCI_Packet:
|
||||
packet_type = packet[0]
|
||||
|
||||
if packet_type == HCI_COMMAND_PACKET:
|
||||
@@ -2323,7 +2323,7 @@ class HCI_Packet:
|
||||
|
||||
return HCI_CustomPacket(packet)
|
||||
|
||||
def __init__(self, name):
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
@@ -2335,7 +2335,7 @@ class HCI_Packet:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class HCI_CustomPacket(HCI_Packet):
|
||||
def __init__(self, payload):
|
||||
def __init__(self, payload: bytes) -> None:
|
||||
super().__init__('HCI_CUSTOM_PACKET')
|
||||
self.hci_packet_type = payload[0]
|
||||
self.payload = payload
|
||||
@@ -7452,6 +7452,7 @@ class HCI_Vendor_Event(HCI_Event):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclasses.dataclass
|
||||
class HCI_AclDataPacket(HCI_Packet):
|
||||
'''
|
||||
See Bluetooth spec @ 5.4.2 HCI ACL Data Packets
|
||||
@@ -7459,8 +7460,14 @@ class HCI_AclDataPacket(HCI_Packet):
|
||||
|
||||
hci_packet_type = HCI_ACL_DATA_PACKET
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(packet: bytes) -> HCI_AclDataPacket:
|
||||
connection_handle: int
|
||||
pb_flag: int
|
||||
bc_flag: int
|
||||
data_total_length: int
|
||||
data: bytes
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, packet: bytes) -> HCI_AclDataPacket:
|
||||
# Read the header
|
||||
h, data_total_length = struct.unpack_from('<HH', packet, 1)
|
||||
connection_handle = h & 0xFFF
|
||||
@@ -7469,25 +7476,22 @@ class HCI_AclDataPacket(HCI_Packet):
|
||||
data = packet[5:]
|
||||
if len(data) != data_total_length:
|
||||
raise InvalidPacketError('invalid packet length')
|
||||
return HCI_AclDataPacket(
|
||||
connection_handle, pb_flag, bc_flag, data_total_length, data
|
||||
return cls(
|
||||
connection_handle=connection_handle,
|
||||
pb_flag=pb_flag,
|
||||
bc_flag=bc_flag,
|
||||
data_total_length=data_total_length,
|
||||
data=data,
|
||||
)
|
||||
|
||||
def __bytes__(self):
|
||||
def __bytes__(self) -> bytes:
|
||||
h = (self.pb_flag << 12) | (self.bc_flag << 14) | self.connection_handle
|
||||
return (
|
||||
struct.pack('<BHH', HCI_ACL_DATA_PACKET, h, self.data_total_length)
|
||||
+ self.data
|
||||
)
|
||||
|
||||
def __init__(self, connection_handle, pb_flag, bc_flag, data_total_length, data):
|
||||
self.connection_handle = connection_handle
|
||||
self.pb_flag = pb_flag
|
||||
self.bc_flag = bc_flag
|
||||
self.data_total_length = data_total_length
|
||||
self.data = data
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f'{color("ACL", "blue")}: '
|
||||
f'handle=0x{self.connection_handle:04x}, '
|
||||
@@ -7498,6 +7502,7 @@ class HCI_AclDataPacket(HCI_Packet):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclasses.dataclass
|
||||
class HCI_SynchronousDataPacket(HCI_Packet):
|
||||
'''
|
||||
See Bluetooth spec @ 5.4.3 HCI SCO Data Packets
|
||||
@@ -7505,8 +7510,13 @@ class HCI_SynchronousDataPacket(HCI_Packet):
|
||||
|
||||
hci_packet_type = HCI_SYNCHRONOUS_DATA_PACKET
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(packet: bytes) -> HCI_SynchronousDataPacket:
|
||||
connection_handle: int
|
||||
packet_status: int
|
||||
data_total_length: int
|
||||
data: bytes
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, packet: bytes) -> HCI_SynchronousDataPacket:
|
||||
# Read the header
|
||||
h, data_total_length = struct.unpack_from('<HB', packet, 1)
|
||||
connection_handle = h & 0xFFF
|
||||
@@ -7516,8 +7526,11 @@ class HCI_SynchronousDataPacket(HCI_Packet):
|
||||
raise InvalidPacketError(
|
||||
f'invalid packet length {len(data)} != {data_total_length}'
|
||||
)
|
||||
return HCI_SynchronousDataPacket(
|
||||
connection_handle, packet_status, data_total_length, data
|
||||
return cls(
|
||||
connection_handle=connection_handle,
|
||||
packet_status=packet_status,
|
||||
data_total_length=data_total_length,
|
||||
data=data,
|
||||
)
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
@@ -7527,18 +7540,6 @@ class HCI_SynchronousDataPacket(HCI_Packet):
|
||||
+ self.data
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
connection_handle: int,
|
||||
packet_status: int,
|
||||
data_total_length: int,
|
||||
data: bytes,
|
||||
) -> None:
|
||||
self.connection_handle = connection_handle
|
||||
self.packet_status = packet_status
|
||||
self.data_total_length = data_total_length
|
||||
self.data = data
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f'{color("SCO", "blue")}: '
|
||||
@@ -7556,7 +7557,7 @@ class HCI_IsoDataPacket(HCI_Packet):
|
||||
See Bluetooth spec @ 5.4.5 HCI ISO Data Packets
|
||||
'''
|
||||
|
||||
hci_packet_type: ClassVar[int] = HCI_ISO_DATA_PACKET
|
||||
hci_packet_type = HCI_ISO_DATA_PACKET
|
||||
|
||||
connection_handle: int
|
||||
data_total_length: int
|
||||
|
||||
@@ -312,11 +312,11 @@ class HID(ABC, utils.EventEmitter):
|
||||
|
||||
def send_pdu_on_ctrl(self, msg: bytes) -> None:
|
||||
assert self.l2cap_ctrl_channel
|
||||
self.l2cap_ctrl_channel.send_pdu(msg)
|
||||
self.l2cap_ctrl_channel.write(msg)
|
||||
|
||||
def send_pdu_on_intr(self, msg: bytes) -> None:
|
||||
assert self.l2cap_intr_channel
|
||||
self.l2cap_intr_channel.send_pdu(msg)
|
||||
self.l2cap_intr_channel.write(msg)
|
||||
|
||||
def send_data(self, data: bytes) -> None:
|
||||
if self.role == HID.Role.HOST:
|
||||
|
||||
@@ -732,6 +732,16 @@ class Host(utils.EventEmitter):
|
||||
)
|
||||
packet_queue.enqueue(acl_packet, connection_handle)
|
||||
|
||||
def send_sco_sdu(self, connection_handle: int, sdu: bytes) -> None:
|
||||
self.send_hci_packet(
|
||||
hci.HCI_SynchronousDataPacket(
|
||||
connection_handle=connection_handle,
|
||||
packet_status=0,
|
||||
data_total_length=len(sdu),
|
||||
data=sdu,
|
||||
)
|
||||
)
|
||||
|
||||
def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None:
|
||||
self.send_acl_sdu(connection_handle, bytes(L2CAP_PDU(cid, pdu)))
|
||||
|
||||
|
||||
183
bumble/l2cap.py
183
bumble/l2cap.py
@@ -20,6 +20,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import enum
|
||||
import itertools
|
||||
import logging
|
||||
import struct
|
||||
from collections import deque
|
||||
@@ -302,11 +303,9 @@ class EnhancedControlField(ControlField):
|
||||
|
||||
@dataclasses.dataclass
|
||||
class InformationEnhancedControlField(EnhancedControlField):
|
||||
tx_seq: int = 0
|
||||
tx_seq: int
|
||||
sar: int
|
||||
req_seq: int = 0
|
||||
segmentation_and_reassembly: int = (
|
||||
EnhancedControlField.SegmentationAndReassembly.UNSEGMENTED
|
||||
)
|
||||
final: int = 1
|
||||
|
||||
frame_type = EnhancedControlField.FieldType.I_FRAME
|
||||
@@ -316,15 +315,15 @@ class InformationEnhancedControlField(EnhancedControlField):
|
||||
return cls(
|
||||
tx_seq=(data[0] >> 1) & 0b0111111,
|
||||
final=(data[0] >> 7) & 0b1,
|
||||
req_seq=(data[1] & 0b001111111),
|
||||
segmentation_and_reassembly=(data[1] >> 6) & 0b11,
|
||||
req_seq=(data[1] & 0b00111111),
|
||||
sar=(data[1] >> 6) & 0b11,
|
||||
)
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return bytes(
|
||||
[
|
||||
self.frame_type | (self.tx_seq << 1) | (self.final << 7),
|
||||
self.req_seq | (self.segmentation_and_reassembly << 6),
|
||||
self.req_seq | (self.sar << 6),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -889,27 +888,38 @@ class EnhancedRetransmissionProcessor(Processor):
|
||||
class _PendingPdu:
|
||||
payload: bytes
|
||||
tx_seq: int
|
||||
sar: InformationEnhancedControlField.SegmentationAndReassembly
|
||||
sdu_length: int = 0
|
||||
req_seq: int = 0
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return (
|
||||
bytes(
|
||||
InformationEnhancedControlField(
|
||||
tx_seq=self.tx_seq, req_seq=self.req_seq
|
||||
tx_seq=self.tx_seq,
|
||||
req_seq=self.req_seq,
|
||||
sar=self.sar,
|
||||
)
|
||||
)
|
||||
+ (
|
||||
struct.pack('<H', self.sdu_length)
|
||||
if self.sar
|
||||
== InformationEnhancedControlField.SegmentationAndReassembly.START
|
||||
else b''
|
||||
)
|
||||
+ self.payload
|
||||
)
|
||||
|
||||
_expected_ack_seq: int = 0
|
||||
_last_acked_tx_seq: int = 0
|
||||
_last_acked_rx_seq: int = 0
|
||||
_next_tx_seq: int = 0
|
||||
_last_tx_seq: int = 0
|
||||
_req_seq_num: int = 0
|
||||
_next_seq_num: int = 0
|
||||
_remote_is_busy: bool = False
|
||||
_in_sdu: bytes = b''
|
||||
|
||||
_num_receiver_ready_polls_sent: int = 0
|
||||
_pending_pdus: list[_PendingPdu]
|
||||
_tx_window: list[_PendingPdu]
|
||||
_monitor_handle: asyncio.TimerHandle | None = None
|
||||
_receiver_ready_poll_handle: asyncio.TimerHandle | None = None
|
||||
|
||||
@@ -917,12 +927,6 @@ class EnhancedRetransmissionProcessor(Processor):
|
||||
monitor_timeout: float
|
||||
retransmission_timeout: float
|
||||
|
||||
@classmethod
|
||||
def _num_frames_between(cls, low: int, high: int) -> int:
|
||||
if high < low:
|
||||
high += cls.MAX_SEQ_NUM
|
||||
return high - low
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
channel: ClassicChannel,
|
||||
@@ -935,6 +939,7 @@ class EnhancedRetransmissionProcessor(Processor):
|
||||
self.peer_mps = peer_mps
|
||||
self.peer_tx_window_size = peer_tx_window_size
|
||||
self._pending_pdus = []
|
||||
self._tx_window = []
|
||||
self.monitor_timeout = spec.monitor_timeout
|
||||
self.channel = channel
|
||||
self.retransmission_timeout = spec.retransmission_timeout
|
||||
@@ -972,12 +977,9 @@ class EnhancedRetransmissionProcessor(Processor):
|
||||
|
||||
def _send_receiver_ready_poll(self) -> None:
|
||||
self._num_receiver_ready_polls_sent += 1
|
||||
self.channel.send_pdu(
|
||||
SupervisoryEnhancedControlField(
|
||||
supervision_function=SupervisoryEnhancedControlField.SupervisoryFunction.RR,
|
||||
final=1,
|
||||
req_seq=self._next_seq_num,
|
||||
)
|
||||
self._send_s_frame(
|
||||
supervision_function=SupervisoryEnhancedControlField.SupervisoryFunction.RR,
|
||||
final=1,
|
||||
)
|
||||
|
||||
def _get_next_tx_seq(self) -> int:
|
||||
@@ -987,12 +989,35 @@ class EnhancedRetransmissionProcessor(Processor):
|
||||
|
||||
@override
|
||||
def send_sdu(self, sdu: bytes) -> None:
|
||||
if len(sdu) > self.peer_mps:
|
||||
raise InvalidArgumentError(
|
||||
f'SDU size({len(sdu)}) exceeds channel MPS {self.peer_mps}'
|
||||
if len(sdu) <= self.peer_mps:
|
||||
pdu = self._PendingPdu(
|
||||
payload=sdu,
|
||||
tx_seq=self._get_next_tx_seq(),
|
||||
req_seq=self._req_seq_num,
|
||||
sar=InformationEnhancedControlField.SegmentationAndReassembly.UNSEGMENTED,
|
||||
)
|
||||
pdu = self._PendingPdu(payload=sdu, tx_seq=self._get_next_tx_seq())
|
||||
self._pending_pdus.append(pdu)
|
||||
self._pending_pdus.append(pdu)
|
||||
else:
|
||||
for offset in range(0, len(sdu), self.peer_mps):
|
||||
payload = sdu[offset : offset + self.peer_mps]
|
||||
if offset == 0:
|
||||
sar = (
|
||||
InformationEnhancedControlField.SegmentationAndReassembly.START
|
||||
)
|
||||
elif offset + len(payload) >= len(sdu):
|
||||
sar = InformationEnhancedControlField.SegmentationAndReassembly.END
|
||||
else:
|
||||
sar = (
|
||||
InformationEnhancedControlField.SegmentationAndReassembly.CONTINUATION
|
||||
)
|
||||
pdu = self._PendingPdu(
|
||||
payload=payload,
|
||||
tx_seq=self._get_next_tx_seq(),
|
||||
req_seq=self._req_seq_num,
|
||||
sar=sar,
|
||||
sdu_length=len(sdu),
|
||||
)
|
||||
self._pending_pdus.append(pdu)
|
||||
self._process_output()
|
||||
|
||||
@override
|
||||
@@ -1000,17 +1025,37 @@ class EnhancedRetransmissionProcessor(Processor):
|
||||
control_field = EnhancedControlField.from_bytes(pdu)
|
||||
self._update_ack_seq(control_field.req_seq, control_field.final != 0)
|
||||
if isinstance(control_field, InformationEnhancedControlField):
|
||||
if control_field.tx_seq != self._next_seq_num:
|
||||
if control_field.tx_seq != self._req_seq_num:
|
||||
logger.error(
|
||||
"tx_seq != self._req_seq_num, tx_seq: %d, self._req_seq_num: %d",
|
||||
control_field.tx_seq,
|
||||
self._req_seq_num,
|
||||
)
|
||||
return
|
||||
self._next_seq_num = (self._next_seq_num + 1) % self.MAX_SEQ_NUM
|
||||
self._req_seq_num = self._next_seq_num
|
||||
self._req_seq_num = (control_field.tx_seq + 1) % self.MAX_SEQ_NUM
|
||||
|
||||
ack_frame = SupervisoryEnhancedControlField(
|
||||
supervision_function=SupervisoryEnhancedControlField.SupervisoryFunction.RR,
|
||||
req_seq=self._next_seq_num,
|
||||
)
|
||||
self.channel.send_pdu(ack_frame)
|
||||
self.channel.on_sdu(pdu[2:])
|
||||
if (
|
||||
control_field.sar
|
||||
== InformationEnhancedControlField.SegmentationAndReassembly.START
|
||||
):
|
||||
# Drop Control Field(2) + SDU Length(2)
|
||||
self._in_sdu += pdu[4:]
|
||||
else:
|
||||
# Drop Control Field(2)
|
||||
self._in_sdu += pdu[2:]
|
||||
if control_field.sar in (
|
||||
InformationEnhancedControlField.SegmentationAndReassembly.END,
|
||||
InformationEnhancedControlField.SegmentationAndReassembly.UNSEGMENTED,
|
||||
):
|
||||
self.channel.on_sdu(self._in_sdu)
|
||||
self._in_sdu = b''
|
||||
|
||||
# If sink doesn't trigger any I-frame, ack this frame.
|
||||
if self._req_seq_num != self._last_acked_rx_seq:
|
||||
self._send_s_frame(
|
||||
supervision_function=SupervisoryEnhancedControlField.SupervisoryFunction.RR,
|
||||
final=0,
|
||||
)
|
||||
elif isinstance(control_field, SupervisoryEnhancedControlField):
|
||||
self._remote_is_busy = (
|
||||
control_field.supervision_function
|
||||
@@ -1022,56 +1067,66 @@ class EnhancedRetransmissionProcessor(Processor):
|
||||
SupervisoryEnhancedControlField.SupervisoryFunction.RNR,
|
||||
):
|
||||
if control_field.poll:
|
||||
self.channel.send_pdu(
|
||||
SupervisoryEnhancedControlField(
|
||||
supervision_function=SupervisoryEnhancedControlField.SupervisoryFunction.RR,
|
||||
final=1,
|
||||
req_seq=self._next_seq_num,
|
||||
)
|
||||
self._send_s_frame(
|
||||
supervision_function=SupervisoryEnhancedControlField.SupervisoryFunction.RR,
|
||||
final=1,
|
||||
)
|
||||
else:
|
||||
# TODO: Handle Retransmission.
|
||||
pass
|
||||
|
||||
def _process_output(self) -> None:
|
||||
if self._remote_is_busy or self._monitor_handle:
|
||||
if self._remote_is_busy:
|
||||
logger.debug("Remote is busy")
|
||||
return
|
||||
if self._monitor_handle:
|
||||
logger.debug("Monitor handle is not None")
|
||||
return
|
||||
|
||||
for pdu in self._pending_pdus:
|
||||
if self._num_unacked_frames >= self.peer_tx_window_size:
|
||||
return
|
||||
self._send_pdu(pdu)
|
||||
self._last_tx_seq = pdu.tx_seq
|
||||
pdu_to_send = self.peer_tx_window_size - len(self._tx_window)
|
||||
for pdu in itertools.islice(self._pending_pdus, pdu_to_send):
|
||||
self._send_i_frame(pdu)
|
||||
self._pending_pdus = self._pending_pdus[pdu_to_send:]
|
||||
|
||||
@property
|
||||
def _num_unacked_frames(self) -> int:
|
||||
if not self._pending_pdus:
|
||||
return 0
|
||||
return self._num_frames_between(self._expected_ack_seq, self._last_tx_seq + 1)
|
||||
|
||||
def _send_pdu(self, pdu: _PendingPdu) -> None:
|
||||
def _send_i_frame(self, pdu: _PendingPdu) -> None:
|
||||
pdu.req_seq = self._req_seq_num
|
||||
|
||||
self._start_receiver_ready_poll()
|
||||
self._tx_window.append(pdu)
|
||||
self.channel.send_pdu(bytes(pdu))
|
||||
self._last_acked_rx_seq = self._req_seq_num
|
||||
|
||||
def _send_s_frame(
|
||||
self,
|
||||
supervision_function: SupervisoryEnhancedControlField.SupervisoryFunction,
|
||||
final: int,
|
||||
) -> None:
|
||||
self.channel.send_pdu(
|
||||
SupervisoryEnhancedControlField(
|
||||
supervision_function=supervision_function,
|
||||
final=final,
|
||||
req_seq=self._req_seq_num,
|
||||
)
|
||||
)
|
||||
self._last_acked_rx_seq = self._req_seq_num
|
||||
|
||||
def _update_ack_seq(self, new_seq: int, is_poll_response: bool) -> None:
|
||||
num_frames_acked = self._num_frames_between(self._expected_ack_seq, new_seq)
|
||||
if num_frames_acked > self._num_unacked_frames:
|
||||
num_frames_acked = (new_seq - self._last_acked_tx_seq) % self.MAX_SEQ_NUM
|
||||
if num_frames_acked > len(self._tx_window):
|
||||
logger.error(
|
||||
"Received acknowledgment for %d frames but only %d frames are pending",
|
||||
num_frames_acked,
|
||||
self._num_unacked_frames,
|
||||
len(self._tx_window),
|
||||
)
|
||||
return
|
||||
if is_poll_response and self._monitor_handle:
|
||||
self._monitor_handle.cancel()
|
||||
self._monitor_handle = None
|
||||
|
||||
del self._pending_pdus[:num_frames_acked]
|
||||
self._expected_ack_seq = new_seq
|
||||
del self._tx_window[:num_frames_acked]
|
||||
self._last_acked_tx_seq = new_seq
|
||||
if (
|
||||
self._expected_ack_seq == self._next_tx_seq
|
||||
self._last_acked_tx_seq == self._next_tx_seq
|
||||
and self._receiver_ready_poll_handle
|
||||
):
|
||||
self._receiver_ready_poll_handle.cancel()
|
||||
@@ -1592,7 +1647,9 @@ class LeCreditBasedChannel(utils.EventEmitter):
|
||||
self.connection_result = None
|
||||
self.disconnection_result = None
|
||||
self.drained = asyncio.Event()
|
||||
self.att_mtu = 0 # Filled by GATT client or server later.
|
||||
# Core Specification Vol 3, Part G, 5.3.1 ATT_MTU
|
||||
# ATT_MTU shall be set to the minimum of the MTU field values of the two devices.
|
||||
self.att_mtu = min(mtu, peer_mtu)
|
||||
|
||||
self.drained.set()
|
||||
|
||||
|
||||
@@ -278,7 +278,7 @@ class L2CAPService(L2CAPServicer):
|
||||
if not l2cap_channel:
|
||||
return SendResponse(error=COMMAND_NOT_UNDERSTOOD)
|
||||
if isinstance(l2cap_channel, ClassicChannel):
|
||||
l2cap_channel.send_pdu(request.data)
|
||||
l2cap_channel.write(request.data)
|
||||
else:
|
||||
l2cap_channel.write(request.data)
|
||||
return SendResponse(success=empty_pb2.Empty())
|
||||
|
||||
@@ -16,35 +16,28 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from collections.abc import Callable
|
||||
|
||||
from bumble.gatt import (
|
||||
GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
||||
GATT_BATTERY_SERVICE,
|
||||
Characteristic,
|
||||
CharacteristicValue,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_adapters import (
|
||||
PackedCharacteristicAdapter,
|
||||
PackedCharacteristicProxyAdapter,
|
||||
)
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy
|
||||
from bumble import device, gatt, gatt_adapters, gatt_client
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class BatteryService(TemplateService):
|
||||
UUID = GATT_BATTERY_SERVICE
|
||||
class BatteryService(gatt.TemplateService):
|
||||
UUID = gatt.GATT_BATTERY_SERVICE
|
||||
BATTERY_LEVEL_FORMAT = 'B'
|
||||
|
||||
battery_level_characteristic: Characteristic[int]
|
||||
battery_level_characteristic: gatt.Characteristic[int]
|
||||
|
||||
def __init__(self, read_battery_level):
|
||||
self.battery_level_characteristic = PackedCharacteristicAdapter(
|
||||
Characteristic(
|
||||
GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
||||
Characteristic.Properties.READ | Characteristic.Properties.NOTIFY,
|
||||
Characteristic.READABLE,
|
||||
CharacteristicValue(read=read_battery_level),
|
||||
def __init__(self, read_battery_level: Callable[[device.Connection], int]) -> None:
|
||||
self.battery_level_characteristic = gatt_adapters.PackedCharacteristicAdapter(
|
||||
gatt.Characteristic(
|
||||
gatt.GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
||||
properties=(
|
||||
gatt.Characteristic.Properties.READ
|
||||
| gatt.Characteristic.Properties.NOTIFY
|
||||
),
|
||||
permissions=gatt.Characteristic.READABLE,
|
||||
value=gatt.CharacteristicValue(read=read_battery_level),
|
||||
),
|
||||
pack_format=BatteryService.BATTERY_LEVEL_FORMAT,
|
||||
)
|
||||
@@ -52,19 +45,17 @@ class BatteryService(TemplateService):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class BatteryServiceProxy(ProfileServiceProxy):
|
||||
class BatteryServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
SERVICE_CLASS = BatteryService
|
||||
|
||||
battery_level: CharacteristicProxy[int] | None
|
||||
battery_level: gatt_client.CharacteristicProxy[int]
|
||||
|
||||
def __init__(self, service_proxy):
|
||||
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(
|
||||
GATT_BATTERY_LEVEL_CHARACTERISTIC
|
||||
):
|
||||
self.battery_level = PackedCharacteristicProxyAdapter(
|
||||
characteristics[0], pack_format=BatteryService.BATTERY_LEVEL_FORMAT
|
||||
)
|
||||
else:
|
||||
self.battery_level = None
|
||||
self.battery_level = gatt_adapters.PackedCharacteristicProxyAdapter(
|
||||
service_proxy.get_required_characteristic_by_uuid(
|
||||
gatt.GATT_BATTERY_LEVEL_CHARACTERISTIC
|
||||
),
|
||||
pack_format=BatteryService.BATTERY_LEVEL_FORMAT,
|
||||
)
|
||||
|
||||
@@ -18,40 +18,30 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import enum
|
||||
import struct
|
||||
from enum import IntEnum
|
||||
from collections.abc import Callable, Sequence
|
||||
from typing import Any
|
||||
|
||||
from bumble import core
|
||||
from bumble.att import ATT_Error
|
||||
from bumble.gatt import (
|
||||
GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC,
|
||||
GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC,
|
||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||
GATT_HEART_RATE_SERVICE,
|
||||
Characteristic,
|
||||
CharacteristicValue,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_adapters import (
|
||||
DelegatedCharacteristicAdapter,
|
||||
PackedCharacteristicAdapter,
|
||||
SerializableCharacteristicAdapter,
|
||||
)
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import att, core, device, gatt, gatt_adapters, gatt_client, utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class HeartRateService(TemplateService):
|
||||
UUID = GATT_HEART_RATE_SERVICE
|
||||
class HeartRateService(gatt.TemplateService):
|
||||
UUID = gatt.GATT_HEART_RATE_SERVICE
|
||||
|
||||
HEART_RATE_CONTROL_POINT_FORMAT = 'B'
|
||||
CONTROL_POINT_NOT_SUPPORTED = 0x80
|
||||
RESET_ENERGY_EXPENDED = 0x01
|
||||
|
||||
heart_rate_measurement_characteristic: Characteristic[HeartRateMeasurement]
|
||||
body_sensor_location_characteristic: Characteristic[BodySensorLocation]
|
||||
heart_rate_control_point_characteristic: Characteristic[int]
|
||||
heart_rate_measurement_characteristic: gatt.Characteristic[HeartRateMeasurement]
|
||||
body_sensor_location_characteristic: gatt.Characteristic[BodySensorLocation]
|
||||
heart_rate_control_point_characteristic: gatt.Characteristic[int]
|
||||
|
||||
class BodySensorLocation(IntEnum):
|
||||
class BodySensorLocation(utils.OpenIntEnum):
|
||||
OTHER = 0
|
||||
CHEST = 1
|
||||
WRIST = 2
|
||||
@@ -60,82 +50,90 @@ class HeartRateService(TemplateService):
|
||||
EAR_LOBE = 5
|
||||
FOOT = 6
|
||||
|
||||
@dataclasses.dataclass
|
||||
class HeartRateMeasurement:
|
||||
def __init__(
|
||||
self,
|
||||
heart_rate,
|
||||
sensor_contact_detected=None,
|
||||
energy_expended=None,
|
||||
rr_intervals=None,
|
||||
):
|
||||
if heart_rate < 0 or heart_rate > 0xFFFF:
|
||||
heart_rate: int
|
||||
sensor_contact_detected: bool | None = None
|
||||
energy_expended: int | None = None
|
||||
rr_intervals: Sequence[float] | None = None
|
||||
|
||||
class Flag(enum.IntFlag):
|
||||
INT16_HEART_RATE = 1 << 0
|
||||
SENSOR_CONTACT_DETECTED = 1 << 1
|
||||
SENSOR_CONTACT_SUPPORTED = 1 << 2
|
||||
ENERGY_EXPENDED_STATUS = 1 << 3
|
||||
RR_INTERVAL = 1 << 4
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.heart_rate < 0 or self.heart_rate > 0xFFFF:
|
||||
raise core.InvalidArgumentError('heart_rate out of range')
|
||||
|
||||
if energy_expended is not None and (
|
||||
energy_expended < 0 or energy_expended > 0xFFFF
|
||||
if self.energy_expended is not None and (
|
||||
self.energy_expended < 0 or self.energy_expended > 0xFFFF
|
||||
):
|
||||
raise core.InvalidArgumentError('energy_expended out of range')
|
||||
|
||||
if rr_intervals:
|
||||
for rr_interval in rr_intervals:
|
||||
if self.rr_intervals:
|
||||
for rr_interval in self.rr_intervals:
|
||||
if rr_interval < 0 or rr_interval * 1024 > 0xFFFF:
|
||||
raise core.InvalidArgumentError('rr_intervals out of range')
|
||||
|
||||
self.heart_rate = heart_rate
|
||||
self.sensor_contact_detected = sensor_contact_detected
|
||||
self.energy_expended = energy_expended
|
||||
self.rr_intervals = rr_intervals
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
def from_bytes(cls, data: bytes) -> Self:
|
||||
flags = data[0]
|
||||
offset = 1
|
||||
|
||||
if flags & 1:
|
||||
hr = struct.unpack_from('<H', data, offset)[0]
|
||||
if flags & cls.Flag.INT16_HEART_RATE:
|
||||
heart_rate = struct.unpack_from('<H', data, offset)[0]
|
||||
offset += 2
|
||||
else:
|
||||
hr = struct.unpack_from('B', data, offset)[0]
|
||||
heart_rate = struct.unpack_from('B', data, offset)[0]
|
||||
offset += 1
|
||||
|
||||
if flags & (1 << 2):
|
||||
sensor_contact_detected = flags & (1 << 1) != 0
|
||||
if flags & cls.Flag.SENSOR_CONTACT_SUPPORTED:
|
||||
sensor_contact_detected = flags & cls.Flag.SENSOR_CONTACT_DETECTED != 0
|
||||
else:
|
||||
sensor_contact_detected = None
|
||||
|
||||
if flags & (1 << 3):
|
||||
if flags & cls.Flag.ENERGY_EXPENDED_STATUS:
|
||||
energy_expended = struct.unpack_from('<H', data, offset)[0]
|
||||
offset += 2
|
||||
else:
|
||||
energy_expended = None
|
||||
|
||||
if flags & (1 << 4):
|
||||
rr_intervals: Sequence[float] | None = None
|
||||
if flags & cls.Flag.RR_INTERVAL:
|
||||
rr_intervals = tuple(
|
||||
struct.unpack_from('<H', data, offset + i * 2)[0] / 1024
|
||||
for i in range((len(data) - offset) // 2)
|
||||
struct.unpack_from('<H', data, i)[0] / 1024
|
||||
for i in range(offset, len(data), 2)
|
||||
)
|
||||
else:
|
||||
rr_intervals = ()
|
||||
|
||||
return cls(hr, sensor_contact_detected, energy_expended, rr_intervals)
|
||||
return cls(
|
||||
heart_rate=heart_rate,
|
||||
sensor_contact_detected=sensor_contact_detected,
|
||||
energy_expended=energy_expended,
|
||||
rr_intervals=rr_intervals,
|
||||
)
|
||||
|
||||
def __bytes__(self):
|
||||
def __bytes__(self) -> bytes:
|
||||
flags = 0
|
||||
if self.heart_rate < 256:
|
||||
flags = 0
|
||||
data = struct.pack('B', self.heart_rate)
|
||||
else:
|
||||
flags = 1
|
||||
flags |= self.Flag.INT16_HEART_RATE
|
||||
data = struct.pack('<H', self.heart_rate)
|
||||
|
||||
if self.sensor_contact_detected is not None:
|
||||
flags |= ((1 if self.sensor_contact_detected else 0) << 1) | (1 << 2)
|
||||
flags |= self.Flag.SENSOR_CONTACT_SUPPORTED
|
||||
if self.sensor_contact_detected:
|
||||
flags |= self.Flag.SENSOR_CONTACT_DETECTED
|
||||
|
||||
if self.energy_expended is not None:
|
||||
flags |= 1 << 3
|
||||
flags |= self.Flag.ENERGY_EXPENDED_STATUS
|
||||
data += struct.pack('<H', self.energy_expended)
|
||||
|
||||
if self.rr_intervals:
|
||||
flags |= 1 << 4
|
||||
if self.rr_intervals is not None:
|
||||
flags |= self.Flag.RR_INTERVAL
|
||||
data += b''.join(
|
||||
[
|
||||
struct.pack('<H', int(rr_interval * 1024))
|
||||
@@ -145,57 +143,67 @@ class HeartRateService(TemplateService):
|
||||
|
||||
return bytes([flags]) + data
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f'HeartRateMeasurement(heart_rate={self.heart_rate},'
|
||||
f' sensor_contact_detected={self.sensor_contact_detected},'
|
||||
f' energy_expended={self.energy_expended},'
|
||||
f' rr_intervals={self.rr_intervals})'
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
read_heart_rate_measurement,
|
||||
body_sensor_location=None,
|
||||
reset_energy_expended=None,
|
||||
read_heart_rate_measurement: Callable[
|
||||
[device.Connection], HeartRateMeasurement
|
||||
],
|
||||
body_sensor_location: HeartRateService.BodySensorLocation | None = None,
|
||||
reset_energy_expended: Callable[[device.Connection], Any] | None = None,
|
||||
):
|
||||
self.heart_rate_measurement_characteristic = SerializableCharacteristicAdapter(
|
||||
Characteristic(
|
||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||
Characteristic.Properties.NOTIFY,
|
||||
0,
|
||||
CharacteristicValue(read=read_heart_rate_measurement),
|
||||
),
|
||||
HeartRateService.HeartRateMeasurement,
|
||||
self.heart_rate_measurement_characteristic = (
|
||||
gatt_adapters.SerializableCharacteristicAdapter(
|
||||
gatt.Characteristic(
|
||||
uuid=gatt.GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||
properties=gatt.Characteristic.Properties.NOTIFY,
|
||||
permissions=gatt.Characteristic.Permissions(0),
|
||||
value=gatt.CharacteristicValue(read=read_heart_rate_measurement),
|
||||
),
|
||||
HeartRateService.HeartRateMeasurement,
|
||||
)
|
||||
)
|
||||
characteristics = [self.heart_rate_measurement_characteristic]
|
||||
characteristics: list[gatt.Characteristic] = [
|
||||
self.heart_rate_measurement_characteristic
|
||||
]
|
||||
|
||||
if body_sensor_location is not None:
|
||||
self.body_sensor_location_characteristic = Characteristic(
|
||||
GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC,
|
||||
Characteristic.Properties.READ,
|
||||
Characteristic.READABLE,
|
||||
bytes([int(body_sensor_location)]),
|
||||
self.body_sensor_location_characteristic = (
|
||||
gatt_adapters.EnumCharacteristicAdapter(
|
||||
gatt.Characteristic(
|
||||
uuid=gatt.GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC,
|
||||
properties=gatt.Characteristic.Properties.READ,
|
||||
permissions=gatt.Characteristic.READABLE,
|
||||
value=body_sensor_location,
|
||||
),
|
||||
cls=self.BodySensorLocation,
|
||||
length=1,
|
||||
)
|
||||
)
|
||||
characteristics.append(self.body_sensor_location_characteristic)
|
||||
|
||||
if reset_energy_expended:
|
||||
|
||||
def write_heart_rate_control_point_value(connection, value):
|
||||
def write_heart_rate_control_point_value(
|
||||
connection: device.Connection, value: bytes
|
||||
) -> None:
|
||||
if value == self.RESET_ENERGY_EXPENDED:
|
||||
if reset_energy_expended is not None:
|
||||
reset_energy_expended(connection)
|
||||
else:
|
||||
raise ATT_Error(self.CONTROL_POINT_NOT_SUPPORTED)
|
||||
raise att.ATT_Error(self.CONTROL_POINT_NOT_SUPPORTED)
|
||||
|
||||
self.heart_rate_control_point_characteristic = PackedCharacteristicAdapter(
|
||||
Characteristic(
|
||||
GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC,
|
||||
Characteristic.Properties.WRITE,
|
||||
Characteristic.WRITEABLE,
|
||||
CharacteristicValue(write=write_heart_rate_control_point_value),
|
||||
),
|
||||
pack_format=HeartRateService.HEART_RATE_CONTROL_POINT_FORMAT,
|
||||
self.heart_rate_control_point_characteristic = (
|
||||
gatt_adapters.PackedCharacteristicAdapter(
|
||||
gatt.Characteristic(
|
||||
uuid=gatt.GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC,
|
||||
properties=gatt.Characteristic.Properties.WRITE,
|
||||
permissions=gatt.Characteristic.WRITEABLE,
|
||||
value=gatt.CharacteristicValue(
|
||||
write=write_heart_rate_control_point_value
|
||||
),
|
||||
),
|
||||
pack_format=HeartRateService.HEART_RATE_CONTROL_POINT_FORMAT,
|
||||
)
|
||||
)
|
||||
characteristics.append(self.heart_rate_control_point_characteristic)
|
||||
|
||||
@@ -203,50 +211,51 @@ class HeartRateService(TemplateService):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class HeartRateServiceProxy(ProfileServiceProxy):
|
||||
class HeartRateServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
SERVICE_CLASS = HeartRateService
|
||||
|
||||
heart_rate_measurement: (
|
||||
CharacteristicProxy[HeartRateService.HeartRateMeasurement] | None
|
||||
)
|
||||
heart_rate_measurement: gatt_client.CharacteristicProxy[
|
||||
HeartRateService.HeartRateMeasurement
|
||||
]
|
||||
body_sensor_location: (
|
||||
CharacteristicProxy[HeartRateService.BodySensorLocation] | None
|
||||
gatt_client.CharacteristicProxy[HeartRateService.BodySensorLocation] | None
|
||||
)
|
||||
heart_rate_control_point: CharacteristicProxy[int] | None
|
||||
heart_rate_control_point: gatt_client.CharacteristicProxy[int] | None
|
||||
|
||||
def __init__(self, service_proxy):
|
||||
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(
|
||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC
|
||||
):
|
||||
self.heart_rate_measurement = SerializableCharacteristicAdapter(
|
||||
characteristics[0], HeartRateService.HeartRateMeasurement
|
||||
self.heart_rate_measurement = (
|
||||
gatt_adapters.SerializableCharacteristicProxyAdapter(
|
||||
service_proxy.get_required_characteristic_by_uuid(
|
||||
gatt.GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC
|
||||
),
|
||||
HeartRateService.HeartRateMeasurement,
|
||||
)
|
||||
else:
|
||||
self.heart_rate_measurement = None
|
||||
)
|
||||
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(
|
||||
GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC
|
||||
gatt.GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC
|
||||
):
|
||||
self.body_sensor_location = DelegatedCharacteristicAdapter(
|
||||
characteristics[0],
|
||||
decode=lambda value: HeartRateService.BodySensorLocation(value[0]),
|
||||
self.body_sensor_location = gatt_adapters.EnumCharacteristicProxyAdapter(
|
||||
characteristics[0], cls=HeartRateService.BodySensorLocation, length=1
|
||||
)
|
||||
else:
|
||||
self.body_sensor_location = None
|
||||
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(
|
||||
GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC
|
||||
gatt.GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC
|
||||
):
|
||||
self.heart_rate_control_point = PackedCharacteristicAdapter(
|
||||
characteristics[0],
|
||||
pack_format=HeartRateService.HEART_RATE_CONTROL_POINT_FORMAT,
|
||||
self.heart_rate_control_point = (
|
||||
gatt_adapters.PackedCharacteristicProxyAdapter(
|
||||
characteristics[0],
|
||||
pack_format=HeartRateService.HEART_RATE_CONTROL_POINT_FORMAT,
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.heart_rate_control_point = None
|
||||
|
||||
async def reset_energy_expended(self):
|
||||
async def reset_energy_expended(self) -> None:
|
||||
if self.heart_rate_control_point is not None:
|
||||
return await self.heart_rate_control_point.write_value(
|
||||
HeartRateService.RESET_ENERGY_EXPENDED
|
||||
|
||||
@@ -800,7 +800,7 @@ class Multiplexer(utils.EventEmitter):
|
||||
|
||||
def send_frame(self, frame: RFCOMM_Frame) -> None:
|
||||
logger.debug(f'>>> Multiplexer sending {frame}')
|
||||
self.l2cap_channel.send_pdu(frame)
|
||||
self.l2cap_channel.write(bytes(frame))
|
||||
|
||||
def on_pdu(self, pdu: bytes) -> None:
|
||||
frame = RFCOMM_Frame.from_bytes(pdu)
|
||||
|
||||
@@ -847,7 +847,7 @@ class Client:
|
||||
self.pending_request = request
|
||||
|
||||
try:
|
||||
self.channel.send_pdu(bytes(request))
|
||||
self.channel.write(bytes(request))
|
||||
return await self.pending_response
|
||||
finally:
|
||||
self.pending_request = None
|
||||
@@ -1061,7 +1061,7 @@ class Server:
|
||||
|
||||
def send_response(self, response):
|
||||
logger.debug(f'{color(">>> Sending SDP Response", "blue")}: {response}')
|
||||
self.channel.send_pdu(response)
|
||||
self.channel.write(response)
|
||||
|
||||
def match_services(self, search_pattern: DataElement) -> dict[int, Service]:
|
||||
# Find the services for which the attributes in the pattern is a subset of the
|
||||
|
||||
@@ -63,7 +63,7 @@ HCI sockets provide a way to send/receive HCI packets to/from a Bluetooth contro
|
||||
See the [HCI Socket Transport page](../transports/hci_socket.md) for details on the `hci-socket` tansport syntax.
|
||||
|
||||
The HCI device referenced by an `hci-socket` transport (`hci<X>`, where `<X>` is an integer, with `hci0` being the first controller device, and so on) must be in the `DOWN` state before it can be opened as a transport.
|
||||
You can bring a HCI controller `UP` or `DOWN` with `hciconfig hci<X> up` and `hciconfig hci<X> up`.
|
||||
You can bring a HCI controller `UP` or `DOWN` with `hciconfig hci<X> up` and `hciconfig hci<X> down`.
|
||||
|
||||
!!! tip "HCI Socket Permissions"
|
||||
By default, when running as a regular user, you won't have the permission to use
|
||||
|
||||
@@ -71,8 +71,8 @@ async def main() -> None:
|
||||
rr_intervals=random.choice(
|
||||
(
|
||||
(
|
||||
random.randint(900, 1100) / 1000,
|
||||
random.randint(900, 1100) / 1000,
|
||||
random.randint(900, 1100) // 1000,
|
||||
random.randint(900, 1100) // 1000,
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -100,13 +100,9 @@ def on_sco_packet(packet: hci.HCI_SynchronousDataPacket):
|
||||
if source_file and (pcm_data := source_file.read(packet.data_total_length)):
|
||||
assert ag_protocol
|
||||
host = ag_protocol.dlc.multiplexer.l2cap_channel.connection.device.host
|
||||
host.send_hci_packet(
|
||||
hci.HCI_SynchronousDataPacket(
|
||||
connection_handle=packet.connection_handle,
|
||||
packet_status=0,
|
||||
data_total_length=len(pcm_data),
|
||||
data=pcm_data,
|
||||
)
|
||||
host.send_sco_sdu(
|
||||
connection_handle=packet.connection_handle,
|
||||
sdu=pcm_data,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,6 @@ use pyo3::PyResult;
|
||||
|
||||
#[pyo3_asyncio::tokio::test]
|
||||
async fn realtek_driver_info_all_drivers() -> PyResult<()> {
|
||||
assert_eq!(12, DriverInfo::all_drivers()?.len());
|
||||
assert_eq!(13, DriverInfo::all_drivers()?.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
34
tests/battery_service_test.py
Normal file
34
tests/battery_service_test.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Copyright 2021-2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
from bumble import device as device_module
|
||||
from bumble.profiles import battery_service
|
||||
|
||||
from . import test_utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_battery_level():
|
||||
devices = await test_utils.TwoDevices.create_with_connection()
|
||||
service = battery_service.BatteryService(lambda _: 1)
|
||||
devices[0].add_service(service)
|
||||
|
||||
async with device_module.Peer(devices.connections[1]) as peer:
|
||||
client = peer.create_service_proxy(battery_service.BatteryServiceProxy)
|
||||
assert client
|
||||
assert await client.battery_level.read_value() == 1
|
||||
@@ -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())
|
||||
|
||||
89
tests/heart_rate_service_test.py
Normal file
89
tests/heart_rate_service_test.py
Normal file
@@ -0,0 +1,89 @@
|
||||
# Copyright 2021-2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import asyncio
|
||||
import itertools
|
||||
from collections.abc import Sequence
|
||||
|
||||
import pytest
|
||||
|
||||
from bumble import device as device_module
|
||||
from bumble.profiles import heart_rate_service
|
||||
|
||||
from . import test_utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"heart_rate, sensor_contact_detected, energy_expanded, rr_intervals",
|
||||
itertools.product(
|
||||
(1, 1000), (True, False, None), (2, None), ((3.0, 4.0, 5.0), None)
|
||||
),
|
||||
)
|
||||
async def test_read_measurement(
|
||||
heart_rate: int,
|
||||
sensor_contact_detected: bool | None,
|
||||
energy_expanded: int | None,
|
||||
rr_intervals: Sequence[int] | None,
|
||||
):
|
||||
devices = await test_utils.TwoDevices.create_with_connection()
|
||||
measurement = heart_rate_service.HeartRateService.HeartRateMeasurement(
|
||||
heart_rate, sensor_contact_detected, energy_expanded, rr_intervals
|
||||
)
|
||||
service = heart_rate_service.HeartRateService(lambda _: measurement)
|
||||
devices[0].add_service(service)
|
||||
|
||||
async with device_module.Peer(devices.connections[1]) as peer:
|
||||
client = peer.create_service_proxy(heart_rate_service.HeartRateServiceProxy)
|
||||
assert client
|
||||
assert await client.heart_rate_measurement.read_value() == measurement
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_body_sensor_location():
|
||||
devices = await test_utils.TwoDevices.create_with_connection()
|
||||
measurement = heart_rate_service.HeartRateService.HeartRateMeasurement(0)
|
||||
location = heart_rate_service.HeartRateService.BodySensorLocation.FINGER
|
||||
service = heart_rate_service.HeartRateService(
|
||||
lambda _: measurement,
|
||||
body_sensor_location=location,
|
||||
)
|
||||
devices[0].add_service(service)
|
||||
|
||||
async with device_module.Peer(devices.connections[1]) as peer:
|
||||
client = peer.create_service_proxy(heart_rate_service.HeartRateServiceProxy)
|
||||
assert client
|
||||
assert client.body_sensor_location
|
||||
assert await client.body_sensor_location.read_value() == location
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_energy_expended() -> None:
|
||||
devices = await test_utils.TwoDevices.create_with_connection()
|
||||
measurement = heart_rate_service.HeartRateService.HeartRateMeasurement(1)
|
||||
reset_energy_expended = asyncio.Queue[None]()
|
||||
service = heart_rate_service.HeartRateService(
|
||||
lambda _: measurement,
|
||||
reset_energy_expended=lambda _: reset_energy_expended.put_nowait(None),
|
||||
)
|
||||
devices[0].add_service(service)
|
||||
|
||||
async with device_module.Peer(devices.connections[1]) as peer:
|
||||
client = peer.create_service_proxy(heart_rate_service.HeartRateServiceProxy)
|
||||
assert client
|
||||
await client.reset_energy_expended()
|
||||
await reset_energy_expended.get()
|
||||
@@ -239,20 +239,7 @@ async def transfer_payload(
|
||||
channels[1].sink = received.put_nowait
|
||||
sdu_lengths = (21, 70, 700, 5523)
|
||||
|
||||
if isinstance(channels[1], l2cap.LeCreditBasedChannel):
|
||||
mps = channels[1].mps
|
||||
elif isinstance(
|
||||
processor := channels[1].processor, l2cap.EnhancedRetransmissionProcessor
|
||||
):
|
||||
mps = processor.mps
|
||||
else:
|
||||
mps = channels[1].mtu
|
||||
|
||||
messages = [
|
||||
bytes([i % 8 for i in range(sdu_length)])
|
||||
for sdu_length in sdu_lengths
|
||||
if sdu_length <= mps
|
||||
]
|
||||
messages = [bytes([i % 8 for i in range(sdu_length)]) for sdu_length in sdu_lengths]
|
||||
for message in messages:
|
||||
channels[0].write(message)
|
||||
if isinstance(channels[0], l2cap.LeCreditBasedChannel):
|
||||
@@ -334,20 +321,26 @@ async def test_mtu():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_enhanced_retransmission_mode():
|
||||
@pytest.mark.parametrize("mtu,", (50, 255, 256, 1000))
|
||||
async def test_enhanced_retransmission_mode(mtu: int):
|
||||
devices = TwoDevices()
|
||||
await devices.setup_connection()
|
||||
|
||||
server_channels = asyncio.Queue[l2cap.ClassicChannel]()
|
||||
server = devices.devices[1].create_l2cap_server(
|
||||
spec=l2cap.ClassicChannelSpec(
|
||||
mode=l2cap.TransmissionMode.ENHANCED_RETRANSMISSION
|
||||
mode=l2cap.TransmissionMode.ENHANCED_RETRANSMISSION,
|
||||
mtu=mtu,
|
||||
mps=256,
|
||||
),
|
||||
handler=server_channels.put_nowait,
|
||||
)
|
||||
client_channel = await devices.connections[0].create_l2cap_channel(
|
||||
spec=l2cap.ClassicChannelSpec(
|
||||
server.psm, mode=l2cap.TransmissionMode.ENHANCED_RETRANSMISSION
|
||||
server.psm,
|
||||
mode=l2cap.TransmissionMode.ENHANCED_RETRANSMISSION,
|
||||
mtu=mtu,
|
||||
mps=1024,
|
||||
)
|
||||
)
|
||||
server_channel = await server_channels.get()
|
||||
|
||||
Reference in New Issue
Block a user