forked from auracaster/bumble_mirror
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fe7931d7d | |||
| cbd46adbcf | |||
| af466c2970 | |||
| bade4502f9 | |||
| 9f952f202f |
+15
-1
@@ -1,4 +1,4 @@
|
||||
# Copyright 2024 Google LLC
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -825,10 +825,24 @@ async def run_broadcast(
|
||||
),
|
||||
)
|
||||
print('Setup ISO Data Path')
|
||||
|
||||
def on_flow(packet_queue):
|
||||
print(
|
||||
f'\rPACKETS: pending={packet_queue.pending}, '
|
||||
f'queued={packet_queue.queued}, completed={packet_queue.completed}',
|
||||
end='',
|
||||
)
|
||||
|
||||
packet_queue = None
|
||||
for bis_link in big.bis_links:
|
||||
await bis_link.setup_data_path(
|
||||
direction=bis_link.Direction.HOST_TO_CONTROLLER
|
||||
)
|
||||
if packet_queue is None:
|
||||
packet_queue = bis_link.data_packet_queue
|
||||
|
||||
if packet_queue:
|
||||
packet_queue.on('flow', lambda: on_flow(packet_queue))
|
||||
|
||||
for frame in itertools.cycle(frames):
|
||||
mid = len(frame) // 2
|
||||
|
||||
+22
-6
@@ -37,6 +37,8 @@ from bumble.hci import (
|
||||
HCI_Command_Status_Event,
|
||||
HCI_READ_BUFFER_SIZE_COMMAND,
|
||||
HCI_Read_Buffer_Size_Command,
|
||||
HCI_LE_READ_BUFFER_SIZE_V2_COMMAND,
|
||||
HCI_LE_Read_Buffer_Size_V2_Command,
|
||||
HCI_READ_BD_ADDR_COMMAND,
|
||||
HCI_Read_BD_ADDR_Command,
|
||||
HCI_READ_LOCAL_NAME_COMMAND,
|
||||
@@ -147,7 +149,7 @@ async def get_le_info(host: Host) -> None:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def get_acl_flow_control_info(host: Host) -> None:
|
||||
async def get_flow_control_info(host: Host) -> None:
|
||||
print()
|
||||
|
||||
if host.supports_command(HCI_READ_BUFFER_SIZE_COMMAND):
|
||||
@@ -160,14 +162,28 @@ async def get_acl_flow_control_info(host: Host) -> None:
|
||||
f'packets of size {response.return_parameters.hc_acl_data_packet_length}',
|
||||
)
|
||||
|
||||
if host.supports_command(HCI_LE_READ_BUFFER_SIZE_COMMAND):
|
||||
if host.supports_command(HCI_LE_READ_BUFFER_SIZE_V2_COMMAND):
|
||||
response = await host.send_command(
|
||||
HCI_LE_Read_Buffer_Size_V2_Command(), check_result=True
|
||||
)
|
||||
print(
|
||||
color('LE ACL Flow Control:', 'yellow'),
|
||||
f'{response.return_parameters.total_num_le_acl_data_packets} '
|
||||
f'packets of size {response.return_parameters.le_acl_data_packet_length}',
|
||||
)
|
||||
print(
|
||||
color('LE ISO Flow Control:', 'yellow'),
|
||||
f'{response.return_parameters.total_num_iso_data_packets} '
|
||||
f'packets of size {response.return_parameters.iso_data_packet_length}',
|
||||
)
|
||||
elif host.supports_command(HCI_LE_READ_BUFFER_SIZE_COMMAND):
|
||||
response = await host.send_command(
|
||||
HCI_LE_Read_Buffer_Size_Command(), check_result=True
|
||||
)
|
||||
print(
|
||||
color('LE ACL Flow Control:', 'yellow'),
|
||||
f'{response.return_parameters.hc_total_num_le_acl_data_packets} '
|
||||
f'packets of size {response.return_parameters.hc_le_acl_data_packet_length}',
|
||||
f'{response.return_parameters.total_num_le_acl_data_packets} '
|
||||
f'packets of size {response.return_parameters.le_acl_data_packet_length}',
|
||||
)
|
||||
|
||||
|
||||
@@ -274,8 +290,8 @@ async def async_main(latency_probes, transport):
|
||||
# Get the LE info
|
||||
await get_le_info(host)
|
||||
|
||||
# Print the ACL flow control info
|
||||
await get_acl_flow_control_info(host)
|
||||
# Print the flow control info
|
||||
await get_flow_control_info(host)
|
||||
|
||||
# Get codec info
|
||||
await get_codecs_info(host)
|
||||
|
||||
+24
-9
@@ -154,15 +154,17 @@ class Controller:
|
||||
'0000000060000000'
|
||||
) # BR/EDR Not Supported, LE Supported (Controller)
|
||||
self.manufacturer_name = 0xFFFF
|
||||
self.hc_data_packet_length = 27
|
||||
self.hc_total_num_data_packets = 64
|
||||
self.hc_le_data_packet_length = 27
|
||||
self.hc_total_num_le_data_packets = 64
|
||||
self.acl_data_packet_length = 27
|
||||
self.total_num_acl_data_packets = 64
|
||||
self.le_acl_data_packet_length = 27
|
||||
self.total_num_le_acl_data_packets = 64
|
||||
self.iso_data_packet_length = 960
|
||||
self.total_num_iso_data_packets = 64
|
||||
self.event_mask = 0
|
||||
self.event_mask_page_2 = 0
|
||||
self.supported_commands = bytes.fromhex(
|
||||
'2000800000c000000000e4000000a822000000000000040000f7ffff7f000000'
|
||||
'30f0f9ff01008004000000000000000000000000000000000000000000000000'
|
||||
'30f0f9ff01008004002000000000000000000000000000000000000000000000'
|
||||
)
|
||||
self.le_event_mask = 0
|
||||
self.advertising_parameters = None
|
||||
@@ -1181,9 +1183,9 @@ class Controller:
|
||||
return struct.pack(
|
||||
'<BHBHH',
|
||||
HCI_SUCCESS,
|
||||
self.hc_data_packet_length,
|
||||
self.acl_data_packet_length,
|
||||
0,
|
||||
self.hc_total_num_data_packets,
|
||||
self.total_num_acl_data_packets,
|
||||
0,
|
||||
)
|
||||
|
||||
@@ -1212,8 +1214,21 @@ class Controller:
|
||||
return struct.pack(
|
||||
'<BHB',
|
||||
HCI_SUCCESS,
|
||||
self.hc_le_data_packet_length,
|
||||
self.hc_total_num_le_data_packets,
|
||||
self.le_acl_data_packet_length,
|
||||
self.total_num_le_acl_data_packets,
|
||||
)
|
||||
|
||||
def on_hci_le_read_buffer_size_v2_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.2 LE Read Buffer Size Command
|
||||
'''
|
||||
return struct.pack(
|
||||
'<BHBHB',
|
||||
HCI_SUCCESS,
|
||||
self.le_acl_data_packet_length,
|
||||
self.total_num_le_acl_data_packets,
|
||||
self.iso_data_packet_length,
|
||||
self.total_num_iso_data_packets,
|
||||
)
|
||||
|
||||
def on_hci_le_read_local_supported_features_command(self, _command):
|
||||
|
||||
+10
-19
@@ -52,7 +52,7 @@ from pyee import EventEmitter
|
||||
from .colors import color
|
||||
from .att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU
|
||||
from .gatt import Characteristic, Descriptor, Service
|
||||
from .host import Host
|
||||
from .host import DataPacketQueue, Host
|
||||
from .profiles.gap import GenericAccessService
|
||||
from .core import (
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
@@ -1329,7 +1329,6 @@ class ScoLink(CompositeEventEmitter):
|
||||
class _IsoLink:
|
||||
handle: int
|
||||
device: Device
|
||||
packet_sequence_number: int
|
||||
sink: Callable[[hci.HCI_IsoDataPacket], Any] | None = None
|
||||
|
||||
class Direction(IntEnum):
|
||||
@@ -1391,22 +1390,12 @@ class _IsoLink:
|
||||
return response.return_parameters.status
|
||||
|
||||
def write(self, sdu: bytes) -> None:
|
||||
"""Write an ISO SDU.
|
||||
"""Write an ISO SDU."""
|
||||
self.device.host.send_iso_sdu(connection_handle=self.handle, sdu=sdu)
|
||||
|
||||
This will automatically increase the packet sequence number.
|
||||
"""
|
||||
self.device.host.send_hci_packet(
|
||||
hci.HCI_IsoDataPacket(
|
||||
connection_handle=self.handle,
|
||||
data_total_length=len(sdu) + 4,
|
||||
packet_sequence_number=self.packet_sequence_number,
|
||||
pb_flag=0b10,
|
||||
packet_status_flag=0,
|
||||
iso_sdu_length=len(sdu),
|
||||
iso_sdu_fragment=sdu,
|
||||
)
|
||||
)
|
||||
self.packet_sequence_number = (self.packet_sequence_number + 1) % 0x10000
|
||||
@property
|
||||
def data_packet_queue(self) -> DataPacketQueue | None:
|
||||
return self.device.host.get_data_packet_queue(self.handle)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1426,7 +1415,6 @@ class CisLink(CompositeEventEmitter, _IsoLink):
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
super().__init__()
|
||||
self.packet_sequence_number = 0
|
||||
|
||||
async def disconnect(
|
||||
self, reason: int = hci.HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR
|
||||
@@ -1443,7 +1431,6 @@ class BisLink(_IsoLink):
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.device = self.big.device
|
||||
self.packet_sequence_number = 0
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1691,6 +1678,10 @@ class Connection(CompositeEventEmitter):
|
||||
self.peer_le_features = await self.device.get_remote_le_features(self)
|
||||
return self.peer_le_features
|
||||
|
||||
@property
|
||||
def data_packet_queue(self) -> DataPacketQueue | None:
|
||||
return self.device.host.get_data_packet_queue(self.handle)
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
|
||||
+625
-8
@@ -275,7 +275,7 @@ HCI_LE_CS_READ_REMOTE_SUPPORTED_CAPABILITIES_COMPLETE_EVENT = 0x2C
|
||||
HCI_LE_CS_READ_REMOTE_FAE_TABLE_COMPLETE_EVENT = 0x2D
|
||||
HCI_LE_CS_SECURITY_ENABLE_COMPLETE_EVENT = 0x2E
|
||||
HCI_LE_CS_CONFIG_COMPLETE_EVENT = 0x2F
|
||||
HCI_LE_CS_PROCEDURE_ENABLE_EVENT = 0x30
|
||||
HCI_LE_CS_PROCEDURE_ENABLE_COMPLETE_EVENT = 0x30
|
||||
HCI_LE_CS_SUBEVENT_RESULT_EVENT = 0x31
|
||||
HCI_LE_CS_SUBEVENT_RESULT_CONTINUE_EVENT = 0x32
|
||||
HCI_LE_CS_TEST_END_COMPLETE_EVENT = 0x33
|
||||
@@ -599,7 +599,7 @@ HCI_LE_READ_ALL_LOCAL_SUPPORTED_FEATURES_COMMAND = hci_c
|
||||
HCI_LE_READ_ALL_REMOTE_FEATURES_COMMAND = hci_command_op_code(0x08, 0x0088)
|
||||
HCI_LE_CS_READ_LOCAL_SUPPORTED_CAPABILITIES_COMMAND = hci_command_op_code(0x08, 0x0089)
|
||||
HCI_LE_CS_READ_REMOTE_SUPPORTED_CAPABILITIES_COMMAND = hci_command_op_code(0x08, 0x008A)
|
||||
HCI_LE_CS_WRITE_CACHED_REMOTE_SUPPORTED_CAPABILITIES = hci_command_op_code(0x08, 0x008B)
|
||||
HCI_LE_CS_WRITE_CACHED_REMOTE_SUPPORTED_CAPABILITIES_COMMAND = hci_command_op_code(0x08, 0x008B)
|
||||
HCI_LE_CS_SECURITY_ENABLE_COMMAND = hci_command_op_code(0x08, 0x008C)
|
||||
HCI_LE_CS_SET_DEFAULT_SETTINGS_COMMAND = hci_command_op_code(0x08, 0x008D)
|
||||
HCI_LE_CS_READ_REMOTE_FAE_TABLE_COMMAND = hci_command_op_code(0x08, 0x008E)
|
||||
@@ -751,6 +751,46 @@ class PhyBit(enum.IntFlag):
|
||||
LE_CODED = 1 << HCI_LE_CODED_PHY_BIT
|
||||
|
||||
|
||||
class CsRole(OpenIntEnum):
|
||||
INITIATOR = 0x00
|
||||
REFLECTOR = 0x01
|
||||
|
||||
|
||||
class CsRoleMask(enum.IntFlag):
|
||||
INITIATOR = 0x01
|
||||
REFLECTOR = 0x02
|
||||
|
||||
|
||||
class CsSyncPhy(OpenIntEnum):
|
||||
LE_1M = 1
|
||||
LE_2M = 2
|
||||
LE_2M_2BT = 3
|
||||
|
||||
|
||||
class CsSyncPhySupported(enum.IntFlag):
|
||||
LE_2M = 0x01
|
||||
LE_2M_2BT = 0x02
|
||||
|
||||
|
||||
class RttType(OpenIntEnum):
|
||||
AA_ONLY = 0x00
|
||||
SOUNDING_SEQUENCE_32_BIT = 0x01
|
||||
SOUNDING_SEQUENCE_96_BIT = 0x02
|
||||
RANDOM_SEQUENCE_32_BIT = 0x03
|
||||
RANDOM_SEQUENCE_64_BIT = 0x04
|
||||
RANDOM_SEQUENCE_96_BIT = 0x05
|
||||
RANDOM_SEQUENCE_128_BIT = 0x06
|
||||
|
||||
|
||||
class CsSnr(OpenIntEnum):
|
||||
SNR_18_DB = 0x00
|
||||
SNR_21_DB = 0x01
|
||||
SNR_24_DB = 0x02
|
||||
SNR_27_DB = 0x03
|
||||
SNR_30_DB = 0x04
|
||||
NOT_APPLIED = 0xFF
|
||||
|
||||
|
||||
# Connection Parameters
|
||||
HCI_CONNECTION_INTERVAL_MS_PER_UNIT = 1.25
|
||||
HCI_CONNECTION_LATENCY_MS_PER_UNIT = 1.25
|
||||
@@ -971,7 +1011,7 @@ HCI_SUPPORTED_COMMANDS_MASKS = {
|
||||
HCI_READ_ENCRYPTION_KEY_SIZE_COMMAND : 1 << (20*8+4),
|
||||
HCI_LE_CS_READ_LOCAL_SUPPORTED_CAPABILITIES_COMMAND : 1 << (20*8+5),
|
||||
HCI_LE_CS_READ_REMOTE_SUPPORTED_CAPABILITIES_COMMAND : 1 << (20*8+6),
|
||||
HCI_LE_CS_WRITE_CACHED_REMOTE_SUPPORTED_CAPABILITIES : 1 << (20*8+7),
|
||||
HCI_LE_CS_WRITE_CACHED_REMOTE_SUPPORTED_CAPABILITIES_COMMAND : 1 << (20*8+7),
|
||||
HCI_SET_EVENT_MASK_PAGE_2_COMMAND : 1 << (22*8+2),
|
||||
HCI_READ_FLOW_CONTROL_MODE_COMMAND : 1 << (23*8+0),
|
||||
HCI_WRITE_FLOW_CONTROL_MODE_COMMAND : 1 << (23*8+1),
|
||||
@@ -1462,6 +1502,12 @@ class LmpFeatureMask(enum.IntFlag):
|
||||
# -----------------------------------------------------------------------------
|
||||
# pylint: disable-next=unnecessary-lambda
|
||||
STATUS_SPEC = {'size': 1, 'mapper': lambda x: HCI_Constant.status_name(x)}
|
||||
CS_ROLE_SPEC = {'size': 1, 'mapper': lambda x: CsRole(x).name}
|
||||
CS_ROLE_MASK_SPEC = {'size': 1, 'mapper': lambda x: CsRoleMask(x).name}
|
||||
CS_SYNC_PHY_SPEC = {'size': 1, 'mapper': lambda x: CsSyncPhy(x).name}
|
||||
CS_SYNC_PHY_SUPPORTED_SPEC = {'size': 1, 'mapper': lambda x: CsSyncPhySupported(x).name}
|
||||
RTT_TYPE_SPEC = {'size': 1, 'mapper': lambda x: RttType(x).name}
|
||||
CS_SNR_SPEC = {'size': 1, 'mapper': lambda x: CsSnr(x).name}
|
||||
|
||||
|
||||
class CodecID(OpenIntEnum):
|
||||
@@ -3539,8 +3585,8 @@ class HCI_LE_Set_Event_Mask_Command(HCI_Command):
|
||||
@HCI_Command.command(
|
||||
return_parameters_fields=[
|
||||
('status', STATUS_SPEC),
|
||||
('hc_le_acl_data_packet_length', 2),
|
||||
('hc_total_num_le_acl_data_packets', 1),
|
||||
('le_acl_data_packet_length', 2),
|
||||
('total_num_le_acl_data_packets', 1),
|
||||
]
|
||||
)
|
||||
class HCI_LE_Read_Buffer_Size_Command(HCI_Command):
|
||||
@@ -3549,6 +3595,22 @@ class HCI_LE_Read_Buffer_Size_Command(HCI_Command):
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
return_parameters_fields=[
|
||||
('status', STATUS_SPEC),
|
||||
('le_acl_data_packet_length', 2),
|
||||
('total_num_le_acl_data_packets', 1),
|
||||
('iso_data_packet_length', 2),
|
||||
('total_num_iso_data_packets', 1),
|
||||
]
|
||||
)
|
||||
class HCI_LE_Read_Buffer_Size_V2_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.8.2 LE Read Buffer Size V2 Command
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
return_parameters_fields=[('status', STATUS_SPEC), ('le_features', 8)]
|
||||
@@ -5059,6 +5121,275 @@ class HCI_LE_Set_Host_Feature_Command(HCI_Command):
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
return_parameters_fields=[
|
||||
('status', STATUS_SPEC),
|
||||
('num_config_supported', 1),
|
||||
('max_consecutive_procedures_supported', 2),
|
||||
('num_antennas_supported', 1),
|
||||
('max_antenna_paths_supported', 1),
|
||||
('roles_supported', 1),
|
||||
('modes_supported', 1),
|
||||
('rtt_capability', 1),
|
||||
('rtt_aa_only_n', 1),
|
||||
('rtt_sounding_n', 1),
|
||||
('rtt_random_payload_n', 1),
|
||||
('nadm_sounding_capability', 2),
|
||||
('nadm_random_capability', 2),
|
||||
('cs_sync_phys_supported', CS_SYNC_PHY_SUPPORTED_SPEC),
|
||||
('subfeatures_supported', 2),
|
||||
('t_ip1_times_supported', 2),
|
||||
('t_ip2_times_supported', 2),
|
||||
('t_fcs_times_supported', 2),
|
||||
('t_pm_times_supported', 2),
|
||||
('t_sw_time_supported', 1),
|
||||
('tx_snr_capability', CS_SNR_SPEC),
|
||||
]
|
||||
)
|
||||
class HCI_LE_CS_Read_Local_Supported_Capabilities_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.8.130 LE CS Read Local Supported Capabilities command
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command([('connection_handle', 2)])
|
||||
class HCI_LE_CS_Read_Remote_Supported_Capabilities_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.8.131 LE CS Read Remote Supported Capabilities command
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
[
|
||||
('connection_handle', 2),
|
||||
('num_config_supported', 1),
|
||||
('max_consecutive_procedures_supported', 2),
|
||||
('num_antennas_supported', 1),
|
||||
('max_antenna_paths_supported', 1),
|
||||
('roles_supported', 1),
|
||||
('modes_supported', 1),
|
||||
('rtt_capability', 1),
|
||||
('rtt_aa_only_n', 1),
|
||||
('rtt_sounding_n', 1),
|
||||
('rtt_random_payload_n', 1),
|
||||
('nadm_sounding_capability', 2),
|
||||
('nadm_random_capability', 2),
|
||||
('cs_sync_phys_supported', CS_SYNC_PHY_SUPPORTED_SPEC),
|
||||
('subfeatures_supported', 2),
|
||||
('t_ip1_times_supported', 2),
|
||||
('t_ip2_times_supported', 2),
|
||||
('t_fcs_times_supported', 2),
|
||||
('t_pm_times_supported', 2),
|
||||
('t_sw_time_supported', 1),
|
||||
('tx_snr_capability', CS_SNR_SPEC),
|
||||
],
|
||||
return_parameters_fields=[
|
||||
('status', STATUS_SPEC),
|
||||
('connection_handle', 2),
|
||||
],
|
||||
)
|
||||
class HCI_LE_CS_Write_Cached_Remote_Supported_Capabilities_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.8.132 LE CS Write Cached Remote Supported Capabilities command
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command([('connection_handle', 2)])
|
||||
class HCI_LE_CS_Security_Enable_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.8.133 LE CS Security Enable command
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
[
|
||||
('connection_handle', 2),
|
||||
(
|
||||
'role_enable',
|
||||
CS_ROLE_MASK_SPEC,
|
||||
),
|
||||
('cs_sync_antenna_selection', 1),
|
||||
('max_tx_power', 1),
|
||||
],
|
||||
return_parameters_fields=[('status', STATUS_SPEC), ('connection_handle', 2)],
|
||||
)
|
||||
class HCI_LE_CS_Set_Default_Settings_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.8.134 LE CS Security Enable command
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command([('connection_handle', 2)])
|
||||
class HCI_LE_CS_Read_Remote_FAE_Table_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.8.135 LE CS Read Remote FAE Table command
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
[
|
||||
('connection_handle', 2),
|
||||
('remote_fae_table', 72),
|
||||
],
|
||||
return_parameters_fields=[('status', STATUS_SPEC), ('connection_handle', 2)],
|
||||
)
|
||||
class HCI_LE_CS_Write_Cached_Remote_FAE_Table_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.8.136 LE CS Write Cached Remote FAE Table command
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
[
|
||||
('connection_handle', 2),
|
||||
('config_id', 1),
|
||||
('create_context', 1),
|
||||
('main_mode_type', 1),
|
||||
('sub_mode_type', 1),
|
||||
('min_main_mode_steps', 1),
|
||||
('max_main_mode_steps', 1),
|
||||
('main_mode_repetition', 1),
|
||||
('mode_0_steps', 1),
|
||||
('role', CS_ROLE_SPEC),
|
||||
('rtt_type', RTT_TYPE_SPEC),
|
||||
('cs_sync_phy', CS_SYNC_PHY_SPEC),
|
||||
('channel_map', 10),
|
||||
('channel_map_repetition', 1),
|
||||
('channel_selection_type', 1),
|
||||
('ch3c_shape', 1),
|
||||
('ch3c_jump', 1),
|
||||
('reserved', 1),
|
||||
],
|
||||
)
|
||||
class HCI_LE_CS_Create_Config_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.8.137 LE CS Create Config command
|
||||
'''
|
||||
|
||||
class ChannelSelectionType(OpenIntEnum):
|
||||
ALGO_3B = 0
|
||||
ALGO_3C = 1
|
||||
|
||||
class Ch3cShape(OpenIntEnum):
|
||||
HAT = 0x00
|
||||
X = 0x01
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
[
|
||||
('connection_handle', 2),
|
||||
('config_id', 1),
|
||||
],
|
||||
)
|
||||
class HCI_LE_CS_Remove_Config_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.8.138 LE CS Remove Config command
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
[('channel_classification', 10)], return_parameters_fields=[('status', STATUS_SPEC)]
|
||||
)
|
||||
class HCI_LE_CS_Set_Channel_Classification_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.8.139 LE CS Set Channel Classification command
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
[
|
||||
('connection_handle', 2),
|
||||
('config_id', 1),
|
||||
('max_procedure_len', 2),
|
||||
('min_procedure_interval', 2),
|
||||
('max_procedure_interval', 2),
|
||||
('max_procedure_count', 2),
|
||||
('min_subevent_len', 3),
|
||||
('max_subevent_len', 3),
|
||||
('tone_antenna_config_selection', 1),
|
||||
('phy', 1),
|
||||
('tx_power_delta', 1),
|
||||
('preferred_peer_antenna', 1),
|
||||
('snr_control_initiator', CS_SNR_SPEC),
|
||||
('snr_control_reflector', CS_SNR_SPEC),
|
||||
],
|
||||
return_parameters_fields=[('status', STATUS_SPEC), ('connection_handle', 2)],
|
||||
)
|
||||
class HCI_LE_CS_Set_Procedure_Parameters_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.8.140 LE CS Set Procedure Parameters command
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
[
|
||||
('connection_handle', 2),
|
||||
('config_id', 1),
|
||||
('enable', 1),
|
||||
],
|
||||
)
|
||||
class HCI_LE_CS_Procedure_Enable_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.8.141 LE CS Procedure Enable command
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
[
|
||||
('main_mode_type', 1),
|
||||
('sub_mode_type', 1),
|
||||
('main_mode_repetition', 1),
|
||||
('mode_0_steps', 1),
|
||||
('role', CS_ROLE_SPEC),
|
||||
('rtt_type', RTT_TYPE_SPEC),
|
||||
('cs_sync_phy', CS_SYNC_PHY_SPEC),
|
||||
('cs_sync_antenna_selection', 1),
|
||||
('subevent_len', 3),
|
||||
('subevent_interval', 2),
|
||||
('max_num_subevents', 1),
|
||||
('transmit_power_level', 1),
|
||||
('t_ip1_time', 1),
|
||||
('t_ip2_time', 1),
|
||||
('t_fcs_time', 1),
|
||||
('t_pm_time', 1),
|
||||
('t_sw_time', 1),
|
||||
('tone_antenna_config_selection', 1),
|
||||
('reserved', 1),
|
||||
('snr_control_initiator', CS_SNR_SPEC),
|
||||
('snr_control_reflector', CS_SNR_SPEC),
|
||||
('drbg_nonce', 2),
|
||||
('channel_map_repetition', 1),
|
||||
('override_config', 2),
|
||||
('override_parameters_data', 'v'),
|
||||
],
|
||||
)
|
||||
class HCI_LE_CS_Test_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.8.142 LE CS Test command
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command()
|
||||
class HCI_LE_CS_Test_End_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.8.143 LE CS Test End command
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# HCI Events
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -6009,6 +6340,291 @@ class HCI_LE_BIGInfo_Advertising_Report_Event(HCI_LE_Meta_Event):
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_LE_Meta_Event.event(
|
||||
[
|
||||
('status', STATUS_SPEC),
|
||||
('connection_handle', 2),
|
||||
('num_config_supported', 1),
|
||||
('max_consecutive_procedures_supported', 2),
|
||||
('num_antennas_supported', 1),
|
||||
('max_antenna_paths_supported', 1),
|
||||
('roles_supported', 1),
|
||||
('modes_supported', 1),
|
||||
('rtt_capability', 1),
|
||||
('rtt_aa_only_n', 1),
|
||||
('rtt_sounding_n', 1),
|
||||
('rtt_random_payload_n', 1),
|
||||
('nadm_sounding_capability', 2),
|
||||
('nadm_random_capability', 2),
|
||||
('cs_sync_phys_supported', CS_SYNC_PHY_SUPPORTED_SPEC),
|
||||
('subfeatures_supported', 2),
|
||||
('t_ip1_times_supported', 2),
|
||||
('t_ip2_times_supported', 2),
|
||||
('t_fcs_times_supported', 2),
|
||||
('t_pm_times_supported', 2),
|
||||
('t_sw_time_supported', 1),
|
||||
('tx_snr_capability', CS_SNR_SPEC),
|
||||
]
|
||||
)
|
||||
class HCI_LE_CS_Read_Remote_Supported_Capabilities_Complete_Event(HCI_LE_Meta_Event):
|
||||
'''
|
||||
See Bluetooth spec @ 7.7.65.39 LE CS Read Remote Supported Capabilities Complete event
|
||||
'''
|
||||
|
||||
status: int
|
||||
connection_handle: int
|
||||
num_config_supported: int
|
||||
max_consecutive_procedures_supported: int
|
||||
num_antennas_supported: int
|
||||
max_antenna_paths_supported: int
|
||||
roles_supported: int
|
||||
modes_supported: int
|
||||
rtt_capability: int
|
||||
rtt_aa_only_n: int
|
||||
rtt_sounding_n: int
|
||||
rtt_random_payload_n: int
|
||||
nadm_sounding_capability: int
|
||||
nadm_random_capability: int
|
||||
cs_sync_phys_supported: int
|
||||
subfeatures_supported: int
|
||||
t_ip1_times_supported: int
|
||||
t_ip2_times_supported: int
|
||||
t_fcs_times_supported: int
|
||||
t_pm_times_supported: int
|
||||
t_sw_time_supported: int
|
||||
tx_snr_capability: int
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_LE_Meta_Event.event(
|
||||
[
|
||||
('status', STATUS_SPEC),
|
||||
('connection_handle', 2),
|
||||
('remote_fae_table', 72),
|
||||
]
|
||||
)
|
||||
class HCI_LE_CS_Read_Remote_FAE_Table_Complete_Event(HCI_LE_Meta_Event):
|
||||
'''
|
||||
See Bluetooth spec @ 7.7.65.40 LE CS Read Remote FAE Table Complete event
|
||||
'''
|
||||
|
||||
status: int
|
||||
connection_handle: int
|
||||
remote_fae_table: bytes
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_LE_Meta_Event.event(
|
||||
[
|
||||
('status', STATUS_SPEC),
|
||||
('connection_handle', 2),
|
||||
]
|
||||
)
|
||||
class HCI_LE_CS_Security_Enable_Complete_Event(HCI_LE_Meta_Event):
|
||||
'''
|
||||
See Bluetooth spec @ 7.7.65.41 LE CS Security Enable Complete event
|
||||
'''
|
||||
|
||||
status: int
|
||||
connection_handle: int
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_LE_Meta_Event.event(
|
||||
[
|
||||
('status', STATUS_SPEC),
|
||||
('connection_handle', 2),
|
||||
('config_id', 1),
|
||||
(
|
||||
'action',
|
||||
{
|
||||
'size': 1,
|
||||
'mapper': lambda x: HCI_LE_CS_Config_Complete_Event.Action(x).name,
|
||||
},
|
||||
),
|
||||
('main_mode_type', 1),
|
||||
('sub_mode_type', 1),
|
||||
('min_main_mode_steps', 1),
|
||||
('max_main_mode_steps', 1),
|
||||
('main_mode_repetition', 1),
|
||||
('mode_0_steps', 1),
|
||||
('role', CS_ROLE_SPEC),
|
||||
('rtt_type', RTT_TYPE_SPEC),
|
||||
('cs_sync_phy', CS_SYNC_PHY_SPEC),
|
||||
('channel_map', 10),
|
||||
('channel_map_repetition', 1),
|
||||
('channel_selection_type', 1),
|
||||
('ch3c_shape', 1),
|
||||
('ch3c_jump', 1),
|
||||
('reserved', 1),
|
||||
('t_ip1_time', 1),
|
||||
('t_ip2_time', 1),
|
||||
('t_fcs_time', 1),
|
||||
('t_pm_time', 1),
|
||||
]
|
||||
)
|
||||
class HCI_LE_CS_Config_Complete_Event(HCI_LE_Meta_Event):
|
||||
'''
|
||||
See Bluetooth spec @ 7.7.65.42 LE CS Config Complete event
|
||||
'''
|
||||
|
||||
class Action(OpenIntEnum):
|
||||
REMOVED = 0
|
||||
CREATED = 1
|
||||
|
||||
status: int
|
||||
connection_handle: int
|
||||
config_id: int
|
||||
action: int
|
||||
main_mode_type: int
|
||||
sub_mode_type: int
|
||||
min_main_mode_steps: int
|
||||
max_main_mode_steps: int
|
||||
main_mode_repetition: int
|
||||
mode_0_steps: int
|
||||
role: int
|
||||
rtt_type: int
|
||||
cs_sync_phy: int
|
||||
channel_map: bytes
|
||||
channel_map_repetition: int
|
||||
channel_selection_type: int
|
||||
ch3c_shape: int
|
||||
ch3c_jump: int
|
||||
reserved: int
|
||||
t_ip1_time: int
|
||||
t_ip2_time: int
|
||||
t_fcs_time: int
|
||||
t_pm_time: int
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_LE_Meta_Event.event(
|
||||
[
|
||||
('status', STATUS_SPEC),
|
||||
('connection_handle', 2),
|
||||
('config_id', 1),
|
||||
('state', 1),
|
||||
('tone_antenna_config_selection', 1),
|
||||
('selected_tx_power', 1),
|
||||
('subevent_len', 3),
|
||||
('subevents_per_event', 1),
|
||||
('subevent_interval', 2),
|
||||
('event_interval', 2),
|
||||
('procedure_interval', 2),
|
||||
('procedure_count', 2),
|
||||
('max_procedure_len', 2),
|
||||
]
|
||||
)
|
||||
class HCI_LE_CS_Procedure_Enable_Complete_Event(HCI_LE_Meta_Event):
|
||||
'''
|
||||
See Bluetooth spec @ 7.7.65.43 LE CS Procedure Enable Complete event
|
||||
'''
|
||||
|
||||
class State(OpenIntEnum):
|
||||
DISABLED = 0
|
||||
ENABLED = 1
|
||||
|
||||
status: int
|
||||
connection_handle: int
|
||||
config_id: int
|
||||
state: int
|
||||
tone_antenna_config_selection: int
|
||||
selected_tx_power: int
|
||||
subevent_len: int
|
||||
subevents_per_event: int
|
||||
subevent_interval: int
|
||||
event_interval: int
|
||||
procedure_interval: int
|
||||
procedure_count: int
|
||||
max_procedure_len: int
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_LE_Meta_Event.event(
|
||||
[
|
||||
('connection_handle', 2),
|
||||
('config_id', 1),
|
||||
('start_acl_conn_event_counter', 2),
|
||||
('procedure_counter', 2),
|
||||
('frequency_compensation', 2),
|
||||
('reference_power_level', 1),
|
||||
('procedure_done_status', 1),
|
||||
('subevent_done_status', 1),
|
||||
('abort_reason', 1),
|
||||
('num_antenna_paths', 1),
|
||||
[
|
||||
('step_mode', 1),
|
||||
('step_channel', 1),
|
||||
('step_data', 'v'),
|
||||
],
|
||||
]
|
||||
)
|
||||
class HCI_LE_CS_Subevent_Result_Event(HCI_LE_Meta_Event):
|
||||
'''
|
||||
See Bluetooth spec @ 7.7.65.44 LE CS Subevent Result event
|
||||
'''
|
||||
|
||||
status: int
|
||||
config_id: int
|
||||
start_acl_conn_event_counter: int
|
||||
procedure_counter: int
|
||||
frequency_compensation: int
|
||||
reference_power_level: int
|
||||
procedure_done_status: int
|
||||
subevent_done_status: int
|
||||
abort_reason: int
|
||||
num_antenna_paths: int
|
||||
step_mode: list[int]
|
||||
step_channel: list[int]
|
||||
step_data: list[bytes]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_LE_Meta_Event.event(
|
||||
[
|
||||
('connection_handle', 2),
|
||||
('config_id', 1),
|
||||
('procedure_done_status', 1),
|
||||
('subevent_done_status', 1),
|
||||
('abort_reason', 1),
|
||||
('num_antenna_paths', 1),
|
||||
[
|
||||
('step_mode', 1),
|
||||
('step_channel', 1),
|
||||
('step_data', 'v'),
|
||||
],
|
||||
]
|
||||
)
|
||||
class HCI_LE_CS_Subevent_Result_Continue_Event(HCI_LE_Meta_Event):
|
||||
'''
|
||||
See Bluetooth spec @ 7.7.65.45 LE CS Subevent Result Continue event
|
||||
'''
|
||||
|
||||
status: int
|
||||
config_id: int
|
||||
procedure_done_status: int
|
||||
subevent_done_status: int
|
||||
abort_reason: int
|
||||
num_antenna_paths: int
|
||||
step_mode: list[int]
|
||||
step_channel: list[int]
|
||||
step_data: list[bytes]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_LE_Meta_Event.event(
|
||||
[
|
||||
('connection_handle', 2),
|
||||
('status', STATUS_SPEC),
|
||||
]
|
||||
)
|
||||
class HCI_LE_CS_Test_End_Complete_Event(HCI_LE_Meta_Event):
|
||||
'''
|
||||
See Bluetooth spec @ 7.7.65.46 LE CS Test End Complete event
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Event.event([('status', STATUS_SPEC)])
|
||||
class HCI_Inquiry_Complete_Event(HCI_Event):
|
||||
@@ -6955,7 +7571,7 @@ class HCI_IsoDataPacket(HCI_Packet):
|
||||
if should_include_sdu_info:
|
||||
packet_sequence_number, sdu_info = struct.unpack_from('<HH', packet, pos)
|
||||
iso_sdu_length = sdu_info & 0xFFF
|
||||
packet_status_flag = sdu_info >> 14
|
||||
packet_status_flag = (sdu_info >> 15) & 1
|
||||
pos += 4
|
||||
|
||||
iso_sdu_fragment = packet[pos:]
|
||||
@@ -6989,7 +7605,7 @@ class HCI_IsoDataPacket(HCI_Packet):
|
||||
fmt += 'HH'
|
||||
args += [
|
||||
self.packet_sequence_number,
|
||||
self.iso_sdu_length | self.packet_status_flag << 14,
|
||||
self.iso_sdu_length | self.packet_status_flag << 15,
|
||||
]
|
||||
return struct.pack(fmt, *args) + self.iso_sdu_fragment
|
||||
|
||||
@@ -6997,9 +7613,10 @@ class HCI_IsoDataPacket(HCI_Packet):
|
||||
return (
|
||||
f'{color("ISO", "blue")}: '
|
||||
f'handle=0x{self.connection_handle:04x}, '
|
||||
f'pb={self.pb_flag}, '
|
||||
f'ps={self.packet_status_flag}, '
|
||||
f'data_total_length={self.data_total_length}, '
|
||||
f'sdu={self.iso_sdu_fragment.hex()}'
|
||||
f'sdu_fragment={self.iso_sdu_fragment.hex()}'
|
||||
)
|
||||
|
||||
|
||||
|
||||
+270
-71
@@ -1,4 +1,4 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
# Copyright 2021-2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -21,7 +21,6 @@ import collections
|
||||
import dataclasses
|
||||
import logging
|
||||
import struct
|
||||
import itertools
|
||||
|
||||
from typing import (
|
||||
Any,
|
||||
@@ -35,6 +34,8 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
import pyee
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.l2cap import L2CAP_PDU
|
||||
from bumble.snoop import Snooper
|
||||
@@ -60,7 +61,19 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class AclPacketQueue:
|
||||
class DataPacketQueue(pyee.EventEmitter):
|
||||
"""
|
||||
Flow-control queue for host->controller data packets (ACL, ISO).
|
||||
|
||||
The queue holds packets associated with a connection handle. The packets
|
||||
are sent to the controller, up to a maximum total number of packets in flight.
|
||||
A packet is considered to be "in flight" when it has been sent to the controller
|
||||
but not completed yet. Packets are no longer "in flight" when the controller
|
||||
declares them as completed.
|
||||
|
||||
The queue emits a 'flow' event whenever one or more packets are completed.
|
||||
"""
|
||||
|
||||
max_packet_size: int
|
||||
|
||||
def __init__(
|
||||
@@ -69,40 +82,105 @@ class AclPacketQueue:
|
||||
max_in_flight: int,
|
||||
send: Callable[[hci.HCI_Packet], None],
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.max_packet_size = max_packet_size
|
||||
self.max_in_flight = max_in_flight
|
||||
self.in_flight = 0
|
||||
self.send = send
|
||||
self.packets: Deque[hci.HCI_AclDataPacket] = collections.deque()
|
||||
self._in_flight = 0 # Total number of packets in flight across all connections
|
||||
self._in_flight_per_connection: dict[int, int] = collections.defaultdict(
|
||||
int
|
||||
) # Number of packets in flight per connection
|
||||
self._send = send
|
||||
self._packets: Deque[tuple[hci.HCI_Packet, int]] = collections.deque()
|
||||
self._queued = 0
|
||||
self._completed = 0
|
||||
|
||||
def enqueue(self, packet: hci.HCI_AclDataPacket) -> None:
|
||||
self.packets.appendleft(packet)
|
||||
self.check_queue()
|
||||
@property
|
||||
def queued(self) -> int:
|
||||
"""Total number of packets queued since creation."""
|
||||
return self._queued
|
||||
|
||||
if self.packets:
|
||||
@property
|
||||
def completed(self) -> int:
|
||||
"""Total number of packets completed since creation."""
|
||||
return self._completed
|
||||
|
||||
@property
|
||||
def pending(self) -> int:
|
||||
"""Number of packets that have been queued but not completed."""
|
||||
return self._queued - self._completed
|
||||
|
||||
def enqueue(self, packet: hci.HCI_Packet, connection_handle: int) -> None:
|
||||
"""Enqueue a packet associated with a connection"""
|
||||
self._packets.appendleft((packet, connection_handle))
|
||||
self._queued += 1
|
||||
self._check_queue()
|
||||
|
||||
if self._packets:
|
||||
logger.debug(
|
||||
f'{self.in_flight} ACL packets in flight, '
|
||||
f'{len(self.packets)} in queue'
|
||||
f'{self._in_flight} packets in flight, '
|
||||
f'{len(self._packets)} in queue'
|
||||
)
|
||||
|
||||
def check_queue(self) -> None:
|
||||
while self.packets and self.in_flight < self.max_in_flight:
|
||||
packet = self.packets.pop()
|
||||
self.send(packet)
|
||||
self.in_flight += 1
|
||||
def flush(self, connection_handle: int) -> None:
|
||||
"""
|
||||
Remove all packets associated with a connection.
|
||||
|
||||
def on_packets_completed(self, packet_count: int) -> None:
|
||||
if packet_count > self.in_flight:
|
||||
All packets associated with the connection that are in flight are implicitly
|
||||
marked as completed, but no 'flow' event is emitted.
|
||||
"""
|
||||
|
||||
packets_to_keep = [
|
||||
(packet, handle)
|
||||
for (packet, handle) in self._packets
|
||||
if handle != connection_handle
|
||||
]
|
||||
if flushed_count := len(self._packets) - len(packets_to_keep):
|
||||
self._completed += flushed_count
|
||||
self._packets = collections.deque(packets_to_keep)
|
||||
|
||||
if connection_handle in self._in_flight_per_connection:
|
||||
in_flight = self._in_flight_per_connection[connection_handle]
|
||||
self._completed += in_flight
|
||||
self._in_flight -= in_flight
|
||||
del self._in_flight_per_connection[connection_handle]
|
||||
|
||||
def _check_queue(self) -> None:
|
||||
while self._packets and self._in_flight < self.max_in_flight:
|
||||
packet, connection_handle = self._packets.pop()
|
||||
self._send(packet)
|
||||
self._in_flight += 1
|
||||
self._in_flight_per_connection[connection_handle] += 1
|
||||
|
||||
def on_packets_completed(self, packet_count: int, connection_handle: int) -> None:
|
||||
"""Mark one or more packets associated with a connection as completed."""
|
||||
if connection_handle not in self._in_flight_per_connection:
|
||||
logger.warning(
|
||||
color(
|
||||
'!!! {packet_count} completed but only '
|
||||
f'{self.in_flight} in flight'
|
||||
)
|
||||
f'received completion for unknown connection {connection_handle}'
|
||||
)
|
||||
packet_count = self.in_flight
|
||||
return
|
||||
|
||||
self.in_flight -= packet_count
|
||||
self.check_queue()
|
||||
in_flight_for_connection = self._in_flight_per_connection[connection_handle]
|
||||
if packet_count <= in_flight_for_connection:
|
||||
self._in_flight_per_connection[connection_handle] -= packet_count
|
||||
else:
|
||||
logger.warning(
|
||||
f'{packet_count} completed for {connection_handle} '
|
||||
f'but only {in_flight_for_connection} in flight'
|
||||
)
|
||||
self._in_flight_per_connection[connection_handle] = 0
|
||||
|
||||
if packet_count <= self._in_flight:
|
||||
self._in_flight -= packet_count
|
||||
self._completed += packet_count
|
||||
else:
|
||||
logger.warning(
|
||||
f'{packet_count} completed but only {self._in_flight} in flight'
|
||||
)
|
||||
self._in_flight = 0
|
||||
self._completed = self._queued
|
||||
|
||||
self._check_queue()
|
||||
self.emit('flow')
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -115,7 +193,7 @@ class Connection:
|
||||
self.peer_address = peer_address
|
||||
self.assembler = hci.HCI_AclDataPacketAssembler(self.on_acl_pdu)
|
||||
self.transport = transport
|
||||
acl_packet_queue: Optional[AclPacketQueue] = (
|
||||
acl_packet_queue: Optional[DataPacketQueue] = (
|
||||
host.le_acl_packet_queue
|
||||
if transport == BT_LE_TRANSPORT
|
||||
else host.acl_packet_queue
|
||||
@@ -130,29 +208,37 @@ class Connection:
|
||||
l2cap_pdu = L2CAP_PDU.from_bytes(pdu)
|
||||
self.host.on_l2cap_pdu(self, l2cap_pdu.cid, l2cap_pdu.payload)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f'Connection(transport={self.transport}, peer_address={self.peer_address})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclasses.dataclass
|
||||
class ScoLink:
|
||||
peer_address: hci.Address
|
||||
handle: int
|
||||
connection_handle: int
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclasses.dataclass
|
||||
class CisLink:
|
||||
peer_address: hci.Address
|
||||
class IsoLink:
|
||||
handle: int
|
||||
packet_queue: DataPacketQueue = dataclasses.field(repr=False)
|
||||
packet_sequence_number: int = 0
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Host(AbortableEventEmitter):
|
||||
connections: Dict[int, Connection]
|
||||
cis_links: Dict[int, CisLink]
|
||||
cis_links: Dict[int, IsoLink]
|
||||
bis_links: Dict[int, IsoLink]
|
||||
sco_links: Dict[int, ScoLink]
|
||||
bigs: dict[int, set[int]] = {} # BIG Handle to BIS Handles
|
||||
acl_packet_queue: Optional[AclPacketQueue] = None
|
||||
le_acl_packet_queue: Optional[AclPacketQueue] = None
|
||||
acl_packet_queue: Optional[DataPacketQueue] = None
|
||||
le_acl_packet_queue: Optional[DataPacketQueue] = None
|
||||
iso_packet_queue: Optional[DataPacketQueue] = None
|
||||
hci_sink: Optional[TransportSink] = None
|
||||
hci_metadata: Dict[str, Any]
|
||||
long_term_key_provider: Optional[
|
||||
@@ -171,6 +257,7 @@ class Host(AbortableEventEmitter):
|
||||
self.ready = False # True when we can accept incoming packets
|
||||
self.connections = {} # Connections, by connection handle
|
||||
self.cis_links = {} # CIS links, by connection handle
|
||||
self.bis_links = {} # BIS links, by connection handle
|
||||
self.sco_links = {} # SCO links, by connection handle
|
||||
self.pending_command = None
|
||||
self.pending_response: Optional[asyncio.Future[Any]] = None
|
||||
@@ -413,39 +500,70 @@ class Host(AbortableEventEmitter):
|
||||
f'hc_total_num_acl_data_packets={hc_total_num_acl_data_packets}'
|
||||
)
|
||||
|
||||
self.acl_packet_queue = AclPacketQueue(
|
||||
self.acl_packet_queue = DataPacketQueue(
|
||||
max_packet_size=hc_acl_data_packet_length,
|
||||
max_in_flight=hc_total_num_acl_data_packets,
|
||||
send=self.send_hci_packet,
|
||||
)
|
||||
|
||||
hc_le_acl_data_packet_length = 0
|
||||
hc_total_num_le_acl_data_packets = 0
|
||||
if self.supports_command(hci.HCI_LE_READ_BUFFER_SIZE_COMMAND):
|
||||
le_acl_data_packet_length = 0
|
||||
total_num_le_acl_data_packets = 0
|
||||
iso_data_packet_length = 0
|
||||
total_num_iso_data_packets = 0
|
||||
if self.supports_command(hci.HCI_LE_READ_BUFFER_SIZE_V2_COMMAND):
|
||||
response = await self.send_command(
|
||||
hci.HCI_LE_Read_Buffer_Size_V2_Command(), check_result=True
|
||||
)
|
||||
le_acl_data_packet_length = (
|
||||
response.return_parameters.le_acl_data_packet_length
|
||||
)
|
||||
total_num_le_acl_data_packets = (
|
||||
response.return_parameters.total_num_le_acl_data_packets
|
||||
)
|
||||
iso_data_packet_length = response.return_parameters.iso_data_packet_length
|
||||
total_num_iso_data_packets = (
|
||||
response.return_parameters.total_num_iso_data_packets
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
'HCI LE flow control: '
|
||||
f'le_acl_data_packet_length={le_acl_data_packet_length},'
|
||||
f'total_num_le_acl_data_packets={total_num_le_acl_data_packets}'
|
||||
f'iso_data_packet_length={iso_data_packet_length},'
|
||||
f'total_num_iso_data_packets={total_num_iso_data_packets}'
|
||||
)
|
||||
elif self.supports_command(hci.HCI_LE_READ_BUFFER_SIZE_COMMAND):
|
||||
response = await self.send_command(
|
||||
hci.HCI_LE_Read_Buffer_Size_Command(), check_result=True
|
||||
)
|
||||
hc_le_acl_data_packet_length = (
|
||||
response.return_parameters.hc_le_acl_data_packet_length
|
||||
le_acl_data_packet_length = (
|
||||
response.return_parameters.le_acl_data_packet_length
|
||||
)
|
||||
hc_total_num_le_acl_data_packets = (
|
||||
response.return_parameters.hc_total_num_le_acl_data_packets
|
||||
total_num_le_acl_data_packets = (
|
||||
response.return_parameters.total_num_le_acl_data_packets
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
'HCI LE ACL flow control: '
|
||||
f'hc_le_acl_data_packet_length={hc_le_acl_data_packet_length},'
|
||||
f'hc_total_num_le_acl_data_packets={hc_total_num_le_acl_data_packets}'
|
||||
f'le_acl_data_packet_length={le_acl_data_packet_length},'
|
||||
f'total_num_le_acl_data_packets={total_num_le_acl_data_packets}'
|
||||
)
|
||||
|
||||
if hc_le_acl_data_packet_length == 0 or hc_total_num_le_acl_data_packets == 0:
|
||||
if le_acl_data_packet_length == 0 or total_num_le_acl_data_packets == 0:
|
||||
# LE and Classic share the same queue
|
||||
self.le_acl_packet_queue = self.acl_packet_queue
|
||||
else:
|
||||
# Create a separate queue for LE
|
||||
self.le_acl_packet_queue = AclPacketQueue(
|
||||
max_packet_size=hc_le_acl_data_packet_length,
|
||||
max_in_flight=hc_total_num_le_acl_data_packets,
|
||||
self.le_acl_packet_queue = DataPacketQueue(
|
||||
max_packet_size=le_acl_data_packet_length,
|
||||
max_in_flight=total_num_le_acl_data_packets,
|
||||
send=self.send_hci_packet,
|
||||
)
|
||||
|
||||
if iso_data_packet_length and total_num_iso_data_packets:
|
||||
self.iso_packet_queue = DataPacketQueue(
|
||||
max_packet_size=iso_data_packet_length,
|
||||
max_in_flight=total_num_iso_data_packets,
|
||||
send=self.send_hci_packet,
|
||||
)
|
||||
|
||||
@@ -597,11 +715,78 @@ class Host(AbortableEventEmitter):
|
||||
data=l2cap_pdu[offset : offset + data_total_length],
|
||||
)
|
||||
logger.debug(f'>>> ACL packet enqueue: (CID={cid}) {acl_packet}')
|
||||
packet_queue.enqueue(acl_packet)
|
||||
packet_queue.enqueue(acl_packet, connection_handle)
|
||||
pb_flag = 1
|
||||
offset += data_total_length
|
||||
bytes_remaining -= data_total_length
|
||||
|
||||
def get_data_packet_queue(self, connection_handle: int) -> DataPacketQueue | None:
|
||||
if connection := self.connections.get(connection_handle):
|
||||
return connection.acl_packet_queue
|
||||
|
||||
if iso_link := self.cis_links.get(connection_handle) or self.bis_links.get(
|
||||
connection_handle
|
||||
):
|
||||
return iso_link.packet_queue
|
||||
|
||||
return None
|
||||
|
||||
def send_iso_sdu(self, connection_handle: int, sdu: bytes) -> None:
|
||||
if not (
|
||||
iso_link := self.cis_links.get(connection_handle)
|
||||
or self.bis_links.get(connection_handle)
|
||||
):
|
||||
logger.warning(f"no ISO link for connection handle {connection_handle}")
|
||||
return
|
||||
|
||||
if iso_link.packet_queue is None:
|
||||
logger.warning("ISO link has no data packet queue")
|
||||
return
|
||||
|
||||
bytes_remaining = len(sdu)
|
||||
offset = 0
|
||||
while bytes_remaining:
|
||||
is_first_fragment = offset == 0
|
||||
header_length = 4 if is_first_fragment else 0
|
||||
assert iso_link.packet_queue.max_packet_size > header_length
|
||||
fragment_length = min(
|
||||
bytes_remaining, iso_link.packet_queue.max_packet_size - header_length
|
||||
)
|
||||
is_last_fragment = bytes_remaining == fragment_length
|
||||
iso_sdu_fragment = sdu[offset : offset + fragment_length]
|
||||
iso_link.packet_queue.enqueue(
|
||||
(
|
||||
hci.HCI_IsoDataPacket(
|
||||
connection_handle=connection_handle,
|
||||
data_total_length=header_length + fragment_length,
|
||||
packet_sequence_number=iso_link.packet_sequence_number,
|
||||
pb_flag=0b10 if is_last_fragment else 0b00,
|
||||
packet_status_flag=0,
|
||||
iso_sdu_length=len(sdu),
|
||||
iso_sdu_fragment=iso_sdu_fragment,
|
||||
)
|
||||
if is_first_fragment
|
||||
else hci.HCI_IsoDataPacket(
|
||||
connection_handle=connection_handle,
|
||||
data_total_length=fragment_length,
|
||||
pb_flag=0b11 if is_last_fragment else 0b01,
|
||||
iso_sdu_fragment=iso_sdu_fragment,
|
||||
)
|
||||
),
|
||||
connection_handle,
|
||||
)
|
||||
|
||||
offset += fragment_length
|
||||
bytes_remaining -= fragment_length
|
||||
|
||||
iso_link.packet_sequence_number = (iso_link.packet_sequence_number + 1) & 0xFFFF
|
||||
|
||||
def remove_big(self, big_handle: int) -> None:
|
||||
if big := self.bigs.pop(big_handle, None):
|
||||
for connection_handle in big:
|
||||
if bis_link := self.bis_links.pop(connection_handle, None):
|
||||
bis_link.packet_queue.flush(bis_link.handle)
|
||||
|
||||
def supports_command(self, op_code: int) -> bool:
|
||||
return (
|
||||
self.local_supported_commands
|
||||
@@ -729,17 +914,17 @@ class Host(AbortableEventEmitter):
|
||||
def on_hci_command_status_event(self, event):
|
||||
return self.on_command_processed(event)
|
||||
|
||||
def on_hci_number_of_completed_packets_event(self, event):
|
||||
def on_hci_number_of_completed_packets_event(
|
||||
self, event: hci.HCI_Number_Of_Completed_Packets_Event
|
||||
) -> None:
|
||||
for connection_handle, num_completed_packets in zip(
|
||||
event.connection_handles, event.num_completed_packets
|
||||
):
|
||||
if connection := self.connections.get(connection_handle):
|
||||
connection.acl_packet_queue.on_packets_completed(num_completed_packets)
|
||||
elif connection_handle not in itertools.chain(
|
||||
self.cis_links.keys(),
|
||||
self.sco_links.keys(),
|
||||
itertools.chain.from_iterable(self.bigs.values()),
|
||||
):
|
||||
if queue := self.get_data_packet_queue(connection_handle):
|
||||
queue.on_packets_completed(num_completed_packets, connection_handle)
|
||||
continue
|
||||
|
||||
if connection_handle not in self.sco_links:
|
||||
logger.warning(
|
||||
'received packet completion event for unknown handle '
|
||||
f'0x{connection_handle:04X}'
|
||||
@@ -857,11 +1042,7 @@ class Host(AbortableEventEmitter):
|
||||
return
|
||||
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
logger.debug(
|
||||
f'### DISCONNECTION: [0x{handle:04X}] '
|
||||
f'{connection.peer_address} '
|
||||
f'reason={event.reason}'
|
||||
)
|
||||
logger.debug(f'### DISCONNECTION: {connection}, reason={event.reason}')
|
||||
|
||||
# Notify the listeners
|
||||
self.emit('disconnection', handle, event.reason)
|
||||
@@ -872,6 +1053,12 @@ class Host(AbortableEventEmitter):
|
||||
or self.cis_links.pop(handle, 0)
|
||||
or self.sco_links.pop(handle, 0)
|
||||
)
|
||||
|
||||
# Flush the data queues
|
||||
self.acl_packet_queue.flush(handle)
|
||||
self.le_acl_packet_queue.flush(handle)
|
||||
if self.iso_packet_queue:
|
||||
self.iso_packet_queue.flush(handle)
|
||||
else:
|
||||
logger.debug(f'### DISCONNECTION FAILED: {event.status}')
|
||||
|
||||
@@ -958,6 +1145,14 @@ class Host(AbortableEventEmitter):
|
||||
|
||||
def on_hci_le_create_big_complete_event(self, event):
|
||||
self.bigs[event.big_handle] = set(event.connection_handle)
|
||||
if self.iso_packet_queue is None:
|
||||
logger.warning("BIS established but ISO packets not supported")
|
||||
|
||||
for connection_handle in event.connection_handle:
|
||||
self.bis_links[connection_handle] = IsoLink(
|
||||
connection_handle, self.iso_packet_queue
|
||||
)
|
||||
|
||||
self.emit(
|
||||
'big_establishment',
|
||||
event.status,
|
||||
@@ -975,6 +1170,12 @@ class Host(AbortableEventEmitter):
|
||||
)
|
||||
|
||||
def on_hci_le_big_sync_established_event(self, event):
|
||||
self.bigs[event.big_handle] = set(event.connection_handle)
|
||||
for connection_handle in event.connection_handle:
|
||||
self.bis_links[connection_handle] = IsoLink(
|
||||
connection_handle, self.iso_packet_queue
|
||||
)
|
||||
|
||||
self.emit(
|
||||
'big_sync_establishment',
|
||||
event.status,
|
||||
@@ -990,22 +1191,20 @@ class Host(AbortableEventEmitter):
|
||||
)
|
||||
|
||||
def on_hci_le_big_sync_lost_event(self, event):
|
||||
self.emit(
|
||||
'big_sync_lost',
|
||||
event.big_handle,
|
||||
event.reason,
|
||||
)
|
||||
self.remove_big(event.big_handle)
|
||||
self.emit('big_sync_lost', event.big_handle, event.reason)
|
||||
|
||||
def on_hci_le_terminate_big_complete_event(self, event):
|
||||
self.bigs.pop(event.big_handle)
|
||||
self.remove_big(event.big_handle)
|
||||
self.emit('big_termination', event.reason, event.big_handle)
|
||||
|
||||
def on_hci_le_cis_established_event(self, event):
|
||||
# The remaining parameters are unused for now.
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
self.cis_links[event.connection_handle] = CisLink(
|
||||
handle=event.connection_handle,
|
||||
peer_address=hci.Address.ANY,
|
||||
if self.iso_packet_queue is None:
|
||||
logger.warning("CIS established but ISO packets not supported")
|
||||
self.cis_links[event.connection_handle] = IsoLink(
|
||||
handle=event.connection_handle, packet_queue=self.iso_packet_queue
|
||||
)
|
||||
self.emit('cis_establishment', event.connection_handle)
|
||||
else:
|
||||
@@ -1075,7 +1274,7 @@ class Host(AbortableEventEmitter):
|
||||
|
||||
self.sco_links[event.connection_handle] = ScoLink(
|
||||
peer_address=event.bd_addr,
|
||||
handle=event.connection_handle,
|
||||
connection_handle=event.connection_handle,
|
||||
)
|
||||
|
||||
# Notify the client
|
||||
|
||||
@@ -34,7 +34,7 @@ from bumble.device import (
|
||||
Device,
|
||||
PeriodicAdvertisingParameters,
|
||||
)
|
||||
from bumble.host import AclPacketQueue, Host
|
||||
from bumble.host import DataPacketQueue, Host
|
||||
from bumble.hci import (
|
||||
HCI_ACCEPT_CONNECTION_REQUEST_COMMAND,
|
||||
HCI_COMMAND_STATUS_PENDING,
|
||||
@@ -90,9 +90,9 @@ async def test_device_connect_parallel():
|
||||
def _send(packet):
|
||||
pass
|
||||
|
||||
d0.host.acl_packet_queue = AclPacketQueue(0, 0, _send)
|
||||
d1.host.acl_packet_queue = AclPacketQueue(0, 0, _send)
|
||||
d2.host.acl_packet_queue = AclPacketQueue(0, 0, _send)
|
||||
d0.host.acl_packet_queue = DataPacketQueue(0, 0, _send)
|
||||
d1.host.acl_packet_queue = DataPacketQueue(0, 0, _send)
|
||||
d2.host.acl_packet_queue = DataPacketQueue(0, 0, _send)
|
||||
|
||||
# enable classic
|
||||
d0.classic_enabled = True
|
||||
|
||||
+2
-2
@@ -170,8 +170,8 @@ def test_HCI_Command_Complete_Event():
|
||||
command_opcode=HCI_LE_READ_BUFFER_SIZE_COMMAND,
|
||||
return_parameters=HCI_LE_Read_Buffer_Size_Command.create_return_parameters(
|
||||
status=0,
|
||||
hc_le_acl_data_packet_length=1234,
|
||||
hc_total_num_le_acl_data_packets=56,
|
||||
le_acl_data_packet_length=1234,
|
||||
total_num_le_acl_data_packets=56,
|
||||
),
|
||||
)
|
||||
basic_check(event)
|
||||
|
||||
+91
-1
@@ -16,11 +16,14 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import unittest.mock
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from bumble.controller import Controller
|
||||
from bumble.host import Host
|
||||
from bumble.host import Host, DataPacketQueue
|
||||
from bumble.transport import AsyncPipeSink
|
||||
from bumble.hci import HCI_AclDataPacket
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -60,3 +63,90 @@ async def test_reset(supported_commands: str, lmp_features: str):
|
||||
assert host.local_lmp_features == int.from_bytes(
|
||||
bytes.fromhex(lmp_features), 'little'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_data_packet_queue():
|
||||
controller = unittest.mock.Mock()
|
||||
queue = DataPacketQueue(10, 2, controller.send)
|
||||
assert queue.queued == 0
|
||||
assert queue.completed == 0
|
||||
packet = HCI_AclDataPacket(
|
||||
connection_handle=123, pb_flag=0, bc_flag=0, data_total_length=0, data=b''
|
||||
)
|
||||
|
||||
queue.enqueue(packet, packet.connection_handle)
|
||||
assert queue.queued == 1
|
||||
assert queue.completed == 0
|
||||
assert controller.send.call_count == 1
|
||||
|
||||
queue.enqueue(packet, packet.connection_handle)
|
||||
assert queue.queued == 2
|
||||
assert queue.completed == 0
|
||||
assert controller.send.call_count == 2
|
||||
|
||||
queue.enqueue(packet, packet.connection_handle)
|
||||
assert queue.queued == 3
|
||||
assert queue.completed == 0
|
||||
assert controller.send.call_count == 2
|
||||
|
||||
queue.on_packets_completed(1, 8000)
|
||||
assert queue.queued == 3
|
||||
assert queue.completed == 0
|
||||
assert controller.send.call_count == 2
|
||||
|
||||
queue.on_packets_completed(1, 123)
|
||||
assert queue.queued == 3
|
||||
assert queue.completed == 1
|
||||
assert controller.send.call_count == 3
|
||||
|
||||
queue.enqueue(packet, packet.connection_handle)
|
||||
assert queue.queued == 4
|
||||
assert queue.completed == 1
|
||||
assert controller.send.call_count == 3
|
||||
|
||||
queue.on_packets_completed(2, 123)
|
||||
assert queue.queued == 4
|
||||
assert queue.completed == 3
|
||||
assert controller.send.call_count == 4
|
||||
|
||||
queue.on_packets_completed(1, 123)
|
||||
assert queue.queued == 4
|
||||
assert queue.completed == 4
|
||||
assert controller.send.call_count == 4
|
||||
|
||||
queue.enqueue(packet, 123)
|
||||
queue.enqueue(packet, 123)
|
||||
queue.enqueue(packet, 123)
|
||||
queue.enqueue(packet, 124)
|
||||
queue.enqueue(packet, 124)
|
||||
queue.enqueue(packet, 124)
|
||||
queue.on_packets_completed(1, 123)
|
||||
assert queue.queued == 10
|
||||
assert queue.completed == 5
|
||||
queue.flush(123)
|
||||
queue.flush(124)
|
||||
assert queue.queued == 10
|
||||
assert queue.completed == 10
|
||||
|
||||
queue.enqueue(packet, 123)
|
||||
queue.on_packets_completed(1, 124)
|
||||
assert queue.queued == 11
|
||||
assert queue.completed == 10
|
||||
queue.on_packets_completed(1000, 123)
|
||||
assert queue.queued == 11
|
||||
assert queue.completed == 11
|
||||
|
||||
drain_listener = unittest.mock.Mock()
|
||||
queue.on('flow', drain_listener.on_flow)
|
||||
queue.enqueue(packet, 123)
|
||||
assert drain_listener.on_flow.call_count == 0
|
||||
queue.on_packets_completed(1, 123)
|
||||
assert drain_listener.on_flow.call_count == 1
|
||||
queue.enqueue(packet, 123)
|
||||
queue.enqueue(packet, 123)
|
||||
queue.enqueue(packet, 123)
|
||||
queue.flush(123)
|
||||
assert drain_listener.on_flow.call_count == 1
|
||||
assert queue.queued == 15
|
||||
assert queue.completed == 15
|
||||
|
||||
Reference in New Issue
Block a user