From 9f0bcc131f050783a29cc7b7dccb0f6df7eba57e Mon Sep 17 00:00:00 2001 From: Josh Wu Date: Thu, 8 Jun 2023 20:48:56 +0800 Subject: [PATCH] eSCO support --- bumble/hci.py | 121 +++++++++++++++++++++++++++++++++++ bumble/hfp.py | 168 +++++++++++++++++++++++++++++++++++++++++++++++++ bumble/host.py | 42 ++++++++++--- 3 files changed, 323 insertions(+), 8 deletions(-) diff --git a/bumble/hci.py b/bumble/hci.py index 41deed2f..67edde8e 100644 --- a/bumble/hci.py +++ b/bumble/hci.py @@ -17,6 +17,7 @@ # ----------------------------------------------------------------------------- from __future__ import annotations import collections +import enum import functools import logging import struct @@ -1368,6 +1369,7 @@ def phy_list_to_bits(phys): if feature_name.startswith('HCI_') and feature_name.endswith('_LE_SUPPORTED_FEATURE') } + # fmt: on # pylint: enable=line-too-long # pylint: disable=invalid-name @@ -1923,6 +1925,9 @@ def from_bytes(packet: bytes) -> HCI_Packet: if packet_type == HCI_ACL_DATA_PACKET: return HCI_AclDataPacket.from_bytes(packet) + if packet_type == HCI_SYNCHRONOUS_DATA_PACKET: + return HCI_SynchronousDataPacket.from_bytes(packet) + if packet_type == HCI_EVENT_PACKET: return HCI_Event.from_bytes(packet) @@ -2291,6 +2296,19 @@ class HCI_Read_Clock_Offset_Command(HCI_Command): ''' +# ----------------------------------------------------------------------------- +@HCI_Command.command( + fields=[ + ('bd_addr', Address.parse_address), + ('reason', {'size': 1, 'mapper': HCI_Constant.error_name}), + ], +) +class HCI_Reject_Synchronous_Connection_Request_Command(HCI_Command): + ''' + See Bluetooth spec @ 7.1.28 Reject Synchronous Connection Request Command + ''' + + # ----------------------------------------------------------------------------- @HCI_Command.command( fields=[ @@ -2452,6 +2470,51 @@ class HCI_Enhanced_Setup_Synchronous_Connection_Command(HCI_Command): See Bluetooth spec @ 7.1.45 Enhanced Setup Synchronous Connection Command ''' + class CodingFormat(enum.IntEnum): + U_LOG = 0x00 + A_LOG = 0x01 + CVSD = 0x02 + TRANSPARENT = 0x03 + PCM = 0x04 + MSBC = 0x05 + LC3 = 0x06 + G729A = 0x07 + + def to_bytes(self): + return self.value.to_bytes(5, 'little') + + def __bytes__(self): + return self.to_bytes() + + class PcmDataFormat(enum.IntEnum): + NA = 0x00 + ONES_COMPLEMENT = 0x01 + TWOS_COMPLEMENT = 0x02 + SIGN_MAGNITUDE = 0x03 + UNSIGNED = 0x04 + + class DataPath(enum.IntEnum): + HCI = 0x00 + PCM = 0x01 + + class RetransmissionEffort(enum.IntEnum): + NO_RETRANSMISSION = 0x00 + OPTIMIZE_FOR_POWER = 0x01 + OPTIMIZE_FOR_QUALITY = 0x02 + DONT_CARE = 0xFF + + class PacketType(enum.IntFlag): + HV1 = 0x0001 + HV2 = 0x0002 + HV3 = 0x0004 + EV3 = 0x0008 + EV4 = 0x0010 + EV5 = 0x0020 + NO_2_EV3 = 0x0040 + NO_3_EV3 = 0x0080 + NO_2_EV5 = 0x0100 + NO_3_EV5 = 0x0200 + # ----------------------------------------------------------------------------- @HCI_Command.command( @@ -5736,6 +5799,64 @@ def __str__(self): ) +# ----------------------------------------------------------------------------- +class HCI_SynchronousDataPacket(HCI_Packet): + ''' + See Bluetooth spec @ 5.4.3 HCI SCO Data Packets + ''' + + hci_packet_type = HCI_SYNCHRONOUS_DATA_PACKET + + @staticmethod + def from_bytes(packet: bytes) -> HCI_SynchronousDataPacket: + # Read the header + h, data_total_length = struct.unpack_from('> 12) & 0b11 + rfu = (h >> 14) & 0b11 + data = packet[4:] + if len(data) != data_total_length: + raise ValueError( + f'invalid packet length {len(data)} != {data_total_length}' + ) + return HCI_SynchronousDataPacket( + connection_handle, packet_status, rfu, data_total_length, data + ) + + def to_bytes(self) -> bytes: + h = (self.packet_status << 12) | (self.rfu << 14) | self.connection_handle + return ( + struct.pack(' None: + self.connection_handle = connection_handle + self.packet_status = packet_status + self.rfu = rfu + self.data_total_length = data_total_length + self.data = data + + def __bytes__(self) -> bytes: + return self.to_bytes() + + def __str__(self) -> str: + return ( + f'{color("SCO", "blue")}: ' + f'handle=0x{self.connection_handle:04x}, ' + f'ps={self.packet_status}, rfu={self.rfu}, ' + f'data_total_length={self.data_total_length}, ' + f'data={self.data.hex()}' + ) + + # ----------------------------------------------------------------------------- class HCI_AclDataPacketAssembler: current_data: Optional[bytes] diff --git a/bumble/hfp.py b/bumble/hfp.py index bb009208..42683d52 100644 --- a/bumble/hfp.py +++ b/bumble/hfp.py @@ -35,6 +35,7 @@ BT_L2CAP_PROTOCOL_ID, BT_RFCOMM_PROTOCOL_ID, ) +from bumble.hci import HCI_Enhanced_Setup_Synchronous_Connection_Command from bumble.sdp import ( DataElement, ServiceAttribute, @@ -819,3 +820,170 @@ def sdp_records( DataElement.unsigned_integer_16(hf_supported_features), ), ] + + +# ----------------------------------------------------------------------------- +# ESCO Codec Default Parameters +# ----------------------------------------------------------------------------- + + +# Hands-Free Profile v1.8, 5.7 Codec Interoperability Requirements +class DefaultCodecParameters(enum.IntEnum): + SCO_CVSD_D0 = enum.auto() + SCO_CVSD_D1 = enum.auto() + ESCO_CVSD_S1 = enum.auto() + ESCO_CVSD_S2 = enum.auto() + ESCO_CVSD_S3 = enum.auto() + ESCO_CVSD_S4 = enum.auto() + ESCO_MSBC_T1 = enum.auto() + ESCO_MSBC_T2 = enum.auto() + + +@dataclasses.dataclass +class EscoParameters: + # Codec specific + transmit_coding_format: HCI_Enhanced_Setup_Synchronous_Connection_Command.CodingFormat + receive_coding_format: HCI_Enhanced_Setup_Synchronous_Connection_Command.CodingFormat + packet_type: HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType + retransmission_effort: HCI_Enhanced_Setup_Synchronous_Connection_Command.RetransmissionEffort + max_latency: int + + # Common + input_coding_format: HCI_Enhanced_Setup_Synchronous_Connection_Command.CodingFormat = ( + HCI_Enhanced_Setup_Synchronous_Connection_Command.CodingFormat.TRANSPARENT + ) + output_coding_format: HCI_Enhanced_Setup_Synchronous_Connection_Command.CodingFormat = ( + HCI_Enhanced_Setup_Synchronous_Connection_Command.CodingFormat.TRANSPARENT + ) + input_coded_data_size: int = 16 + output_coded_data_size: int = 16 + input_pcm_data_format: HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat = ( + HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat.TWOS_COMPLEMENT + ) + output_pcm_data_format: HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat = ( + HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat.TWOS_COMPLEMENT + ) + input_pcm_sample_payload_msb_position: int = 0 + output_pcm_sample_payload_msb_position: int = 0 + input_data_path: HCI_Enhanced_Setup_Synchronous_Connection_Command.DataPath = ( + HCI_Enhanced_Setup_Synchronous_Connection_Command.DataPath.HCI + ) + output_data_path: HCI_Enhanced_Setup_Synchronous_Connection_Command.DataPath = ( + HCI_Enhanced_Setup_Synchronous_Connection_Command.DataPath.HCI + ) + input_transport_unit_size: int = 0 + output_transport_unit_size: int = 0 + input_bandwidth: int = 16000 + output_bandwidth: int = 16000 + transmit_bandwidth: int = 8000 + receive_bandwidth: int = 8000 + transmit_codec_frame_size: int = 60 + receive_codec_frame_size: int = 60 + + +_ESCO_PARAMETERS_CVSD_D0 = EscoParameters( + transmit_coding_format=HCI_Enhanced_Setup_Synchronous_Connection_Command.CodingFormat.CVSD, + receive_coding_format=HCI_Enhanced_Setup_Synchronous_Connection_Command.CodingFormat.CVSD, + max_latency=0xFFFF, + packet_type=HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.HV1, + retransmission_effort=HCI_Enhanced_Setup_Synchronous_Connection_Command.RetransmissionEffort.NO_RETRANSMISSION, +) + +_ESCO_PARAMETERS_CVSD_D1 = EscoParameters( + transmit_coding_format=HCI_Enhanced_Setup_Synchronous_Connection_Command.CodingFormat.CVSD, + receive_coding_format=HCI_Enhanced_Setup_Synchronous_Connection_Command.CodingFormat.CVSD, + max_latency=0xFFFF, + packet_type=HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.HV3, + retransmission_effort=HCI_Enhanced_Setup_Synchronous_Connection_Command.RetransmissionEffort.NO_RETRANSMISSION, +) + +_ESCO_PARAMETERS_CVSD_S1 = EscoParameters( + transmit_coding_format=HCI_Enhanced_Setup_Synchronous_Connection_Command.CodingFormat.CVSD, + receive_coding_format=HCI_Enhanced_Setup_Synchronous_Connection_Command.CodingFormat.CVSD, + max_latency=0x0007, + packet_type=( + HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.EV3 + | HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.NO_2_EV3 + | HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.NO_3_EV3 + | HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.NO_2_EV5 + | HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.NO_3_EV5 + ), + retransmission_effort=HCI_Enhanced_Setup_Synchronous_Connection_Command.RetransmissionEffort.OPTIMIZE_FOR_POWER, +) + +_ESCO_PARAMETERS_CVSD_S2 = EscoParameters( + transmit_coding_format=HCI_Enhanced_Setup_Synchronous_Connection_Command.CodingFormat.CVSD, + receive_coding_format=HCI_Enhanced_Setup_Synchronous_Connection_Command.CodingFormat.CVSD, + max_latency=0x0007, + packet_type=( + HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.EV3 + | HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.NO_3_EV3 + | HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.NO_2_EV5 + | HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.NO_3_EV5 + ), + retransmission_effort=HCI_Enhanced_Setup_Synchronous_Connection_Command.RetransmissionEffort.OPTIMIZE_FOR_POWER, +) + +_ESCO_PARAMETERS_CVSD_S3 = EscoParameters( + transmit_coding_format=HCI_Enhanced_Setup_Synchronous_Connection_Command.CodingFormat.CVSD, + receive_coding_format=HCI_Enhanced_Setup_Synchronous_Connection_Command.CodingFormat.CVSD, + max_latency=0x000A, + packet_type=( + HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.EV3 + | HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.NO_3_EV3 + | HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.NO_2_EV5 + | HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.NO_3_EV5 + ), + retransmission_effort=HCI_Enhanced_Setup_Synchronous_Connection_Command.RetransmissionEffort.OPTIMIZE_FOR_POWER, +) + +_ESCO_PARAMETERS_CVSD_S4 = EscoParameters( + transmit_coding_format=HCI_Enhanced_Setup_Synchronous_Connection_Command.CodingFormat.CVSD, + receive_coding_format=HCI_Enhanced_Setup_Synchronous_Connection_Command.CodingFormat.CVSD, + max_latency=0x000C, + packet_type=( + HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.EV3 + | HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.NO_3_EV3 + | HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.NO_2_EV5 + | HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.NO_3_EV5 + ), + retransmission_effort=HCI_Enhanced_Setup_Synchronous_Connection_Command.RetransmissionEffort.OPTIMIZE_FOR_QUALITY, +) + +_ESCO_PARAMETERS_MSBC_T1 = EscoParameters( + transmit_coding_format=HCI_Enhanced_Setup_Synchronous_Connection_Command.CodingFormat.MSBC, + receive_coding_format=HCI_Enhanced_Setup_Synchronous_Connection_Command.CodingFormat.MSBC, + max_latency=0x0008, + packet_type=( + HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.EV3 + | HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.NO_3_EV3 + | HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.NO_2_EV5 + | HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.NO_3_EV5 + ), + retransmission_effort=HCI_Enhanced_Setup_Synchronous_Connection_Command.RetransmissionEffort.OPTIMIZE_FOR_QUALITY, +) + +_ESCO_PARAMETERS_MSBC_T2 = EscoParameters( + transmit_coding_format=HCI_Enhanced_Setup_Synchronous_Connection_Command.CodingFormat.MSBC, + receive_coding_format=HCI_Enhanced_Setup_Synchronous_Connection_Command.CodingFormat.MSBC, + max_latency=0x000D, + packet_type=( + HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.EV3 + | HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.NO_2_EV3 + | HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.NO_3_EV3 + | HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.NO_2_EV5 + | HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType.NO_3_EV5 + ), + retransmission_effort=HCI_Enhanced_Setup_Synchronous_Connection_Command.RetransmissionEffort.OPTIMIZE_FOR_QUALITY, +) + +ESCO_PERAMETERS = { + DefaultCodecParameters.SCO_CVSD_D0: _ESCO_PARAMETERS_CVSD_D0, + DefaultCodecParameters.SCO_CVSD_D1: _ESCO_PARAMETERS_CVSD_D1, + DefaultCodecParameters.ESCO_CVSD_S1: _ESCO_PARAMETERS_CVSD_S1, + DefaultCodecParameters.ESCO_CVSD_S2: _ESCO_PARAMETERS_CVSD_S2, + DefaultCodecParameters.ESCO_CVSD_S3: _ESCO_PARAMETERS_CVSD_S3, + DefaultCodecParameters.ESCO_CVSD_S4: _ESCO_PARAMETERS_CVSD_S4, + DefaultCodecParameters.ESCO_MSBC_T1: _ESCO_PARAMETERS_MSBC_T1, + DefaultCodecParameters.ESCO_MSBC_T2: _ESCO_PARAMETERS_MSBC_T2, +} diff --git a/bumble/host.py b/bumble/host.py index 02caa46f..a649eb6d 100644 --- a/bumble/host.py +++ b/bumble/host.py @@ -21,7 +21,7 @@ import logging import struct -from typing import Optional, TYPE_CHECKING, Dict, Callable, Awaitable +from typing import Optional, TYPE_CHECKING, Dict, Callable, Awaitable, cast from bumble.colors import color from bumble.l2cap import L2CAP_PDU @@ -43,6 +43,7 @@ HCI_RESET_COMMAND, HCI_SUCCESS, HCI_SUPPORTED_COMMANDS_FLAGS, + HCI_SYNCHRONOUS_DATA_PACKET, HCI_VERSION_BLUETOOTH_CORE_4_0, HCI_AclDataPacket, HCI_AclDataPacketAssembler, @@ -67,6 +68,7 @@ HCI_Read_Local_Version_Information_Command, HCI_Reset_Command, HCI_Set_Event_Mask_Command, + HCI_SynchronousDataPacket, ) from .core import ( BT_BR_EDR_TRANSPORT, @@ -485,12 +487,14 @@ def on_hci_packet(self, packet: HCI_Packet) -> None: self.snooper.snoop(bytes(packet), Snooper.Direction.CONTROLLER_TO_HOST) # If the packet is a command, invoke the handler for this packet - if isinstance(packet, HCI_Command): - self.on_hci_command_packet(packet) - elif isinstance(packet, HCI_Event): - self.on_hci_event_packet(packet) - elif isinstance(packet, HCI_AclDataPacket): - self.on_hci_acl_data_packet(packet) + if packet.hci_packet_type == HCI_COMMAND_PACKET: + self.on_hci_command_packet(cast(HCI_Command, packet)) + elif packet.hci_packet_type == HCI_EVENT_PACKET: + self.on_hci_event_packet(cast(HCI_Event, packet)) + elif packet.hci_packet_type == HCI_ACL_DATA_PACKET: + self.on_hci_acl_data_packet(cast(HCI_AclDataPacket, packet)) + elif packet.hci_packet_type == HCI_SYNCHRONOUS_DATA_PACKET: + self.on_hci_sco_data_packet(cast(HCI_SynchronousDataPacket, packet)) else: logger.warning(f'!!! unknown packet type {packet.hci_packet_type}') @@ -507,6 +511,10 @@ def on_hci_acl_data_packet(self, packet: HCI_AclDataPacket) -> None: if connection := self.connections.get(packet.connection_handle): connection.on_hci_acl_data_packet(packet) + def on_hci_sco_data_packet(self, packet: HCI_SynchronousDataPacket) -> None: + # Experimental + self.emit('sco_packet', packet.connection_handle, packet) + def on_l2cap_pdu(self, connection: Connection, cid: int, pdu: bytes) -> None: self.emit('l2cap_pdu', connection.handle, cid, pdu) @@ -760,7 +768,25 @@ async def send_long_term_key(): asyncio.create_task(send_long_term_key()) def on_hci_synchronous_connection_complete_event(self, event): - pass + if event.status == HCI_SUCCESS: + # Create/update the connection + logger.debug( + f'### SCO CONNECTION: [0x{event.connection_handle:04X}] ' + f'{event.bd_addr}' + ) + + # Notify the client + self.emit( + 'sco_connection', + event.bd_addr, + event.connection_handle, + event.link_type, + ) + else: + logger.debug(f'### SCO CONNECTION FAILED: {event.status}') + + # Notify the client + self.emit('sco_connection_failure', event.bd_addr, event.status) def on_hci_synchronous_connection_changed_event(self, event): pass