add basic support for SCO packets over USB

This commit is contained in:
Gilles Boccon-Gibod
2024-09-26 17:26:50 -07:00
parent 2d17a5f742
commit 794a4a3ef0
11 changed files with 1190 additions and 541 deletions

View File

@@ -1423,6 +1423,9 @@ class ScoLink(utils.CompositeEventEmitter):
acl_connection: Connection
handle: int
link_type: int
rx_packet_length: int
tx_packet_length: int
air_mode: hci.CodecID
sink: Callable[[hci.HCI_SynchronousDataPacket], Any] | None = None
EVENT_DISCONNECTION: ClassVar[str] = "disconnection"
@@ -5968,7 +5971,7 @@ class Device(utils.CompositeEventEmitter):
def on_connection_request(
self, bd_addr: hci.Address, class_of_device: int, link_type: int
):
logger.debug(f'*** Connection request: {bd_addr}')
logger.debug(f'*** Connection request: {bd_addr} link_type={link_type}')
# Handle SCO request.
if link_type in (
@@ -5978,6 +5981,7 @@ class Device(utils.CompositeEventEmitter):
if connection := self.find_connection_by_bd_addr(
bd_addr, transport=PhysicalTransport.BR_EDR
):
connection.emit(self.EVENT_SCO_REQUEST, link_type)
self.emit(self.EVENT_SCO_REQUEST, connection, link_type)
else:
logger.error(f'SCO request from a non-connected device {bd_addr}')
@@ -6337,8 +6341,7 @@ class Device(utils.CompositeEventEmitter):
logger.warning('peer name is not valid UTF-8')
if connection:
connection.emit(connection.EVENT_REMOTE_NAME_FAILURE, error)
else:
self.emit(self.EVENT_REMOTE_NAME_FAILURE, address, error)
self.emit(self.EVENT_REMOTE_NAME_FAILURE, address, error)
# [Classic only]
@host_event_handler
@@ -6355,7 +6358,13 @@ class Device(utils.CompositeEventEmitter):
@with_connection_from_address
@utils.experimental('Only for testing.')
def on_sco_connection(
self, acl_connection: Connection, sco_handle: int, link_type: int
self,
acl_connection: Connection,
sco_handle: int,
link_type: int,
rx_packet_length: int,
tx_packet_length: int,
air_mode: int,
) -> None:
logger.debug(
f'*** SCO connected: {acl_connection.peer_address}, '
@@ -6367,7 +6376,11 @@ class Device(utils.CompositeEventEmitter):
acl_connection=acl_connection,
handle=sco_handle,
link_type=link_type,
rx_packet_length=rx_packet_length,
tx_packet_length=tx_packet_length,
air_mode=hci.CodecID(air_mode),
)
acl_connection.emit(self.EVENT_SCO_CONNECTION, sco_link)
self.emit(self.EVENT_SCO_CONNECTION, sco_link)
# [Classic only]
@@ -6378,7 +6391,8 @@ class Device(utils.CompositeEventEmitter):
self, acl_connection: Connection, status: int
) -> None:
logger.debug(f'*** SCO connection failure: {acl_connection.peer_address}***')
self.emit(self.EVENT_SCO_CONNECTION_FAILURE)
acl_connection.emit(self.EVENT_SCO_CONNECTION_FAILURE, status)
self.emit(self.EVENT_SCO_CONNECTION_FAILURE, status)
# [Classic only]
@host_event_handler
@@ -6841,15 +6855,18 @@ class Device(utils.CompositeEventEmitter):
@with_connection_from_address
def on_classic_pairing(self, connection: Connection) -> None:
connection.emit(connection.EVENT_CLASSIC_PAIRING)
self.emit(connection.EVENT_CLASSIC_PAIRING, connection)
# [Classic only]
@host_event_handler
@with_connection_from_address
def on_classic_pairing_failure(self, connection: Connection, status: int) -> None:
connection.emit(connection.EVENT_CLASSIC_PAIRING_FAILURE, status)
self.emit(connection.EVENT_CLASSIC_PAIRING_FAILURE, connection, status)
def on_pairing_start(self, connection: Connection) -> None:
connection.emit(connection.EVENT_PAIRING_START)
self.emit(connection.EVENT_PAIRING_START, connection)
def on_pairing(
self,

View File

@@ -1769,6 +1769,61 @@ class CodingFormat:
)
@dataclasses.dataclass(frozen=True)
class VoiceSetting:
class AirCodingFormat(enum.IntEnum):
CVSD = 0
U_LAW = 1
A_LAW = 2
TRANSPARENT_DATA = 3
class InputSampleSize(enum.IntEnum):
SIZE_8_BITS = 0
SIZE_16_BITS = 1
class InputDataFormat(enum.IntEnum):
ONES_COMPLEMENT = 0
TWOS_COMPLEMENT = 1
SIGN_AND_MAGNITUDE = 2
UNSIGNED = 3
class InputCodingFormat(enum.IntEnum):
LINEAR = 0
U_LAW = 1
A_LAW = 2
RESERVED = 3
air_coding_format: AirCodingFormat = AirCodingFormat.CVSD
linear_pcm_bit_position: int = 0
input_sample_size: InputSampleSize = InputSampleSize.SIZE_8_BITS
input_data_format: InputDataFormat = InputDataFormat.ONES_COMPLEMENT
input_coding_format: InputCodingFormat = InputCodingFormat.LINEAR
@classmethod
def from_int(cls, value: int) -> VoiceSetting:
air_coding_format = cls.AirCodingFormat(value & 0b11)
linear_pcm_bit_position = (value >> 2) & 0b111
input_sample_size = cls.InputSampleSize((value >> 5) & 0b1)
input_data_format = cls.InputDataFormat((value >> 6) & 0b11)
input_coding_format = cls.InputCodingFormat((value >> 8) & 0b11)
return cls(
air_coding_format=air_coding_format,
linear_pcm_bit_position=linear_pcm_bit_position,
input_sample_size=input_sample_size,
input_data_format=input_data_format,
input_coding_format=input_coding_format,
)
def __int__(self) -> int:
return (
self.air_coding_format
| (self.linear_pcm_bit_position << 2)
| (self.input_sample_size << 5)
| (self.input_data_format << 6)
| (self.input_coding_format << 8)
)
# -----------------------------------------------------------------------------
class HCI_Constant:
@staticmethod
@@ -2907,6 +2962,23 @@ class HCI_Read_Clock_Offset_Command(HCI_AsyncCommand):
connection_handle: int = field(metadata=metadata(2))
# -----------------------------------------------------------------------------
@HCI_Command.command
@dataclasses.dataclass
class HCI_Accept_Synchronous_Connection_Request_Command(HCI_AsyncCommand):
'''
See Bluetooth spec @ 7.1.27 Accept Synchronous Connection Request Command
'''
bd_addr: Address = field(metadata=metadata(Address.parse_address))
transmit_bandwidth: int = field(metadata=metadata(4))
receive_bandwidth: int = field(metadata=metadata(4))
max_latency: int = field(metadata=metadata(2))
voice_setting: int = field(metadata=metadata(2))
retransmission_effort: int = field(metadata=metadata(1))
packet_type: int = field(metadata=metadata(2))
# -----------------------------------------------------------------------------
@HCI_Command.command
@dataclasses.dataclass
@@ -3965,6 +4037,23 @@ class HCI_Read_Local_OOB_Extended_Data_Command(
'''
# -----------------------------------------------------------------------------
@HCI_SyncCommand.sync_command(HCI_StatusReturnParameters)
@dataclasses.dataclass
class HCI_Configure_Data_Path_Command(HCI_SyncCommand[HCI_StatusReturnParameters]):
'''
See Bluetooth spec @ 7.3.101 Configure Data Path Command
'''
class DataPathDirection(SpecableEnum):
INPUT = 0x00
OUTPUT = 0x01
data_path_direction: DataPathDirection = field(metadata=metadata(1))
data_path_id: int = field(metadata=metadata(1))
vendor_specific_config: bytes = field(metadata=metadata('*'))
# -----------------------------------------------------------------------------
@dataclasses.dataclass
class HCI_Read_Local_Version_Information_ReturnParameters(HCI_StatusReturnParameters):
@@ -7355,7 +7444,7 @@ class HCI_Connection_Complete_Event(HCI_Event):
status: int = field(metadata=metadata(STATUS_SPEC))
connection_handle: int = field(metadata=metadata(2))
bd_addr: Address = field(metadata=metadata(Address.parse_address))
link_type: int = field(metadata=LinkType.type_metadata(1))
link_type: LinkType = field(metadata=LinkType.type_metadata(1))
encryption_enabled: int = field(metadata=metadata(1))
@@ -7751,12 +7840,6 @@ class HCI_Synchronous_Connection_Complete_Event(HCI_Event):
SCO = 0x00
ESCO = 0x02
class AirMode(SpecableEnum):
U_LAW_LOG = 0x00
A_LAW_LOG_AIR_MORE = 0x01
CVSD = 0x02
TRANSPARENT_DATA = 0x03
status: int = field(metadata=metadata(STATUS_SPEC))
connection_handle: int = field(metadata=metadata(2))
bd_addr: Address = field(metadata=metadata(Address.parse_address))
@@ -7765,7 +7848,7 @@ class HCI_Synchronous_Connection_Complete_Event(HCI_Event):
retransmission_window: int = field(metadata=metadata(1))
rx_packet_length: int = field(metadata=metadata(2))
tx_packet_length: int = field(metadata=metadata(2))
air_mode: int = field(metadata=AirMode.type_metadata(1))
air_mode: int = field(metadata=CodecID.type_metadata(1))
# -----------------------------------------------------------------------------
@@ -7997,7 +8080,9 @@ class HCI_AclDataPacket(HCI_Packet):
bc_flag = (h >> 14) & 3
data = packet[5:]
if len(data) != data_total_length:
raise InvalidPacketError('invalid packet length')
raise InvalidPacketError(
f'invalid packet length {len(data)} != {data_total_length}'
)
return cls(
connection_handle=connection_handle,
pb_flag=pb_flag,
@@ -8030,10 +8115,16 @@ class HCI_SynchronousDataPacket(HCI_Packet):
See Bluetooth spec @ 5.4.3 HCI SCO Data Packets
'''
class Status(enum.IntEnum):
CORRECTLY_RECEIVED_DATA = 0b00
POSSIBLY_INVALID_DATA = 0b01
NO_DATA = 0b10
DATA_PARTIALLY_LOST = 0b11
hci_packet_type = HCI_SYNCHRONOUS_DATA_PACKET
connection_handle: int
packet_status: int
packet_status: Status
data_total_length: int
data: bytes
@@ -8042,7 +8133,7 @@ class HCI_SynchronousDataPacket(HCI_Packet):
# Read the header
h, data_total_length = struct.unpack_from('<HB', packet, 1)
connection_handle = h & 0xFFF
packet_status = (h >> 12) & 0b11
packet_status = cls.Status((h >> 12) & 0b11)
data = packet[4:]
if len(data) != data_total_length:
raise InvalidPacketError(
@@ -8066,7 +8157,7 @@ class HCI_SynchronousDataPacket(HCI_Packet):
return (
f'{color("SCO", "blue")}: '
f'handle=0x{self.connection_handle:04x}, '
f'ps={self.packet_status}, '
f'ps={self.packet_status.name}, '
f'data_total_length={self.data_total_length}, '
f'data={self.data.hex()}'
)
@@ -8094,8 +8185,8 @@ class HCI_IsoDataPacket(HCI_Packet):
def __post_init__(self) -> None:
self.ts_flag = self.time_stamp is not None
@staticmethod
def from_bytes(packet: bytes) -> HCI_IsoDataPacket:
@classmethod
def from_bytes(cls, packet: bytes) -> HCI_IsoDataPacket:
time_stamp: int | None = None
packet_sequence_number: int | None = None
iso_sdu_length: int | None = None
@@ -8124,7 +8215,7 @@ class HCI_IsoDataPacket(HCI_Packet):
pos += 4
iso_sdu_fragment = packet[pos:]
return HCI_IsoDataPacket(
return cls(
connection_handle=connection_handle,
pb_flag=pb_flag,
ts_flag=ts_flag,

View File

@@ -166,7 +166,7 @@ class AgFeature(enum.IntFlag):
VOICE_RECOGNITION_TEXT = 0x2000
class AudioCodec(enum.IntEnum):
class AudioCodec(utils.OpenIntEnum):
"""
Audio Codec IDs (normative).
@@ -178,7 +178,7 @@ class AudioCodec(enum.IntEnum):
LC3_SWB = 0x03 # Support for LC3-SWB audio codec
class HfIndicator(enum.IntEnum):
class HfIndicator(utils.OpenIntEnum):
"""
HF Indicators (normative).
@@ -207,7 +207,7 @@ class CallHoldOperation(enum.Enum):
)
class ResponseHoldStatus(enum.IntEnum):
class ResponseHoldStatus(utils.OpenIntEnum):
"""
Response Hold status (normative).
@@ -235,7 +235,7 @@ class AgIndicator(enum.Enum):
BATTERY_CHARGE = 'battchg'
class CallSetupAgIndicator(enum.IntEnum):
class CallSetupAgIndicator(utils.OpenIntEnum):
"""
Values for the Call Setup AG indicator (normative).
@@ -248,7 +248,7 @@ class CallSetupAgIndicator(enum.IntEnum):
REMOTE_ALERTED = 3 # Remote party alerted in an outgoing call
class CallHeldAgIndicator(enum.IntEnum):
class CallHeldAgIndicator(utils.OpenIntEnum):
"""
Values for the Call Held AG indicator (normative).
@@ -262,7 +262,7 @@ class CallHeldAgIndicator(enum.IntEnum):
CALL_ON_HOLD_NO_ACTIVE_CALL = 2 # Call on hold, no active call
class CallInfoDirection(enum.IntEnum):
class CallInfoDirection(utils.OpenIntEnum):
"""
Call Info direction (normative).
@@ -273,7 +273,7 @@ class CallInfoDirection(enum.IntEnum):
MOBILE_TERMINATED_CALL = 1
class CallInfoStatus(enum.IntEnum):
class CallInfoStatus(utils.OpenIntEnum):
"""
Call Info status (normative).
@@ -288,7 +288,7 @@ class CallInfoStatus(enum.IntEnum):
WAITING = 5
class CallInfoMode(enum.IntEnum):
class CallInfoMode(utils.OpenIntEnum):
"""
Call Info mode (normative).
@@ -301,7 +301,7 @@ class CallInfoMode(enum.IntEnum):
UNKNOWN = 9
class CallInfoMultiParty(enum.IntEnum):
class CallInfoMultiParty(utils.OpenIntEnum):
"""
Call Info Multi-Party state (normative).
@@ -388,7 +388,7 @@ class CallLineIdentification:
)
class VoiceRecognitionState(enum.IntEnum):
class VoiceRecognitionState(utils.OpenIntEnum):
"""
vrec values provided in AT+BVRA command.
@@ -401,7 +401,7 @@ class VoiceRecognitionState(enum.IntEnum):
ENHANCED_READY = 2
class CmeError(enum.IntEnum):
class CmeError(utils.OpenIntEnum):
"""
CME ERROR codes (partial listed).
@@ -1624,7 +1624,7 @@ class AgProtocol(utils.EventEmitter):
# -----------------------------------------------------------------------------
class ProfileVersion(enum.IntEnum):
class ProfileVersion(utils.OpenIntEnum):
"""
Profile version (normative).
@@ -2076,6 +2076,7 @@ _ESCO_PARAMETERS_MSBC_T1 = EscoParameters(
max_latency=0x0008,
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
@@ -2091,7 +2092,6 @@ _ESCO_PARAMETERS_MSBC_T2 = EscoParameters(
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

View File

@@ -686,6 +686,8 @@ class Host(utils.EventEmitter):
self.pending_response, timeout=response_timeout
)
return response
except asyncio.TimeoutError:
raise
except Exception:
logger.exception(color("!!! Exception while sending command:", "red"))
raise
@@ -866,7 +868,7 @@ class Host(utils.EventEmitter):
self.send_hci_packet(
hci.HCI_SynchronousDataPacket(
connection_handle=connection_handle,
packet_status=0,
packet_status=hci.HCI_SynchronousDataPacket.Status.CORRECTLY_RECEIVED_DATA,
data_total_length=len(sdu),
data=sdu,
)
@@ -1177,11 +1179,28 @@ class Host(utils.EventEmitter):
def on_hci_connection_complete_event(
self, event: hci.HCI_Connection_Complete_Event
):
if event.link_type == hci.HCI_Connection_Complete_Event.LinkType.SCO:
# Pass this on to the synchronous connection handler
forwarded_event = hci.HCI_Synchronous_Connection_Complete_Event(
status=event.status,
connection_handle=event.connection_handle,
bd_addr=event.bd_addr,
link_type=event.link_type,
transmission_interval=0,
retransmission_window=0,
rx_packet_length=0,
tx_packet_length=0,
air_mode=0,
)
self.on_hci_synchronous_connection_complete_event(forwarded_event)
return
if event.status == hci.HCI_SUCCESS:
# Create/update the connection
logger.debug(
f'### BR/EDR CONNECTION: [0x{event.connection_handle:04X}] '
f'{event.bd_addr}'
f'### BR/EDR ACL CONNECTION: [0x{event.connection_handle:04X}] '
f'{event.bd_addr} '
f'{event.link_type.name}'
)
connection = self.connections.get(event.connection_handle)
@@ -1581,6 +1600,9 @@ class Host(utils.EventEmitter):
event.bd_addr,
event.connection_handle,
event.link_type,
event.rx_packet_length,
event.tx_packet_length,
event.air_mode,
)
else:
logger.debug(f'### SCO CONNECTION FAILED: {event.status}')

View File

@@ -110,7 +110,7 @@ RFCOMM_DEFAULT_L2CAP_MTU = 2048
RFCOMM_DEFAULT_INITIAL_CREDITS = 7
RFCOMM_DEFAULT_MAX_CREDITS = 32
RFCOMM_DEFAULT_CREDIT_THRESHOLD = RFCOMM_DEFAULT_MAX_CREDITS // 2
RFCOMM_DEFAULT_MAX_FRAME_SIZE = 2000
RFCOMM_DEFAULT_MAX_FRAME_SIZE = 1000
RFCOMM_DYNAMIC_CHANNEL_NUMBER_START = 1
RFCOMM_DYNAMIC_CHANNEL_NUMBER_END = 30

File diff suppressed because it is too large Load Diff