Migrate AVDTP enums

This commit is contained in:
Josh Wu
2025-10-22 20:33:31 +08:00
parent 6c68115660
commit 5dc76cf7b4
3 changed files with 189 additions and 218 deletions

View File

@@ -41,7 +41,6 @@ from bumble.core import (
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE, BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
InvalidStateError, InvalidStateError,
ProtocolError, ProtocolError,
name_or_number,
) )
from bumble.rtp import MediaPacket from bumble.rtp import MediaPacket
@@ -62,74 +61,72 @@ AVDTP_PSM = 0x0019
AVDTP_DEFAULT_RTX_SIG_TIMER = 5 # Seconds AVDTP_DEFAULT_RTX_SIG_TIMER = 5 # Seconds
# Signal Identifiers (AVDTP spec - 8.5 Signal Command Set) # Signal Identifiers (AVDTP spec - 8.5 Signal Command Set)
AVDTP_DISCOVER = 0x01 class SignalIdentifier(hci.SpecableEnum):
AVDTP_GET_CAPABILITIES = 0x02 DISCOVER = 0x01
AVDTP_SET_CONFIGURATION = 0x03 GET_CAPABILITIES = 0x02
AVDTP_GET_CONFIGURATION = 0x04 SET_CONFIGURATION = 0x03
AVDTP_RECONFIGURE = 0x05 GET_CONFIGURATION = 0x04
AVDTP_OPEN = 0x06 RECONFIGURE = 0x05
AVDTP_START = 0x07 OPEN = 0x06
AVDTP_CLOSE = 0x08 START = 0x07
AVDTP_SUSPEND = 0x09 CLOSE = 0x08
AVDTP_ABORT = 0x0A SUSPEND = 0x09
AVDTP_SECURITY_CONTROL = 0x0B ABORT = 0x0A
AVDTP_GET_ALL_CAPABILITIES = 0x0C SECURITY_CONTROL = 0x0B
AVDTP_DELAYREPORT = 0x0D GET_ALL_CAPABILITIES = 0x0C
DELAYREPORT = 0x0D
AVDTP_SIGNAL_NAMES = { AVDTP_DISCOVER = SignalIdentifier.DISCOVER
AVDTP_DISCOVER: 'AVDTP_DISCOVER', AVDTP_GET_CAPABILITIES = SignalIdentifier.GET_CAPABILITIES
AVDTP_GET_CAPABILITIES: 'AVDTP_GET_CAPABILITIES', AVDTP_SET_CONFIGURATION = SignalIdentifier.SET_CONFIGURATION
AVDTP_SET_CONFIGURATION: 'AVDTP_SET_CONFIGURATION', AVDTP_GET_CONFIGURATION = SignalIdentifier.GET_CONFIGURATION
AVDTP_GET_CONFIGURATION: 'AVDTP_GET_CONFIGURATION', AVDTP_RECONFIGURE = SignalIdentifier.RECONFIGURE
AVDTP_RECONFIGURE: 'AVDTP_RECONFIGURE', AVDTP_OPEN = SignalIdentifier.OPEN
AVDTP_OPEN: 'AVDTP_OPEN', AVDTP_START = SignalIdentifier.START
AVDTP_START: 'AVDTP_START', AVDTP_CLOSE = SignalIdentifier.CLOSE
AVDTP_CLOSE: 'AVDTP_CLOSE', AVDTP_SUSPEND = SignalIdentifier.SUSPEND
AVDTP_SUSPEND: 'AVDTP_SUSPEND', AVDTP_ABORT = SignalIdentifier.ABORT
AVDTP_ABORT: 'AVDTP_ABORT', AVDTP_SECURITY_CONTROL = SignalIdentifier.SECURITY_CONTROL
AVDTP_SECURITY_CONTROL: 'AVDTP_SECURITY_CONTROL', AVDTP_GET_ALL_CAPABILITIES = SignalIdentifier.GET_ALL_CAPABILITIES
AVDTP_GET_ALL_CAPABILITIES: 'AVDTP_GET_ALL_CAPABILITIES', AVDTP_DELAYREPORT = SignalIdentifier.DELAYREPORT
AVDTP_DELAYREPORT: 'AVDTP_DELAYREPORT'
}
# Error codes (AVDTP spec - 8.20.6.2 ERROR_CODE tables) class ErrorCode(hci.SpecableEnum):
AVDTP_BAD_HEADER_FORMAT_ERROR = 0x01 '''Error codes (AVDTP spec - 8.20.6.2 ERROR_CODE tables)'''
AVDTP_BAD_LENGTH_ERROR = 0x11 BAD_HEADER_FORMAT = 0x01
AVDTP_BAD_ACP_SEID_ERROR = 0x12 BAD_LENGTH = 0x11
AVDTP_SEP_IN_USE_ERROR = 0x13 BAD_ACP_SEID = 0x12
AVDTP_SEP_NOT_IN_USE_ERROR = 0x14 SEP_IN_USE = 0x13
AVDTP_BAD_SERV_CATEGORY_ERROR = 0x17 SEP_NOT_IN_USE = 0x14
AVDTP_BAD_PAYLOAD_FORMAT_ERROR = 0x18 BAD_SERV_CATEGORY = 0x17
AVDTP_NOT_SUPPORTED_COMMAND_ERROR = 0x19 BAD_PAYLOAD_FORMAT = 0x18
AVDTP_INVALID_CAPABILITIES_ERROR = 0x1A NOT_SUPPORTED_COMMAND = 0x19
AVDTP_BAD_RECOVERY_TYPE_ERROR = 0x22 INVALID_CAPABILITIES = 0x1A
AVDTP_BAD_MEDIA_TRANSPORT_FORMAT_ERROR = 0x23 BAD_RECOVERY_TYPE = 0x22
AVDTP_BAD_RECOVERY_FORMAT_ERROR = 0x25 BAD_MEDIA_TRANSPORT_FORMAT = 0x23
AVDTP_BAD_ROHC_FORMAT_ERROR = 0x26 BAD_RECOVERY_FORMAT = 0x25
AVDTP_BAD_CP_FORMAT_ERROR = 0x27 BAD_ROHC_FORMAT = 0x26
AVDTP_BAD_MULTIPLEXING_FORMAT_ERROR = 0x28 BAD_CP_FORMAT = 0x27
AVDTP_UNSUPPORTED_CONFIGURATION_ERROR = 0x29 BAD_MULTIPLEXING_FORMAT = 0x28
AVDTP_BAD_STATE_ERROR = 0x31 UNSUPPORTED_CONFIGURATION = 0x29
BAD_STATE = 0x31
AVDTP_ERROR_NAMES = { AVDTP_BAD_HEADER_FORMAT_ERROR = ErrorCode.BAD_HEADER_FORMAT
AVDTP_BAD_HEADER_FORMAT_ERROR: 'AVDTP_BAD_HEADER_FORMAT_ERROR', AVDTP_BAD_LENGTH_ERROR = ErrorCode.BAD_LENGTH
AVDTP_BAD_LENGTH_ERROR: 'AVDTP_BAD_LENGTH_ERROR', AVDTP_BAD_ACP_SEID_ERROR = ErrorCode.BAD_ACP_SEID
AVDTP_BAD_ACP_SEID_ERROR: 'AVDTP_BAD_ACP_SEID_ERROR', AVDTP_SEP_IN_USE_ERROR = ErrorCode.SEP_IN_USE
AVDTP_SEP_IN_USE_ERROR: 'AVDTP_SEP_IN_USE_ERROR', AVDTP_SEP_NOT_IN_USE_ERROR = ErrorCode.SEP_NOT_IN_USE
AVDTP_SEP_NOT_IN_USE_ERROR: 'AVDTP_SEP_NOT_IN_USE_ERROR', AVDTP_BAD_SERV_CATEGORY_ERROR = ErrorCode.BAD_SERV_CATEGORY
AVDTP_BAD_SERV_CATEGORY_ERROR: 'AVDTP_BAD_SERV_CATEGORY_ERROR', AVDTP_BAD_PAYLOAD_FORMAT_ERROR = ErrorCode.BAD_PAYLOAD_FORMAT
AVDTP_BAD_PAYLOAD_FORMAT_ERROR: 'AVDTP_BAD_PAYLOAD_FORMAT_ERROR', AVDTP_NOT_SUPPORTED_COMMAND_ERROR = ErrorCode.NOT_SUPPORTED_COMMAND
AVDTP_NOT_SUPPORTED_COMMAND_ERROR: 'AVDTP_NOT_SUPPORTED_COMMAND_ERROR', AVDTP_INVALID_CAPABILITIES_ERROR = ErrorCode.INVALID_CAPABILITIES
AVDTP_INVALID_CAPABILITIES_ERROR: 'AVDTP_INVALID_CAPABILITIES_ERROR', AVDTP_BAD_RECOVERY_TYPE_ERROR = ErrorCode.BAD_RECOVERY_TYPE
AVDTP_BAD_RECOVERY_TYPE_ERROR: 'AVDTP_BAD_RECOVERY_TYPE_ERROR', AVDTP_BAD_MEDIA_TRANSPORT_FORMAT_ERROR = ErrorCode.BAD_MEDIA_TRANSPORT_FORMAT
AVDTP_BAD_MEDIA_TRANSPORT_FORMAT_ERROR: 'AVDTP_BAD_MEDIA_TRANSPORT_FORMAT_ERROR', AVDTP_BAD_RECOVERY_FORMAT_ERROR = ErrorCode.BAD_RECOVERY_FORMAT
AVDTP_BAD_RECOVERY_FORMAT_ERROR: 'AVDTP_BAD_RECOVERY_FORMAT_ERROR', AVDTP_BAD_ROHC_FORMAT_ERROR = ErrorCode.BAD_ROHC_FORMAT
AVDTP_BAD_ROHC_FORMAT_ERROR: 'AVDTP_BAD_ROHC_FORMAT_ERROR', AVDTP_BAD_CP_FORMAT_ERROR = ErrorCode.BAD_CP_FORMAT
AVDTP_BAD_CP_FORMAT_ERROR: 'AVDTP_BAD_CP_FORMAT_ERROR', AVDTP_BAD_MULTIPLEXING_FORMAT_ERROR = ErrorCode.BAD_MULTIPLEXING_FORMAT
AVDTP_BAD_MULTIPLEXING_FORMAT_ERROR: 'AVDTP_BAD_MULTIPLEXING_FORMAT_ERROR', AVDTP_UNSUPPORTED_CONFIGURATION_ERROR = ErrorCode.UNSUPPORTED_CONFIGURATION
AVDTP_UNSUPPORTED_CONFIGURATION_ERROR: 'AVDTP_UNSUPPORTED_CONFIGURATION_ERROR', AVDTP_BAD_STATE_ERROR = ErrorCode.BAD_STATE
AVDTP_BAD_STATE_ERROR: 'AVDTP_BAD_STATE_ERROR'
}
class MediaType(utils.OpenIntEnum): class MediaType(utils.OpenIntEnum):
AUDIO = 0x00 AUDIO = 0x00
@@ -140,52 +137,42 @@ AVDTP_AUDIO_MEDIA_TYPE = MediaType.AUDIO
AVDTP_VIDEO_MEDIA_TYPE = MediaType.VIDEO AVDTP_VIDEO_MEDIA_TYPE = MediaType.VIDEO
AVDTP_MULTIMEDIA_MEDIA_TYPE = MediaType.MULTIMEDIA AVDTP_MULTIMEDIA_MEDIA_TYPE = MediaType.MULTIMEDIA
# TSEP (AVDTP spec - 8.20.3 Stream End-point Type, Source or Sink (TSEP)) class StreamEndPointType(utils.OpenIntEnum):
AVDTP_TSEP_SRC = 0x00 '''TSEP (AVDTP spec - 8.20.3 Stream End-point Type, Source or Sink (TSEP)).'''
AVDTP_TSEP_SNK = 0x01 SRC = 0x00
SNK = 0x01
AVDTP_TSEP_NAMES = { AVDTP_TSEP_SRC = StreamEndPointType.SRC
AVDTP_TSEP_SRC: 'AVDTP_TSEP_SRC', AVDTP_TSEP_SNK = StreamEndPointType.SNK
AVDTP_TSEP_SNK: 'AVDTP_TSEP_SNK'
}
# Service Categories (AVDTP spec - Table 8.47: Service Category information element field values) class ServiceCategory(hci.SpecableEnum):
AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY = 0x01 '''Service Categories (AVDTP spec - Table 8.47: Service Category information element field values).'''
AVDTP_REPORTING_SERVICE_CATEGORY = 0x02 MEDIA_TRANSPORT = 0x01
AVDTP_RECOVERY_SERVICE_CATEGORY = 0x03 REPORTING = 0x02
AVDTP_CONTENT_PROTECTION_SERVICE_CATEGORY = 0x04 RECOVERY = 0x03
AVDTP_HEADER_COMPRESSION_SERVICE_CATEGORY = 0x05 CONTENT_PROTECTION = 0x04
AVDTP_MULTIPLEXING_SERVICE_CATEGORY = 0x06 HEADER_COMPRESSION = 0x05
AVDTP_MEDIA_CODEC_SERVICE_CATEGORY = 0x07 MULTIPLEXING = 0x06
AVDTP_DELAY_REPORTING_SERVICE_CATEGORY = 0x08 MEDIA_CODEC = 0x07
DELAY_REPORTING = 0x08
AVDTP_SERVICE_CATEGORY_NAMES = { AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY = ServiceCategory.MEDIA_TRANSPORT
AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY: 'AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY', AVDTP_REPORTING_SERVICE_CATEGORY = ServiceCategory.REPORTING
AVDTP_REPORTING_SERVICE_CATEGORY: 'AVDTP_REPORTING_SERVICE_CATEGORY', AVDTP_RECOVERY_SERVICE_CATEGORY = ServiceCategory.RECOVERY
AVDTP_RECOVERY_SERVICE_CATEGORY: 'AVDTP_RECOVERY_SERVICE_CATEGORY', AVDTP_CONTENT_PROTECTION_SERVICE_CATEGORY = ServiceCategory.CONTENT_PROTECTION
AVDTP_CONTENT_PROTECTION_SERVICE_CATEGORY: 'AVDTP_CONTENT_PROTECTION_SERVICE_CATEGORY', AVDTP_HEADER_COMPRESSION_SERVICE_CATEGORY = ServiceCategory.HEADER_COMPRESSION
AVDTP_HEADER_COMPRESSION_SERVICE_CATEGORY: 'AVDTP_HEADER_COMPRESSION_SERVICE_CATEGORY', AVDTP_MULTIPLEXING_SERVICE_CATEGORY = ServiceCategory.MULTIPLEXING
AVDTP_MULTIPLEXING_SERVICE_CATEGORY: 'AVDTP_MULTIPLEXING_SERVICE_CATEGORY', AVDTP_MEDIA_CODEC_SERVICE_CATEGORY = ServiceCategory.MEDIA_CODEC
AVDTP_MEDIA_CODEC_SERVICE_CATEGORY: 'AVDTP_MEDIA_CODEC_SERVICE_CATEGORY', AVDTP_DELAY_REPORTING_SERVICE_CATEGORY = ServiceCategory.DELAY_REPORTING
AVDTP_DELAY_REPORTING_SERVICE_CATEGORY: 'AVDTP_DELAY_REPORTING_SERVICE_CATEGORY'
}
# States (AVDTP spec - 9.1 State Definitions) class State(utils.OpenIntEnum):
AVDTP_IDLE_STATE = 0x00 '''States (AVDTP spec - 9.1 State Definitions)'''
AVDTP_CONFIGURED_STATE = 0x01 IDLE = 0x00
AVDTP_OPEN_STATE = 0x02 CONFIGURED = 0x01
AVDTP_STREAMING_STATE = 0x03 OPEN = 0x02
AVDTP_CLOSING_STATE = 0x04 STREAMING = 0x03
AVDTP_ABORTING_STATE = 0x05 CLOSING = 0x04
ABORTING = 0x05
AVDTP_STATE_NAMES = {
AVDTP_IDLE_STATE: 'AVDTP_IDLE_STATE',
AVDTP_CONFIGURED_STATE: 'AVDTP_CONFIGURED_STATE',
AVDTP_OPEN_STATE: 'AVDTP_OPEN_STATE',
AVDTP_STREAMING_STATE: 'AVDTP_STREAMING_STATE',
AVDTP_CLOSING_STATE: 'AVDTP_CLOSING_STATE',
AVDTP_ABORTING_STATE: 'AVDTP_ABORTING_STATE'
}
# fmt: on # fmt: on
# pylint: enable=line-too-long # pylint: enable=line-too-long
@@ -308,6 +295,7 @@ class MediaPacketPump:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class MessageAssembler: class MessageAssembler:
message: Optional[bytes] message: Optional[bytes]
signal_identifier: SignalIdentifier
def __init__(self, callback: Callable[[int, Message], Any]) -> None: def __init__(self, callback: Callable[[int, Message], Any]) -> None:
self.callback = callback self.callback = callback
@@ -317,7 +305,7 @@ class MessageAssembler:
self.transaction_label = 0 self.transaction_label = 0
self.message = None self.message = None
self.message_type = Message.MessageType.COMMAND self.message_type = Message.MessageType.COMMAND
self.signal_identifier = 0 self.signal_identifier = SignalIdentifier(0)
self.number_of_signal_packets = 0 self.number_of_signal_packets = 0
self.packet_count = 0 self.packet_count = 0
@@ -346,7 +334,7 @@ class MessageAssembler:
self.reset() self.reset()
self.transaction_label = transaction_label self.transaction_label = transaction_label
self.signal_identifier = pdu[1] & 0x3F self.signal_identifier = SignalIdentifier(pdu[1] & 0x3F)
self.message_type = message_type self.message_type = message_type
if packet_type == Protocol.PacketType.SINGLE_PACKET: if packet_type == Protocol.PacketType.SINGLE_PACKET:
@@ -402,7 +390,9 @@ class MessageAssembler:
def on_message_complete(self) -> None: def on_message_complete(self) -> None:
message = Message.create( message = Message.create(
self.signal_identifier, self.message_type, self.message or b'' self.signal_identifier,
self.message_type,
self.message or b'',
) )
try: try:
self.callback(self.transaction_label, message) self.callback(self.transaction_label, message)
@@ -524,7 +514,7 @@ class EndPointInfo:
seid: int seid: int
in_use: int in_use: int
media_type: MediaType media_type: MediaType
tsep: int tsep: StreamEndPointType
@classmethod @classmethod
def from_bytes(cls, payload: bytes) -> EndPointInfo: def from_bytes(cls, payload: bytes) -> EndPointInfo:
@@ -532,7 +522,7 @@ class EndPointInfo:
seid=payload[0] >> 2, seid=payload[0] >> 2,
in_use=payload[0] >> 1 & 1, in_use=payload[0] >> 1 & 1,
media_type=MediaType(payload[1] >> 4), media_type=MediaType(payload[1] >> 4),
tsep=payload[1] >> 3 & 1, tsep=StreamEndPointType(payload[1] >> 3 & 1),
) )
def __bytes__(self) -> bytes: def __bytes__(self) -> bytes:
@@ -560,7 +550,7 @@ class Message:
subclasses: ClassVar[dict[int, dict[int, type[Message]]]] = {} subclasses: ClassVar[dict[int, dict[int, type[Message]]]] = {}
message_type: MessageType message_type: MessageType
signal_identifier: int signal_identifier: SignalIdentifier
_payload: Optional[bytes] = None _payload: Optional[bytes] = None
fields: ClassVar[hci.Fields] = () fields: ClassVar[hci.Fields] = ()
@@ -588,7 +578,10 @@ class Message:
# type # type
@classmethod @classmethod
def create( def create(
cls, signal_identifier: int, message_type: MessageType, payload: bytes cls,
signal_identifier: SignalIdentifier,
message_type: MessageType,
payload: bytes,
) -> Message: ) -> Message:
instance: Message instance: Message
# Look for a registered subclass # Look for a registered subclass
@@ -604,7 +597,7 @@ class Message:
# Instantiate the appropriate class based on the message type # Instantiate the appropriate class based on the message type
if message_type == Message.MessageType.RESPONSE_REJECT: if message_type == Message.MessageType.RESPONSE_REJECT:
# Assume a simple reject message # Assume a simple reject message
instance = Simple_Reject(payload[0]) instance = Simple_Reject(ErrorCode(payload[0]))
else: else:
instance = Message() instance = Message()
instance.payload = payload instance.payload = payload
@@ -614,8 +607,7 @@ class Message:
def to_string(self, details: Union[str, Iterable[str]]) -> str: def to_string(self, details: Union[str, Iterable[str]]) -> str:
base = color( base = color(
f'{name_or_number(AVDTP_SIGNAL_NAMES, self.signal_identifier)}_' f'{self.signal_identifier.name}_{self.message_type.name}',
f'{self.message_type.name}',
'yellow', 'yellow',
) )
@@ -659,10 +651,10 @@ class Simple_Reject(Message):
message_type = Message.MessageType.RESPONSE_REJECT message_type = Message.MessageType.RESPONSE_REJECT
error_code: int = field(metadata=hci.metadata(1)) error_code: ErrorCode = field(metadata=ErrorCode.type_metadata(1))
def __str__(self) -> str: def __str__(self) -> str:
details = [f'error_code: {name_or_number(AVDTP_ERROR_NAMES, self.error_code)}'] details = [f'error_code: {self.error_code.name}']
return self.to_string(details) return self.to_string(details)
@@ -724,7 +716,7 @@ class Discover_Response(Message):
f'ACP SEID: {endpoint.seid}', f'ACP SEID: {endpoint.seid}',
f' in_use: {endpoint.in_use}', f' in_use: {endpoint.in_use}',
f' media_type: {endpoint.media_type.name}', f' media_type: {endpoint.media_type.name}',
f' tsep: {name_or_number(AVDTP_TSEP_NAMES, endpoint.tsep)}', f' tsep: {endpoint.tsep.name}',
] ]
) )
return self.to_string(details) return self.to_string(details)
@@ -857,19 +849,17 @@ class Set_Configuration_Reject(Message):
signal_identifier = AVDTP_SET_CONFIGURATION signal_identifier = AVDTP_SET_CONFIGURATION
message_type = Message.MessageType.RESPONSE_REJECT message_type = Message.MessageType.RESPONSE_REJECT
service_category: int = field(metadata=hci.metadata(1), default=0) service_category: ServiceCategory = field(
error_code: int = field(metadata=hci.metadata(1), default=0) metadata=ServiceCategory.type_metadata(1), default=ServiceCategory(0)
)
error_code: ErrorCode = field(
metadata=ErrorCode.type_metadata(1), default=ErrorCode(0)
)
def __str__(self) -> str: def __str__(self) -> str:
details = [ details = [
( (f'service_category: {self.service_category.name}'),
'service_category: ' (f'error_code: {self.error_code.name}'),
f'{name_or_number(AVDTP_SERVICE_CATEGORY_NAMES, self.service_category)}'
),
(
'error_code: '
f'{name_or_number(AVDTP_ERROR_NAMES, self.error_code)}'
),
] ]
return self.to_string(details) return self.to_string(details)
@@ -1052,12 +1042,12 @@ class Start_Reject(Message):
message_type = Message.MessageType.RESPONSE_REJECT message_type = Message.MessageType.RESPONSE_REJECT
acp_seid: int = field(metadata=Message.SEID_METADATA) acp_seid: int = field(metadata=Message.SEID_METADATA)
error_code: int = field(metadata=hci.metadata(1)) error_code: ErrorCode = field(metadata=ErrorCode.type_metadata(1))
def __str__(self) -> str: def __str__(self) -> str:
details = [ details = [
f'acp_seid: {self.acp_seid}', f'acp_seid: {self.acp_seid}',
f'error_code: {name_or_number(AVDTP_ERROR_NAMES, self.error_code)}', f'error_code: {self.error_code.name}',
] ]
return self.to_string(details) return self.to_string(details)
@@ -1210,7 +1200,7 @@ class General_Reject(Message):
See Bluetooth AVDTP spec - 8.18 General Reject See Bluetooth AVDTP spec - 8.18 General Reject
''' '''
signal_identifier = 0 signal_identifier = SignalIdentifier(0)
message_type = Message.MessageType.GENERAL_REJECT message_type = Message.MessageType.GENERAL_REJECT
def to_string(self, details): def to_string(self, details):
@@ -1286,10 +1276,6 @@ class Protocol(utils.EventEmitter):
CONTINUE_PACKET = 2 CONTINUE_PACKET = 2
END_PACKET = 3 END_PACKET = 3
@staticmethod
def packet_type_name(packet_type):
return name_or_number(Protocol.PACKET_TYPE_NAMES, packet_type)
@staticmethod @staticmethod
async def connect( async def connect(
connection: device.Connection, version: tuple[int, int] = (1, 3) connection: device.Connection, version: tuple[int, int] = (1, 3)
@@ -1462,11 +1448,7 @@ class Protocol(utils.EventEmitter):
if message.message_type == Message.MessageType.COMMAND: if message.message_type == Message.MessageType.COMMAND:
# Command # Command
signal_name = ( signal_name = message.signal_identifier.name.lower()
AVDTP_SIGNAL_NAMES.get(message.signal_identifier, "")
.replace("AVDTP_", "")
.lower()
)
handler_name = f'on_{signal_name}_command' handler_name = f'on_{signal_name}_command'
handler = getattr(self, handler_name, None) handler = getattr(self, handler_name, None)
if handler: if handler:
@@ -1649,11 +1631,11 @@ class Protocol(utils.EventEmitter):
) -> Optional[Message]: ) -> Optional[Message]:
endpoint = self.get_local_endpoint_by_seid(command.acp_seid) endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
if endpoint is None: if endpoint is None:
return Set_Configuration_Reject(AVDTP_BAD_ACP_SEID_ERROR) return Set_Configuration_Reject(error_code=AVDTP_BAD_ACP_SEID_ERROR)
# Check that the local endpoint isn't in use # Check that the local endpoint isn't in use
if endpoint.in_use: if endpoint.in_use:
return Set_Configuration_Reject(AVDTP_SEP_IN_USE_ERROR) return Set_Configuration_Reject(error_code=AVDTP_SEP_IN_USE_ERROR)
# Create a stream object for the pair of endpoints # Create a stream object for the pair of endpoints
stream = Stream(self, endpoint, StreamEndPointProxy(self, command.int_seid)) stream = Stream(self, endpoint, StreamEndPointProxy(self, command.int_seid))
@@ -1676,9 +1658,9 @@ class Protocol(utils.EventEmitter):
def on_reconfigure_command(self, command: Reconfigure_Command) -> Optional[Message]: def on_reconfigure_command(self, command: Reconfigure_Command) -> Optional[Message]:
endpoint = self.get_local_endpoint_by_seid(command.acp_seid) endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
if endpoint is None: if endpoint is None:
return Reconfigure_Reject(0, AVDTP_BAD_ACP_SEID_ERROR) return Reconfigure_Reject(error_code=AVDTP_BAD_ACP_SEID_ERROR)
if endpoint.stream is None: if endpoint.stream is None:
return Reconfigure_Reject(0, AVDTP_BAD_STATE_ERROR) return Reconfigure_Reject(error_code=AVDTP_BAD_STATE_ERROR)
result = endpoint.stream.on_reconfigure_command(command.capabilities) result = endpoint.stream.on_reconfigure_command(command.capabilities)
return result or Reconfigure_Response() return result or Reconfigure_Response()
@@ -1842,12 +1824,8 @@ class Stream:
rtp_channel: Optional[l2cap.ClassicChannel] rtp_channel: Optional[l2cap.ClassicChannel]
@staticmethod def change_state(self, state: State) -> None:
def state_name(state: int) -> str: logger.debug(f'{self} state change -> {color(state.name, "cyan")}')
return name_or_number(AVDTP_STATE_NAMES, state)
def change_state(self, state: int) -> None:
logger.debug(f'{self} state change -> {color(self.state_name(state), "cyan")}')
self.state = state self.state = state
def send_media_packet(self, packet: MediaPacket) -> None: def send_media_packet(self, packet: MediaPacket) -> None:
@@ -1855,22 +1833,22 @@ class Stream:
self.rtp_channel.send_pdu(bytes(packet)) self.rtp_channel.send_pdu(bytes(packet))
async def configure(self) -> None: async def configure(self) -> None:
if self.state != AVDTP_IDLE_STATE: if self.state != State.IDLE:
raise InvalidStateError('current state is not IDLE') raise InvalidStateError('current state is not IDLE')
await self.remote_endpoint.set_configuration( await self.remote_endpoint.set_configuration(
self.local_endpoint.seid, self.local_endpoint.configuration self.local_endpoint.seid, self.local_endpoint.configuration
) )
self.change_state(AVDTP_CONFIGURED_STATE) self.change_state(State.CONFIGURED)
async def open(self) -> None: async def open(self) -> None:
if self.state != AVDTP_CONFIGURED_STATE: if self.state != State.CONFIGURED:
raise InvalidStateError('current state is not CONFIGURED') raise InvalidStateError('current state is not CONFIGURED')
logger.debug('opening remote endpoint') logger.debug('opening remote endpoint')
await self.remote_endpoint.open() await self.remote_endpoint.open()
self.change_state(AVDTP_OPEN_STATE) self.change_state(State.OPEN)
# Create a channel for RTP packets # Create a channel for RTP packets
self.rtp_channel = ( self.rtp_channel = (
@@ -1882,10 +1860,10 @@ class Stream:
async def start(self) -> None: async def start(self) -> None:
"""[Source] Start streaming.""" """[Source] Start streaming."""
# Auto-open if needed # Auto-open if needed
if self.state == AVDTP_CONFIGURED_STATE: if self.state == State.CONFIGURED:
await self.open() await self.open()
if self.state != AVDTP_OPEN_STATE: if self.state != State.OPEN:
raise InvalidStateError('current state is not OPEN') raise InvalidStateError('current state is not OPEN')
logger.debug('starting remote endpoint') logger.debug('starting remote endpoint')
@@ -1894,11 +1872,11 @@ class Stream:
logger.debug('starting local endpoint') logger.debug('starting local endpoint')
await self.local_endpoint.start() await self.local_endpoint.start()
self.change_state(AVDTP_STREAMING_STATE) self.change_state(State.STREAMING)
async def stop(self) -> None: async def stop(self) -> None:
"""[Source] Stop streaming and transit to OPEN state.""" """[Source] Stop streaming and transit to OPEN state."""
if self.state != AVDTP_STREAMING_STATE: if self.state != State.STREAMING:
raise InvalidStateError('current state is not STREAMING') raise InvalidStateError('current state is not STREAMING')
logger.debug('stopping local endpoint') logger.debug('stopping local endpoint')
@@ -1907,11 +1885,11 @@ class Stream:
logger.debug('stopping remote endpoint') logger.debug('stopping remote endpoint')
await self.remote_endpoint.stop() await self.remote_endpoint.stop()
self.change_state(AVDTP_OPEN_STATE) self.change_state(State.OPEN)
async def close(self) -> None: async def close(self) -> None:
"""[Source] Close channel and transit to IDLE state.""" """[Source] Close channel and transit to IDLE state."""
if self.state not in (AVDTP_OPEN_STATE, AVDTP_STREAMING_STATE): if self.state not in (State.OPEN, State.STREAMING):
raise InvalidStateError('current state is not OPEN or STREAMING') raise InvalidStateError('current state is not OPEN or STREAMING')
logger.debug('closing local endpoint') logger.debug('closing local endpoint')
@@ -1921,7 +1899,7 @@ class Stream:
await self.remote_endpoint.close() await self.remote_endpoint.close()
# Release any channels we may have created # Release any channels we may have created
self.change_state(AVDTP_CLOSING_STATE) self.change_state(State.CLOSING)
if self.rtp_channel: if self.rtp_channel:
await self.rtp_channel.disconnect() await self.rtp_channel.disconnect()
self.rtp_channel = None self.rtp_channel = None
@@ -1929,31 +1907,31 @@ class Stream:
# Release the endpoint # Release the endpoint
self.local_endpoint.in_use = 0 self.local_endpoint.in_use = 0
self.change_state(AVDTP_IDLE_STATE) self.change_state(State.IDLE)
def on_set_configuration_command(self, configuration): def on_set_configuration_command(self, configuration):
if self.state != AVDTP_IDLE_STATE: if self.state != State.IDLE:
return Set_Configuration_Reject(AVDTP_BAD_STATE_ERROR) return Set_Configuration_Reject(AVDTP_BAD_STATE_ERROR)
result = self.local_endpoint.on_set_configuration_command(configuration) result = self.local_endpoint.on_set_configuration_command(configuration)
if result is not None: if result is not None:
return result return result
self.change_state(AVDTP_CONFIGURED_STATE) self.change_state(State.CONFIGURED)
return None return None
def on_get_configuration_command(self): def on_get_configuration_command(self):
if self.state not in ( if self.state not in (
AVDTP_CONFIGURED_STATE, State.CONFIGURED,
AVDTP_OPEN_STATE, State.OPEN,
AVDTP_STREAMING_STATE, State.STREAMING,
): ):
return Get_Configuration_Reject(AVDTP_BAD_STATE_ERROR) return Get_Configuration_Reject(AVDTP_BAD_STATE_ERROR)
return self.local_endpoint.on_get_configuration_command() return self.local_endpoint.on_get_configuration_command()
def on_reconfigure_command(self, configuration): def on_reconfigure_command(self, configuration):
if self.state != AVDTP_OPEN_STATE: if self.state != State.OPEN:
return Reconfigure_Reject(AVDTP_BAD_STATE_ERROR) return Reconfigure_Reject(AVDTP_BAD_STATE_ERROR)
result = self.local_endpoint.on_reconfigure_command(configuration) result = self.local_endpoint.on_reconfigure_command(configuration)
@@ -1963,7 +1941,7 @@ class Stream:
return None return None
def on_open_command(self): def on_open_command(self):
if self.state != AVDTP_CONFIGURED_STATE: if self.state != State.CONFIGURED:
return Open_Reject(AVDTP_BAD_STATE_ERROR) return Open_Reject(AVDTP_BAD_STATE_ERROR)
result = self.local_endpoint.on_open_command() result = self.local_endpoint.on_open_command()
@@ -1973,11 +1951,11 @@ class Stream:
# Register to accept the next channel # Register to accept the next channel
self.protocol.channel_acceptor = self self.protocol.channel_acceptor = self
self.change_state(AVDTP_OPEN_STATE) self.change_state(State.OPEN)
return None return None
def on_start_command(self): def on_start_command(self):
if self.state != AVDTP_OPEN_STATE: if self.state != State.OPEN:
return Open_Reject(AVDTP_BAD_STATE_ERROR) return Open_Reject(AVDTP_BAD_STATE_ERROR)
# Check that we have an RTP channel # Check that we have an RTP channel
@@ -1989,33 +1967,33 @@ class Stream:
if result is not None: if result is not None:
return result return result
self.change_state(AVDTP_STREAMING_STATE) self.change_state(State.STREAMING)
return None return None
def on_suspend_command(self): def on_suspend_command(self):
if self.state != AVDTP_STREAMING_STATE: if self.state != State.STREAMING:
return Open_Reject(AVDTP_BAD_STATE_ERROR) return Open_Reject(AVDTP_BAD_STATE_ERROR)
result = self.local_endpoint.on_suspend_command() result = self.local_endpoint.on_suspend_command()
if result is not None: if result is not None:
return result return result
self.change_state(AVDTP_OPEN_STATE) self.change_state(State.OPEN)
return None return None
def on_close_command(self): def on_close_command(self):
if self.state not in (AVDTP_OPEN_STATE, AVDTP_STREAMING_STATE): if self.state not in (State.OPEN, State.STREAMING):
return Open_Reject(AVDTP_BAD_STATE_ERROR) return Open_Reject(AVDTP_BAD_STATE_ERROR)
result = self.local_endpoint.on_close_command() result = self.local_endpoint.on_close_command()
if result is not None: if result is not None:
return result return result
self.change_state(AVDTP_CLOSING_STATE) self.change_state(State.CLOSING)
if self.rtp_channel is None: if self.rtp_channel is None:
# No channel to release, we're done # No channel to release, we're done
self.change_state(AVDTP_IDLE_STATE) self.change_state(State.IDLE)
else: else:
# TODO: set a timer as we wait for the RTP channel to be closed # TODO: set a timer as we wait for the RTP channel to be closed
pass pass
@@ -2025,10 +2003,10 @@ class Stream:
def on_abort_command(self): def on_abort_command(self):
if self.rtp_channel is None: if self.rtp_channel is None:
# No need to wait # No need to wait
self.change_state(AVDTP_IDLE_STATE) self.change_state(State.IDLE)
else: else:
# Wait for the RTP channel to be closed # Wait for the RTP channel to be closed
self.change_state(AVDTP_ABORTING_STATE) self.change_state(State.ABORTING)
def on_l2cap_connection(self, channel: l2cap.ClassicChannel) -> None: def on_l2cap_connection(self, channel: l2cap.ClassicChannel) -> None:
logger.debug(color('<<< stream channel connected', 'magenta')) logger.debug(color('<<< stream channel connected', 'magenta'))
@@ -2049,8 +2027,8 @@ class Stream:
self.local_endpoint.in_use = 0 self.local_endpoint.in_use = 0
self.rtp_channel = None self.rtp_channel = None
if self.state in (AVDTP_CLOSING_STATE, AVDTP_ABORTING_STATE): if self.state in (State.CLOSING, State.ABORTING):
self.change_state(AVDTP_IDLE_STATE) self.change_state(State.IDLE)
else: else:
logger.warning('unexpected channel close while not CLOSING or ABORTING') logger.warning('unexpected channel close while not CLOSING or ABORTING')
@@ -2068,7 +2046,7 @@ class Stream:
self.local_endpoint = local_endpoint self.local_endpoint = local_endpoint
self.remote_endpoint = remote_endpoint self.remote_endpoint = remote_endpoint
self.rtp_channel = None self.rtp_channel = None
self.state = AVDTP_IDLE_STATE self.state = State.IDLE
local_endpoint.stream = self local_endpoint.stream = self
local_endpoint.in_use = 1 local_endpoint.in_use = 1
@@ -2076,7 +2054,7 @@ class Stream:
def __str__(self) -> str: def __str__(self) -> str:
return ( return (
f'Stream({self.local_endpoint.seid} -> ' f'Stream({self.local_endpoint.seid} -> '
f'{self.remote_endpoint.seid} {self.state_name(self.state)})' f'{self.remote_endpoint.seid} {self.state.name})'
) )
@@ -2085,7 +2063,7 @@ class Stream:
class StreamEndPoint: class StreamEndPoint:
seid: int seid: int
media_type: MediaType media_type: MediaType
tsep: int tsep: StreamEndPointType
in_use: int in_use: int
capabilities: Iterable[ServiceCapabilities] capabilities: Iterable[ServiceCapabilities]
@@ -2124,7 +2102,7 @@ class DiscoveredStreamEndPoint(StreamEndPoint, StreamEndPointProxy):
protocol: Protocol, protocol: Protocol,
seid: int, seid: int,
media_type: MediaType, media_type: MediaType,
tsep: int, tsep: StreamEndPointType,
in_use: int, in_use: int,
capabilities: Iterable[ServiceCapabilities], capabilities: Iterable[ServiceCapabilities],
) -> None: ) -> None:
@@ -2154,7 +2132,7 @@ class LocalStreamEndPoint(StreamEndPoint, utils.EventEmitter):
protocol: Protocol, protocol: Protocol,
seid: int, seid: int,
media_type: MediaType, media_type: MediaType,
tsep: int, tsep: StreamEndPointType,
capabilities: Iterable[ServiceCapabilities], capabilities: Iterable[ServiceCapabilities],
configuration: Optional[Iterable[ServiceCapabilities]] = None, configuration: Optional[Iterable[ServiceCapabilities]] = None,
): ):

View File

@@ -23,17 +23,7 @@ from typing import Awaitable
import pytest import pytest
from bumble import a2dp from bumble import a2dp, avdtp
from bumble.avdtp import (
AVDTP_AUDIO_MEDIA_TYPE,
AVDTP_IDLE_STATE,
AVDTP_STREAMING_STATE,
AVDTP_TSEP_SNK,
Listener,
MediaCodecCapabilities,
MediaPacketPump,
Protocol,
)
from bumble.controller import Controller from bumble.controller import Controller
from bumble.core import PhysicalTransport from bumble.core import PhysicalTransport
from bumble.device import Device from bumble.device import Device
@@ -134,8 +124,8 @@ async def test_self_connection():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def source_codec_capabilities(): def source_codec_capabilities():
return MediaCodecCapabilities( return avdtp.MediaCodecCapabilities(
media_type=AVDTP_AUDIO_MEDIA_TYPE, media_type=avdtp.MediaType.AUDIO,
media_codec_type=a2dp.CodecType.SBC, media_codec_type=a2dp.CodecType.SBC,
media_codec_information=a2dp.SbcMediaCodecInformation( media_codec_information=a2dp.SbcMediaCodecInformation(
sampling_frequency=a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_44100, sampling_frequency=a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_44100,
@@ -151,8 +141,8 @@ def source_codec_capabilities():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def sink_codec_capabilities(): def sink_codec_capabilities():
return MediaCodecCapabilities( return avdtp.MediaCodecCapabilities(
media_type=AVDTP_AUDIO_MEDIA_TYPE, media_type=avdtp.MediaType.AUDIO,
media_codec_type=a2dp.CodecType.SBC, media_codec_type=a2dp.CodecType.SBC,
media_codec_information=a2dp.SbcMediaCodecInformation( media_codec_information=a2dp.SbcMediaCodecInformation(
sampling_frequency=a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_48000 sampling_frequency=a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_48000
@@ -200,7 +190,7 @@ async def test_source_sink_1():
sink.on('rtp_packet', on_rtp_packet) sink.on('rtp_packet', on_rtp_packet)
# Create a listener to wait for AVDTP connections # Create a listener to wait for AVDTP connections
listener = Listener.for_device(two_devices.devices[1]) listener = avdtp.Listener.for_device(two_devices.devices[1])
listener.on('connection', on_avdtp_connection) listener.on('connection', on_avdtp_connection)
async def make_connection(): async def make_connection():
@@ -213,13 +203,13 @@ async def test_source_sink_1():
return connections[0] return connections[0]
connection = await make_connection() connection = await make_connection()
client = await Protocol.connect(connection) client = await avdtp.Protocol.connect(connection)
endpoints = await client.discover_remote_endpoints() endpoints = await client.discover_remote_endpoints()
assert len(endpoints) == 1 assert len(endpoints) == 1
remote_sink = list(endpoints)[0] remote_sink = list(endpoints)[0]
assert remote_sink.in_use == 0 assert remote_sink.in_use == 0
assert remote_sink.media_type == AVDTP_AUDIO_MEDIA_TYPE assert remote_sink.media_type == avdtp.MediaType.AUDIO
assert remote_sink.tsep == AVDTP_TSEP_SNK assert remote_sink.tsep == avdtp.StreamEndPointType.SNK
async def generate_packets(packet_count): async def generate_packets(packet_count):
sequence_number = 0 sequence_number = 0
@@ -238,24 +228,24 @@ async def test_source_sink_1():
rtp_packets_fully_received = asyncio.get_running_loop().create_future() rtp_packets_fully_received = asyncio.get_running_loop().create_future()
rtp_packets_expected = 3 rtp_packets_expected = 3
rtp_packets = [] rtp_packets = []
pump = MediaPacketPump(generate_packets(3)) pump = avdtp.MediaPacketPump(generate_packets(3))
source = client.add_source(source_codec_capabilities(), pump) source = client.add_source(source_codec_capabilities(), pump)
stream = await client.create_stream(source, remote_sink) stream = await client.create_stream(source, remote_sink)
await stream.start() await stream.start()
assert stream.state == AVDTP_STREAMING_STATE assert stream.state == avdtp.State.STREAMING
assert stream.local_endpoint.in_use == 1 assert stream.local_endpoint.in_use == 1
assert stream.rtp_channel is not None assert stream.rtp_channel is not None
assert sink.in_use == 1 assert sink.in_use == 1
assert sink.stream is not None assert sink.stream is not None
assert sink.stream.state == AVDTP_STREAMING_STATE assert sink.stream.state == avdtp.State.STREAMING
await rtp_packets_fully_received await rtp_packets_fully_received
await stream.close() await stream.close()
assert stream.rtp_channel is None assert stream.rtp_channel is None
assert source.in_use == 0 assert source.in_use == 0
assert source.stream.state == AVDTP_IDLE_STATE assert source.stream.state == avdtp.State.IDLE
assert sink.in_use == 0 assert sink.in_use == 0
assert sink.stream.state == AVDTP_IDLE_STATE assert sink.stream.state == avdtp.State.IDLE
# Send packets manually # Send packets manually
rtp_packets_fully_received = asyncio.get_running_loop().create_future() rtp_packets_fully_received = asyncio.get_running_loop().create_future()
@@ -267,12 +257,12 @@ async def test_source_sink_1():
source = client.add_source(source_codec_capabilities(), None) source = client.add_source(source_codec_capabilities(), None)
stream = await client.create_stream(source, remote_sink) stream = await client.create_stream(source, remote_sink)
await stream.start() await stream.start()
assert stream.state == AVDTP_STREAMING_STATE assert stream.state == avdtp.State.STREAMING
assert stream.local_endpoint.in_use == 1 assert stream.local_endpoint.in_use == 1
assert stream.rtp_channel is not None assert stream.rtp_channel is not None
assert sink.in_use == 1 assert sink.in_use == 1
assert sink.stream is not None assert sink.stream is not None
assert sink.stream.state == AVDTP_STREAMING_STATE assert sink.stream.state == avdtp.State.STREAMING
stream.send_media_packet(source_packets[0]) stream.send_media_packet(source_packets[0])
stream.send_media_packet(source_packets[1]) stream.send_media_packet(source_packets[1])
@@ -282,9 +272,9 @@ async def test_source_sink_1():
assert stream.rtp_channel is None assert stream.rtp_channel is None
assert len(rtp_packets) == 3 assert len(rtp_packets) == 3
assert source.in_use == 0 assert source.in_use == 0
assert source.stream.state == AVDTP_IDLE_STATE assert source.stream.state == avdtp.State.IDLE
assert sink.in_use == 0 assert sink.in_use == 0
assert sink.stream.state == AVDTP_IDLE_STATE assert sink.stream.state == avdtp.State.IDLE
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -30,7 +30,10 @@ from bumble.rtp import MediaPacket
avdtp.Discover_Response( avdtp.Discover_Response(
endpoints=[ endpoints=[
avdtp.EndPointInfo( avdtp.EndPointInfo(
seid=1, in_use=1, media_type=avdtp.MediaType.AUDIO, tsep=1 seid=1,
in_use=1,
media_type=avdtp.MediaType.AUDIO,
tsep=avdtp.StreamEndPointType.SNK,
) )
] ]
), ),