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
159 changes: 111 additions & 48 deletions bumble/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,73 @@ 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

@classmethod
def from_hci(
cls,
report: (
hci.HCI_LE_CS_Read_Local_Supported_Capabilities_ReturnParameters
| hci.HCI_LE_CS_Read_Local_Supported_Capabilities_V2_ReturnParameters
| hci.HCI_LE_CS_Read_Remote_Supported_Capabilities_Complete_Event
| hci.HCI_LE_CS_Read_Remote_Supported_Capabilities_Complete_V2_Event
),
) -> ChannelSoundingCapabilities:
"""Build capabilities from either the 6.0 or the 6.3 [v2] HCI payload.

The V2 shapes rename rtt_random_sequence_n to rtt_random_payload_n and
append the Inline PCT timings; everything else is common to all four.
"""
if isinstance(
report,
(
hci.HCI_LE_CS_Read_Local_Supported_Capabilities_V2_ReturnParameters,
hci.HCI_LE_CS_Read_Remote_Supported_Capabilities_Complete_V2_Event,
),
):
rtt_random_sequence_n = report.rtt_random_payload_n
t_ip2_ipt_times_supported = report.t_ip2_ipt_times_supported
t_sw_ipt_time_supported = report.t_sw_ipt_time_supported
else:
rtt_random_sequence_n = report.rtt_random_sequence_n
t_ip2_ipt_times_supported = 0
t_sw_ipt_time_supported = 0

return cls(
num_config_supported=report.num_config_supported,
max_consecutive_procedures_supported=report.max_consecutive_procedures_supported,
num_antennas_supported=report.num_antennas_supported,
max_antenna_paths_supported=report.max_antenna_paths_supported,
roles_supported=report.roles_supported,
modes_supported=report.modes_supported,
rtt_capability=report.rtt_capability,
rtt_aa_only_n=report.rtt_aa_only_n,
rtt_sounding_n=report.rtt_sounding_n,
rtt_random_sequence_n=rtt_random_sequence_n,
nadm_sounding_capability=report.nadm_sounding_capability,
nadm_random_capability=report.nadm_random_capability,
cs_sync_phys_supported=report.cs_sync_phys_supported,
subfeatures_supported=report.subfeatures_supported,
t_ip1_times_supported=report.t_ip1_times_supported,
t_ip2_times_supported=report.t_ip2_times_supported,
t_fcs_times_supported=report.t_fcs_times_supported,
t_pm_times_supported=report.t_pm_times_supported,
t_sw_time_supported=report.t_sw_time_supported,
tx_snr_capability=report.tx_snr_capability,
t_ip2_ipt_times_supported=t_ip2_ipt_times_supported,
t_sw_ipt_time_supported=t_sw_ipt_time_supported,
cs_ipt_reflector_supported=bool(
report.subfeatures_supported & hci.CsSubfeature.CS_IPT_REFLECTOR
),
)


# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -2961,31 +3028,26 @@ async def power_on(self) -> None:
bit_value=1,
)
)
result = await self.send_sync_command(
hci.HCI_LE_CS_Read_Local_Supported_Capabilities_Command()
)
self.cs_capabilities = ChannelSoundingCapabilities(
num_config_supported=result.num_config_supported,
max_consecutive_procedures_supported=result.max_consecutive_procedures_supported,
num_antennas_supported=result.num_antennas_supported,
max_antenna_paths_supported=result.max_antenna_paths_supported,
roles_supported=result.roles_supported,
modes_supported=result.modes_supported,
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,
nadm_sounding_capability=result.nadm_sounding_capability,
nadm_random_capability=result.nadm_random_capability,
cs_sync_phys_supported=result.cs_sync_phys_supported,
subfeatures_supported=result.subfeatures_supported,
t_ip1_times_supported=result.t_ip1_times_supported,
t_ip2_times_supported=result.t_ip2_times_supported,
t_fcs_times_supported=result.t_fcs_times_supported,
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,
)
# The 6.3 [v2] caps read appends the Inline PCT (IPT)
# timings after tx_snr_capability. Controllers that predate
# 6.3 don't have it, so pick the variant the controller
# advertises.
if self.host.supports_command(
hci.HCI_LE_CS_READ_LOCAL_SUPPORTED_CAPABILITIES_V2_COMMAND
):
v2_result = await self.send_sync_command(
hci.HCI_LE_CS_Read_Local_Supported_Capabilities_V2_Command()
)
self.cs_capabilities = ChannelSoundingCapabilities.from_hci(
v2_result
)
else:
v1_result = await self.send_sync_command(
hci.HCI_LE_CS_Read_Local_Supported_Capabilities_Command()
)
self.cs_capabilities = ChannelSoundingCapabilities.from_hci(
v1_result
)

if (
self.le_shorter_connection_intervals_enabled
Expand Down Expand Up @@ -5448,6 +5510,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 +5556,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 +6889,25 @@ 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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can just keep a single on_cs_remote_supported_capabilities handler, and emit 'cs_remote_supported_capabilities' from Host.on_hci_le_cs_read_remote_supported_capabilities_complete_v2_event

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll update

self,
event: (
hci.HCI_LE_CS_Read_Remote_Supported_Capabilities_Complete_Event
| hci.HCI_LE_CS_Read_Remote_Supported_Capabilities_Complete_V2_Event
),
) -> None:
if not (connection := self.lookup_connection(event.connection_handle)):
return

Expand All @@ -6835,29 +6917,10 @@ def on_cs_remote_supported_capabilities(
)
return

capabilities = ChannelSoundingCapabilities(
num_config_supported=event.num_config_supported,
max_consecutive_procedures_supported=event.max_consecutive_procedures_supported,
num_antennas_supported=event.num_antennas_supported,
max_antenna_paths_supported=event.max_antenna_paths_supported,
roles_supported=event.roles_supported,
modes_supported=event.modes_supported,
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,
nadm_sounding_capability=event.nadm_sounding_capability,
nadm_random_capability=event.nadm_random_capability,
cs_sync_phys_supported=event.cs_sync_phys_supported,
subfeatures_supported=event.subfeatures_supported,
t_ip1_times_supported=event.t_ip1_times_supported,
t_ip2_times_supported=event.t_ip2_times_supported,
t_fcs_times_supported=event.t_fcs_times_supported,
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,
connection.emit(
connection.EVENT_CHANNEL_SOUNDING_CAPABILITIES,
ChannelSoundingCapabilities.from_hci(event),
)
connection.emit(connection.EVENT_CHANNEL_SOUNDING_CAPABILITIES, capabilities)

@host_event_handler
def on_cs_config(self, event: hci.HCI_LE_CS_Config_Complete_Event):
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(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
Loading