From 982aaeabc396557ff5df5e48f7019761ca96ed5d Mon Sep 17 00:00:00 2001 From: khsiao-google Date: Thu, 31 Jul 2025 02:52:42 +0000 Subject: [PATCH] Support LE Subrating --- bumble/controller.py | 50 ++++++++++++++++++++++++++++++++++++++++++++ bumble/device.py | 22 +++++++++++++++++++ bumble/hci.py | 47 +++++++++++++++++++++++++++++++++++++++++ bumble/host.py | 10 +++++++++ tests/device_test.py | 31 +++++++++++++++++++++++++++ 5 files changed, 160 insertions(+) diff --git a/bumble/controller.py b/bumble/controller.py index 356b7e4..8666481 100644 --- a/bumble/controller.py +++ b/bumble/controller.py @@ -1259,6 +1259,56 @@ class Controller: ) return bytes([HCI_SUCCESS]) + bd_addr + def on_hci_le_set_default_subrate_command( + self, command: hci.HCI_LE_Set_Default_Subrate_Command + ): + ''' + See Bluetooth spec Vol 6, Part E - 7.8.123 LE Set Event Mask Command + ''' + + if ( + command.subrate_max * (command.max_latency) > 500 + or command.subrate_max < command.subrate_min + or command.continuation_number >= command.subrate_max + ): + return bytes([HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR]) + + return bytes([HCI_SUCCESS]) + + def on_hci_le_subrate_request_command( + self, command: hci.HCI_LE_Subrate_Request_Command + ): + ''' + See Bluetooth spec Vol 6, Part E - 7.8.124 LE Subrate Request command + ''' + if ( + command.subrate_max * (command.max_latency) > 500 + or command.continuation_number < command.continuation_number + or command.subrate_max < command.subrate_min + or command.continuation_number >= command.subrate_max + ): + return bytes([HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR]) + + self.send_hci_packet( + hci.HCI_Command_Status_Event( + status=hci.HCI_SUCCESS, + num_hci_command_packets=1, + command_opcode=command.op_code, + ) + ) + + self.send_hci_packet( + hci.HCI_LE_Subrate_Change_Event( + status=hci.HCI_SUCCESS, + connection_handle=command.connection_handle, + subrate_factor=2, + peripheral_latency=2, + continuation_number=command.continuation_number, + supervision_timeout=command.supervision_timeout, + ) + ) + return None + def on_hci_le_set_event_mask_command(self, command): ''' See Bluetooth spec Vol 4, Part E - 7.8.1 LE Set Event Mask Command diff --git a/bumble/device.py b/bumble/device.py index ad03dac..73e6d3d 100644 --- a/bumble/device.py +++ b/bumble/device.py @@ -1752,6 +1752,8 @@ class Connection(utils.CompositeEventEmitter): EVENT_CIS_REQUEST = "cis_request" EVENT_CIS_ESTABLISHMENT = "cis_establishment" EVENT_CIS_ESTABLISHMENT_FAILURE = "cis_establishment_failure" + EVENT_LE_SUBRATE_CHANGE = "le_subrate_change" + EVENT_LE_SUBRATE_CHANGE_FAILURE = "le_subrate_change_failure" @utils.composite_listener class Listener: @@ -1787,6 +1789,8 @@ class Connection(utils.CompositeEventEmitter): connection_interval: float # Connection interval, in milliseconds. [LE only] peripheral_latency: int # Peripheral latency, in number of intervals. [LE only] supervision_timeout: float # Supervision timeout, in milliseconds. + subrate_factor: int = 1 + continuation_number: int = 0 def __init__( self, @@ -2058,6 +2062,7 @@ class DeviceConfiguration: le_simultaneous_enabled: bool = False le_privacy_enabled: bool = False le_rpa_timeout: int = DEVICE_DEFAULT_LE_RPA_TIMEOUT + le_subrate_enabled: bool = True classic_enabled: bool = False classic_sc_enabled: bool = True classic_ssp_enabled: bool = True @@ -2410,6 +2415,7 @@ class Device(utils.CompositeEventEmitter): self.le_privacy_enabled = config.le_privacy_enabled self.le_rpa_timeout = config.le_rpa_timeout self.le_rpa_periodic_update_task: Optional[asyncio.Task] = None + self.le_subrate_enabled = config.le_subrate_enabled self.classic_enabled = config.classic_enabled self.cis_enabled = config.cis_enabled self.classic_sc_enabled = config.classic_sc_enabled @@ -6226,6 +6232,22 @@ class Device(utils.CompositeEventEmitter): ) connection.emit(connection.EVENT_CONNECTION_PHY_UPDATE_FAILURE, error) + @host_event_handler + @with_connection_from_handle + def on_le_subrate_change( + self, + connection: Connection, + subrate_factor: int, + peripheral_latency: int, + continuation_number: int, + supervision_timeout: int, + ): + connection.parameters.subrate_factor = subrate_factor + connection.parameters.peripheral_latency = peripheral_latency + connection.parameters.continuation_number = continuation_number + connection.parameters.supervision_timeout = supervision_timeout * 10 + connection.emit(connection.EVENT_LE_SUBRATE_CHANGE) + @host_event_handler @with_connection_from_handle def on_connection_att_mtu_update(self, connection, att_mtu): diff --git a/bumble/hci.py b/bumble/hci.py index 4a6ae58..95da652 100644 --- a/bumble/hci.py +++ b/bumble/hci.py @@ -5315,6 +5315,37 @@ class HCI_LE_Set_Host_Feature_Command(HCI_Command): bit_value: int = field(metadata=metadata(1)) +# ----------------------------------------------------------------------------- +@HCI_Command.command +@dataclasses.dataclass +class HCI_LE_Set_Default_Subrate_Command(HCI_Command): + ''' + See Bluetooth spec @ 7.8.123 LE Set Default Subrate command + ''' + + subrate_min: int = field(metadata=metadata(2)) + subrate_max: int = field(metadata=metadata(2)) + max_latency: int = field(metadata=metadata(2)) + continuation_number: int = field(metadata=metadata(2)) + supervision_timeout: int = field(metadata=metadata(2)) + + +# ----------------------------------------------------------------------------- +@HCI_Command.command +@dataclasses.dataclass +class HCI_LE_Subrate_Request_Command(HCI_Command): + ''' + See Bluetooth spec @ 7.8.124 LE Subrate Request command + ''' + + connection_handle: int = field(metadata=metadata(2)) + subrate_min: int = field(metadata=metadata(2)) + subrate_max: int = field(metadata=metadata(2)) + max_latency: int = field(metadata=metadata(2)) + continuation_number: int = field(metadata=metadata(2)) + supervision_timeout: int = field(metadata=metadata(2)) + + # ----------------------------------------------------------------------------- @HCI_Command.command @dataclasses.dataclass @@ -6460,6 +6491,22 @@ class HCI_LE_BIGInfo_Advertising_Report_Event(HCI_LE_Meta_Event): encryption: int = field(metadata=metadata(1)) +# ----------------------------------------------------------------------------- +@HCI_LE_Meta_Event.event +@dataclasses.dataclass +class HCI_LE_Subrate_Change_Event(HCI_LE_Meta_Event): + ''' + See Bluetooth spec @ 7.7.65.35 LE Subrate Change event + ''' + + status: int = field(metadata=metadata(1)) + connection_handle: int = field(metadata=metadata(2)) + subrate_factor: int = field(metadata=metadata(2)) + peripheral_latency: int = field(metadata=metadata(2)) + continuation_number: int = field(metadata=metadata(2)) + supervision_timeout: int = field(metadata=metadata(2)) + + # ----------------------------------------------------------------------------- @HCI_LE_Meta_Event.event @dataclasses.dataclass diff --git a/bumble/host.py b/bumble/host.py index bfbe283..28cbe79 100644 --- a/bumble/host.py +++ b/bumble/host.py @@ -1645,5 +1645,15 @@ class Host(utils.EventEmitter): def on_hci_le_cs_subevent_result_continue_event(self, event): self.emit('cs_subevent_result_continue', event) + def on_hci_le_subrate_change_event(self, event: hci.HCI_LE_Subrate_Change_Event): + self.emit( + 'le_subrate_change', + event.connection_handle, + event.subrate_factor, + event.peripheral_latency, + event.continuation_number, + event.supervision_timeout, + ) + def on_hci_vendor_event(self, event): self.emit('vendor_event', event) diff --git a/tests/device_test.py b/tests/device_test.py index 8d0b5ad..2522bfe 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -611,6 +611,37 @@ async def test_enter_and_exit_sniff_mode(): assert devices.connections[0].classic_interval == 2 +# ----------------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_le_request_subrate(): + devices = TwoDevices() + await devices.setup_connection() + + q = asyncio.Queue() + + def on_le_subrate_change(): + q.put_nowait(lambda: None) + + devices.connections[0].on(Connection.EVENT_LE_SUBRATE_CHANGE, on_le_subrate_change) + + await devices[0].send_command( + hci.HCI_LE_Subrate_Request_Command( + connection_handle=devices.connections[0].handle, + subrate_min=2, + subrate_max=2, + max_latency=2, + continuation_number=1, + supervision_timeout=2, + ) + ) + + await asyncio.wait_for(q.get(), _TIMEOUT) + assert devices.connections[0].parameters.subrate_factor == 2 + assert devices.connections[0].parameters.peripheral_latency == 2 + assert devices.connections[0].parameters.continuation_number == 1 + assert devices.connections[0].parameters.supervision_timeout == 20 + + # ----------------------------------------------------------------------------- @pytest.mark.asyncio async def test_power_on_default_static_address_should_not_be_any():