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