Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 66 additions & 6 deletions bumble/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -2961,9 +2970,30 @@ 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.
use_v2 = True
try:
result = await self.send_sync_command(
hci.HCI_LE_CS_Read_Local_Supported_Capabilities_V2_Command()
)
except hci.HCI_Error as e:
if e.error_code != hci.HCI_UNKNOWN_HCI_COMMAND_ERROR:
raise
use_v2 = False
result = await self.send_sync_command(
hci.HCI_LE_CS_Read_Local_Supported_Capabilities_Command()
)
# The V2 dataclass renamed rtt_random_sequence_n → rtt_random_payload_n
# (same semantic). Cover both.
rtt_random = (getattr(result, 'rtt_random_payload_n', None)
if use_v2 else result.rtt_random_sequence_n)
self.cs_capabilities = ChannelSoundingCapabilities(
num_config_supported=result.num_config_supported,
max_consecutive_procedures_supported=result.max_consecutive_procedures_supported,
Expand All @@ -2974,7 +3004,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,
Expand All @@ -2985,6 +3015,11 @@ 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 (
Expand Down Expand Up @@ -5448,6 +5483,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()
Expand Down Expand Up @@ -5493,7 +5529,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
Expand Down Expand Up @@ -6826,6 +6862,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

Expand All @@ -6835,6 +6884,12 @@ 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,
Expand All @@ -6845,7 +6900,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,
Expand All @@ -6856,6 +6911,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)

Expand Down
112 changes: 111 additions & 1 deletion bumble/hci.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))


# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions bumble/host.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
):
Expand Down