Merge pull request #734 from khsiao-google/le_subrating

Support LE Subrating
This commit is contained in:
zxzxwu
2025-08-07 16:52:17 +08:00
committed by GitHub
5 changed files with 201 additions and 5 deletions

View File

@@ -1269,6 +1269,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
@@ -1815,3 +1865,11 @@ class Controller:
See Bluetooth spec Vol 4, Part E - 7.8.110 LE Remove ISO Data Path Command
'''
return struct.pack('<BH', HCI_SUCCESS, command.connection_handle)
def on_hci_le_set_host_feature_command(
self, _command: hci.HCI_LE_Set_Host_Feature_Command
):
'''
See Bluetooth spec Vol 4, Part E - 7.8.115 LE Set Host Feature command
'''
return bytes([HCI_SUCCESS])

View File

@@ -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,12 @@ 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 # See Bluetooth spec Vol 6, Part B - 4.5.1 Connection events
)
continuation_number: int = (
0 # See Bluetooth spec Vol 6, Part B - 4.5.1 Connection events
)
def __init__(
self,
@@ -2058,6 +2066,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 = False
classic_enabled: bool = False
classic_sc_enabled: bool = True
classic_ssp_enabled: bool = True
@@ -2410,6 +2419,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
@@ -2789,6 +2799,15 @@ class Device(utils.CompositeEventEmitter):
check_result=True,
)
if self.le_subrate_enabled:
await self.send_command(
hci.HCI_LE_Set_Host_Feature_Command(
bit_number=hci.LeFeature.CONNECTION_SUBRATING_HOST_SUPPORT,
bit_value=1,
),
check_result=True,
)
if self.config.channel_sounding_enabled:
await self.send_command(
hci.HCI_LE_Set_Host_Feature_Command(
@@ -6189,11 +6208,23 @@ class Device(utils.CompositeEventEmitter):
f'{connection.peer_address} as {connection.role_name}, '
f'{connection_parameters}'
)
connection.parameters = Connection.Parameters(
connection_parameters.connection_interval * 1.25,
connection_parameters.peripheral_latency,
connection_parameters.supervision_timeout * 10.0,
)
if (
connection.parameters.connection_interval
!= connection_parameters.connection_interval * 1.25
):
connection.parameters = Connection.Parameters(
connection_parameters.connection_interval * 1.25,
connection_parameters.peripheral_latency,
connection_parameters.supervision_timeout * 10.0,
)
else:
connection.parameters = Connection.Parameters(
connection_parameters.connection_interval * 1.25,
connection_parameters.peripheral_latency,
connection_parameters.supervision_timeout * 10.0,
connection.parameters.subrate_factor,
connection.parameters.continuation_number,
)
connection.emit(connection.EVENT_CONNECTION_PARAMETERS_UPDATE)
@host_event_handler
@@ -6226,6 +6257,25 @@ 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 = Connection.Parameters(
connection.parameters.connection_interval,
peripheral_latency,
supervision_timeout * 10.0,
subrate_factor,
continuation_number,
)
connection.emit(connection.EVENT_LE_SUBRATE_CHANGE)
@host_event_handler
@with_connection_from_handle
def on_connection_att_mtu_update(self, connection, att_mtu):

View File

@@ -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(STATUS_SPEC))
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

View File

@@ -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)

View File

@@ -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():