diff --git a/bumble/device.py b/bumble/device.py index 7eb5dc15..80690f7a 100644 --- a/bumble/device.py +++ b/bumble/device.py @@ -1189,6 +1189,15 @@ class ChannelSoundingCapabilities: t_pm_times_supported: int t_sw_time_supported: int tx_snr_capability: int + # Bluetooth 6.3 [v2] additions from LE CS Read Local Supported + # Capabilities V2 (opcode 0x20A5). Populated when the local controller + # supports the V2 command; zero otherwise. `cs_ipt_reflector_supported` + # is a derived boolean from `subfeatures_supported` bit 4 (the + # CS_IPT_REFLECTOR subfeature mask per Bluetooth Core Spec / Nordic + # hci_types.h:3934-3935). + t_ip2_ipt_times_supported: int = 0 + t_sw_ipt_time_supported: int = 0 + cs_ipt_reflector_supported: bool = False # ----------------------------------------------------------------------------- @@ -2961,9 +2970,37 @@ async def power_on(self) -> None: bit_value=1, ) ) - result = await self.send_sync_command( - hci.HCI_LE_CS_Read_Local_Supported_Capabilities_Command() + # Prefer the Bluetooth 6.3 V2 caps read (opcode 0x20A5) which + # appends T_IP2_IPT_TIMES and T_SW_IPT_TIME after tx_snr_capability. + # supports_command() would be the clean gate, but Nordic hci_uart + # firmware doesn't always set the LE Supported Commands bit for + # V2 even when the command works — so we blind-call V2 and + # transparently fall back to 6.0 on UNKNOWN_HCI_COMMAND. That + # keeps 6.0-only controllers working unchanged while extracting + # IPT timings whenever the controller actually implements V2. + # `result` is one of two return-parameter shapes; annotate the + # union so the V2 read and the 6.0 fallback both type-check, and + # read the renamed rtt_random field in-branch where the concrete + # type is known. + result: ( + hci.HCI_LE_CS_Read_Local_Supported_Capabilities_V2_ReturnParameters + | hci.HCI_LE_CS_Read_Local_Supported_Capabilities_ReturnParameters ) + try: + v2 = await self.send_sync_command( + hci.HCI_LE_CS_Read_Local_Supported_Capabilities_V2_Command() + ) + result = v2 + # V2 renamed rtt_random_sequence_n → rtt_random_payload_n. + rtt_random = v2.rtt_random_payload_n + except hci.HCI_Error as e: + if e.error_code != hci.HCI_UNKNOWN_HCI_COMMAND_ERROR: + raise + v1 = await self.send_sync_command( + hci.HCI_LE_CS_Read_Local_Supported_Capabilities_Command() + ) + result = v1 + rtt_random = v1.rtt_random_sequence_n self.cs_capabilities = ChannelSoundingCapabilities( num_config_supported=result.num_config_supported, max_consecutive_procedures_supported=result.max_consecutive_procedures_supported, @@ -2974,7 +3011,7 @@ async def power_on(self) -> None: rtt_capability=result.rtt_capability, rtt_aa_only_n=result.rtt_aa_only_n, rtt_sounding_n=result.rtt_sounding_n, - rtt_random_sequence_n=result.rtt_random_sequence_n, + rtt_random_sequence_n=rtt_random, nadm_sounding_capability=result.nadm_sounding_capability, nadm_random_capability=result.nadm_random_capability, cs_sync_phys_supported=result.cs_sync_phys_supported, @@ -2985,6 +3022,15 @@ async def power_on(self) -> None: t_pm_times_supported=result.t_pm_times_supported, t_sw_time_supported=result.t_sw_time_supported, tx_snr_capability=result.tx_snr_capability, + t_ip2_ipt_times_supported=getattr( + result, 't_ip2_ipt_times_supported', 0 + ), + t_sw_ipt_time_supported=getattr( + result, 't_sw_ipt_time_supported', 0 + ), + cs_ipt_reflector_supported=bool( + result.subfeatures_supported & hci.CsSubfeature.CS_IPT_REFLECTOR + ), ) if ( @@ -5448,6 +5494,7 @@ async def create_cs_config( channel_selection_type: int = hci.HCI_LE_CS_Create_Config_Command.ChannelSelectionType.ALGO_3B, ch3c_shape: int = hci.HCI_LE_CS_Create_Config_Command.Ch3cShape.HAT, ch3c_jump: int = 0x03, + cs_enhancements_1: int = 0x00, ) -> ChannelSoundingConfig: complete_future: asyncio.Future[ChannelSoundingConfig] = ( asyncio.get_running_loop().create_future() @@ -5493,7 +5540,7 @@ async def create_cs_config( channel_selection_type=channel_selection_type, ch3c_shape=ch3c_shape, ch3c_jump=ch3c_jump, - reserved=0x00, + cs_enhancements_1=cs_enhancements_1, ) ) return await complete_future @@ -6826,6 +6873,19 @@ def on_connection_data_length_change( def on_cs_remote_supported_capabilities( self, event: hci.HCI_LE_CS_Read_Remote_Supported_Capabilities_Complete_Event ): + self._on_cs_remote_supported_capabilities_impl(event) + + @host_event_handler + def on_cs_remote_supported_capabilities_v2( + self, + event: hci.HCI_LE_CS_Read_Remote_Supported_Capabilities_Complete_V2_Event, + ): + # 6.3 variant with T_IP2_IPT_Times and T_SW_IPT_Time appended. + # Fed through the same builder so downstream code doesn't care which + # HCI event landed. + self._on_cs_remote_supported_capabilities_impl(event) + + def _on_cs_remote_supported_capabilities_impl(self, event) -> None: if not (connection := self.lookup_connection(event.connection_handle)): return @@ -6835,6 +6895,13 @@ def on_cs_remote_supported_capabilities( ) return + # V1 event carries rtt_random_sequence_n; V2 renamed the field to + # rtt_random_payload_n (same semantic per spec 6.3). + rtt_random = getattr( + event, + 'rtt_random_payload_n', + getattr(event, 'rtt_random_sequence_n', 0), + ) capabilities = ChannelSoundingCapabilities( num_config_supported=event.num_config_supported, max_consecutive_procedures_supported=event.max_consecutive_procedures_supported, @@ -6845,7 +6912,7 @@ def on_cs_remote_supported_capabilities( rtt_capability=event.rtt_capability, rtt_aa_only_n=event.rtt_aa_only_n, rtt_sounding_n=event.rtt_sounding_n, - rtt_random_sequence_n=event.rtt_random_sequence_n, + rtt_random_sequence_n=rtt_random, nadm_sounding_capability=event.nadm_sounding_capability, nadm_random_capability=event.nadm_random_capability, cs_sync_phys_supported=event.cs_sync_phys_supported, @@ -6856,6 +6923,11 @@ def on_cs_remote_supported_capabilities( t_pm_times_supported=event.t_pm_times_supported, t_sw_time_supported=event.t_sw_time_supported, tx_snr_capability=event.tx_snr_capability, + t_ip2_ipt_times_supported=getattr(event, 't_ip2_ipt_times_supported', 0), + t_sw_ipt_time_supported=getattr(event, 't_sw_ipt_time_supported', 0), + cs_ipt_reflector_supported=bool( + event.subfeatures_supported & hci.CsSubfeature.CS_IPT_REFLECTOR + ), ) connection.emit(connection.EVENT_CHANNEL_SOUNDING_CAPABILITIES, capabilities) diff --git a/bumble/hci.py b/bumble/hci.py index 30ea1daa..3ab67939 100644 --- a/bumble/hci.py +++ b/bumble/hci.py @@ -380,6 +380,7 @@ class SpecificationVersion(SpecableEnum): HCI_LE_FRAME_SPACE_UPDATE_COMPLETE_EVENT = 0x35 HCI_LE_UTP_RECEIVE_EVENT = 0x36 HCI_LE_CONNECTION_RATE_CHANGE_EVENT = 0x37 +HCI_LE_CS_READ_REMOTE_SUPPORTED_CAPABILITIES_COMPLETE_V2_EVENT = 0x38 # HCI Command @@ -709,6 +710,9 @@ class SpecificationVersion(SpecableEnum): HCI_LE_CS_PROCEDURE_ENABLE_COMMAND = hci_command_op_code(0x08, 0x0094) HCI_LE_CS_TEST_COMMAND = hci_command_op_code(0x08, 0x0095) HCI_LE_CS_TEST_END_COMMAND = hci_command_op_code(0x08, 0x0096) +# Bluetooth 6.3 CS Enhancements (Inline PCT) — opcodes 0x20A5 / 0x20A6. +HCI_LE_CS_READ_LOCAL_SUPPORTED_CAPABILITIES_V2_COMMAND = hci_command_op_code(0x08, 0x00A5) +HCI_LE_CS_WRITE_CACHED_REMOTE_SUPPORTED_CAPABILITIES_V2_COMMAND = hci_command_op_code(0x08, 0x00A6) HCI_LE_SET_HOST_FEATURE_V2_COMMAND = hci_command_op_code(0x08, 0x0097) HCI_LE_ADD_DEVICE_TO_MONITORED_ADVERTISERS_LIST_COMMAND = hci_command_op_code(0x08, 0x0098) HCI_LE_REMOVE_DEVICE_FROM_MONITORED_ADVERTISERS_LIST_COMMAND = hci_command_op_code(0x08, 0x0099) @@ -973,6 +977,20 @@ class CsSubeventAbortReason(SpecableEnum): SCHEDULING_CONFLICT_OR_LIMITED_RESOURCES = 0x03 UNSPECIFIED = 0x0F + +# Bluetooth 6.3 CS Enhancements — bits inside the 2-byte `subfeatures_supported` +# field of the CS Read (Local|Remote) Supported Capabilities V2 reply. Mirrors +# BT_HCI_LE_CS_SUBFEATURE_* masks from Nordic hci_types.h:3934. +class CsSubfeature(enum.IntFlag): + CS_IPT_REFLECTOR = 1 << 4 # Inline PCT capable as reflector + + +# Values for the `cs_enhancements_1` byte of HCI_LE_CS_Create_Config command. +# Bit 0 enables Inline PCT transfer on the CS configuration; other bits +# reserved. Mirrors Zephyr conn.h:851-855 semantics. +class CsEnhancements1(enum.IntFlag): + INLINE_PCT = 0x01 + class Role(SpecableEnum): CENTRAL = 0 PERIPHERAL = 1 @@ -1188,6 +1206,10 @@ class AddressType(SpecableEnum): 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_COMMAND : 1 << (20*8+7), + # Bluetooth 6.3 LE Extended Feature Set — CS V2 read of local supported + # capabilities. Per Nordic hci_types.h:2609: BT_CMD_TEST(cmd, 49, 2) + # → octet 49, bit 2 of the LE Supported Commands bitmap. + HCI_LE_CS_READ_LOCAL_SUPPORTED_CAPABILITIES_V2_COMMAND : 1 << (49*8+2), 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), @@ -5968,6 +5990,54 @@ class HCI_LE_CS_Read_Local_Supported_Capabilities_Command( ''' +# ----------------------------------------------------------------------------- +# Bluetooth 6.3 LE Extended Feature Set — CS Enhancements (V2 variant). +# Adds Inline PCT (IPT) timings after tx_snr_capability. Opcode 0x20A5. +# See Nordic hci_types.h §2607-2660 for the reference struct. +# ----------------------------------------------------------------------------- +@dataclasses.dataclass +class HCI_LE_CS_Read_Local_Supported_Capabilities_V2_ReturnParameters( + HCI_StatusReturnParameters +): + num_config_supported: int = field(metadata=metadata(1)) + max_consecutive_procedures_supported: int = field(metadata=metadata(2)) + num_antennas_supported: int = field(metadata=metadata(1)) + max_antenna_paths_supported: int = field(metadata=metadata(1)) + roles_supported: int = field(metadata=metadata(1)) + modes_supported: int = field(metadata=metadata(1)) + rtt_capability: int = field(metadata=metadata(1)) + rtt_aa_only_n: int = field(metadata=metadata(1)) + rtt_sounding_n: int = field(metadata=metadata(1)) + rtt_random_payload_n: int = field(metadata=metadata(1)) + nadm_sounding_capability: int = field(metadata=metadata(2)) + nadm_random_capability: int = field(metadata=metadata(2)) + cs_sync_phys_supported: int = field(metadata=metadata(CS_SYNC_PHY_SUPPORTED_SPEC)) + subfeatures_supported: int = field(metadata=metadata(2)) + t_ip1_times_supported: int = field(metadata=metadata(2)) + t_ip2_times_supported: int = field(metadata=metadata(2)) + t_fcs_times_supported: int = field(metadata=metadata(2)) + t_pm_times_supported: int = field(metadata=metadata(2)) + t_sw_time_supported: int = field(metadata=metadata(1)) + tx_snr_capability: int = field(metadata=metadata(CS_SNR_SPEC)) + # V2 tail. + t_ip2_ipt_times_supported: int = field(metadata=metadata(2)) + t_sw_ipt_time_supported: int = field(metadata=metadata(1)) + + +@HCI_SyncCommand.sync_command( + HCI_LE_CS_Read_Local_Supported_Capabilities_V2_ReturnParameters +) +@dataclasses.dataclass +class HCI_LE_CS_Read_Local_Supported_Capabilities_V2_Command( + HCI_SyncCommand[HCI_LE_CS_Read_Local_Supported_Capabilities_V2_ReturnParameters] +): + ''' + See Bluetooth spec @ 7.8.161 LE CS Read Local Supported Capabilities V2 command + (opcode 0x20A5). Introduced with LE Extended Feature Set / CS Enhancements + to report Inline PCT (IPT) timing capabilities. + ''' + + # ----------------------------------------------------------------------------- @HCI_Command.command @dataclasses.dataclass @@ -6097,7 +6167,10 @@ class Ch3cShape(SpecableEnum): channel_selection_type: int = field(metadata=ChannelSelectionType.type_metadata(1)) ch3c_shape: int = field(metadata=Ch3cShape.type_metadata(1)) ch3c_jump: int = field(metadata=metadata(1)) - reserved: int = field(metadata=metadata(1)) + # Bluetooth 6.3 CS Enhancements — last byte of the command was called + # "reserved" pre-6.3. Bit 0 (`CsEnhancements1.INLINE_PCT`) enables Inline + # PCT transfer on this CS configuration. Set to zero for 6.0 behavior. + cs_enhancements_1: int = field(default=0, metadata=metadata(1)) # ----------------------------------------------------------------------------- @@ -7220,6 +7293,43 @@ class HCI_LE_CS_Read_Remote_Supported_Capabilities_Complete_Event(HCI_LE_Meta_Ev tx_snr_capability: int = field(metadata=metadata(CS_SNR_SPEC)) +# ----------------------------------------------------------------------------- +@HCI_LE_Meta_Event.event +@dataclasses.dataclass +class HCI_LE_CS_Read_Remote_Supported_Capabilities_Complete_V2_Event(HCI_LE_Meta_Event): + ''' + See Bluetooth spec @ 7.7.65.65 LE CS Read Remote Supported Capabilities + Complete V2 event (subevent 0x38). Emitted (instead of the 6.0 variant + 0x2C) when the LE Extended Feature Set has been negotiated on the link. + Adds T_IP2_IPT and T_SW_IPT timings for Inline PCT. + ''' + + status: int = field(metadata=metadata(STATUS_SPEC)) + connection_handle: int = field(metadata=metadata(2)) + num_config_supported: int = field(metadata=metadata(1)) + max_consecutive_procedures_supported: int = field(metadata=metadata(2)) + num_antennas_supported: int = field(metadata=metadata(1)) + max_antenna_paths_supported: int = field(metadata=metadata(1)) + roles_supported: int = field(metadata=metadata(1)) + modes_supported: int = field(metadata=metadata(1)) + rtt_capability: int = field(metadata=metadata(1)) + rtt_aa_only_n: int = field(metadata=metadata(1)) + rtt_sounding_n: int = field(metadata=metadata(1)) + rtt_random_payload_n: int = field(metadata=metadata(1)) + nadm_sounding_capability: int = field(metadata=metadata(2)) + nadm_random_capability: int = field(metadata=metadata(2)) + cs_sync_phys_supported: int = field(metadata=metadata(CS_SYNC_PHY_SUPPORTED_SPEC)) + subfeatures_supported: int = field(metadata=metadata(2)) + t_ip1_times_supported: int = field(metadata=metadata(2)) + t_ip2_times_supported: int = field(metadata=metadata(2)) + t_fcs_times_supported: int = field(metadata=metadata(2)) + t_pm_times_supported: int = field(metadata=metadata(2)) + t_sw_time_supported: int = field(metadata=metadata(1)) + tx_snr_capability: int = field(metadata=metadata(CS_SNR_SPEC)) + t_ip2_ipt_times_supported: int = field(metadata=metadata(2)) + t_sw_ipt_time_supported: int = field(metadata=metadata(1)) + + # ----------------------------------------------------------------------------- @HCI_LE_Meta_Event.event @dataclasses.dataclass diff --git a/bumble/host.py b/bumble/host.py index 992e042e..50171212 100644 --- a/bumble/host.py +++ b/bumble/host.py @@ -2009,6 +2009,14 @@ def on_hci_le_cs_read_remote_supported_capabilities_complete_event( ): self.emit('cs_remote_supported_capabilities', event) + def on_hci_le_cs_read_remote_supported_capabilities_complete_v2_event( + self, + event: hci.HCI_LE_CS_Read_Remote_Supported_Capabilities_Complete_V2_Event, + ): + # Bluetooth 6.3 variant (subevent 0x38) — dispatched separately so + # device.py handlers can pick the right payload shape. + self.emit('cs_remote_supported_capabilities_v2', event) + def on_hci_le_cs_security_enable_complete_event( self, event: hci.HCI_LE_CS_Security_Enable_Complete_Event ):