Merge pull request #803 from zxzxwu/avdtp

AVDTP: Migrate enums
This commit is contained in:
zxzxwu
2025-10-23 13:45:48 +08:00
committed by GitHub
3 changed files with 232 additions and 238 deletions

View File

@@ -35,13 +35,14 @@ from typing import (
cast,
)
from typing_extensions import override
from bumble import a2dp, device, hci, l2cap, sdp, utils
from bumble.colors import color
from bumble.core import (
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
InvalidStateError,
ProtocolError,
name_or_number,
)
from bumble.rtp import MediaPacket
@@ -62,74 +63,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 +139,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 +297,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 +307,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 +336,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 +392,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 +516,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 +524,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 +552,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 +580,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 +599,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 +609,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 +653,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 +718,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 +851,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 +1044,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 +1202,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):
@@ -1276,6 +1268,7 @@ class Protocol(utils.EventEmitter):
streams: dict[int, Stream]
transaction_results: list[Optional[asyncio.Future[Message]]]
channel_connector: Callable[[], Awaitable[l2cap.ClassicChannel]]
channel_acceptor: Optional[Stream]
EVENT_OPEN = "open"
EVENT_CLOSE = "close"
@@ -1286,10 +1279,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 +1451,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 +1634,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 +1661,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 +1827,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 +1836,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 +1863,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 +1875,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 +1888,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 +1902,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,32 +1910,36 @@ 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:
return Set_Configuration_Reject(AVDTP_BAD_STATE_ERROR)
def on_set_configuration_command(
self, configuration: Iterable[ServiceCapabilities]
) -> Optional[Message]:
if self.state != State.IDLE:
return Set_Configuration_Reject(error_code=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):
def on_get_configuration_command(self) -> Optional[Message]:
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 Get_Configuration_Reject(error_code=AVDTP_BAD_STATE_ERROR)
return self.local_endpoint.on_get_configuration_command()
def on_reconfigure_command(self, configuration):
if self.state != AVDTP_OPEN_STATE:
return Reconfigure_Reject(AVDTP_BAD_STATE_ERROR)
def on_reconfigure_command(
self, configuration: Iterable[ServiceCapabilities]
) -> Optional[Message]:
if self.state != State.OPEN:
return Reconfigure_Reject(error_code=AVDTP_BAD_STATE_ERROR)
result = self.local_endpoint.on_reconfigure_command(configuration)
if result is not None:
@@ -1962,8 +1947,8 @@ class Stream:
return None
def on_open_command(self):
if self.state != AVDTP_CONFIGURED_STATE:
def on_open_command(self) -> Optional[Message]:
if self.state != State.CONFIGURED:
return Open_Reject(AVDTP_BAD_STATE_ERROR)
result = self.local_endpoint.on_open_command()
@@ -1973,11 +1958,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:
def on_start_command(self) -> Optional[Message]:
if self.state != State.OPEN:
return Open_Reject(AVDTP_BAD_STATE_ERROR)
# Check that we have an RTP channel
@@ -1989,46 +1974,47 @@ 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:
def on_suspend_command(self) -> Optional[Message]:
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):
def on_close_command(self) -> Optional[Message]:
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
return None
def on_abort_command(self):
def on_abort_command(self) -> Optional[Message]:
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)
return None
def on_l2cap_connection(self, channel: l2cap.ClassicChannel) -> None:
logger.debug(color('<<< stream channel connected', 'magenta'))
@@ -2049,8 +2035,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 +2054,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 +2062,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 +2071,7 @@ class Stream:
class StreamEndPoint:
seid: int
media_type: MediaType
tsep: int
tsep: StreamEndPointType
in_use: int
capabilities: Iterable[ServiceCapabilities]
@@ -2124,7 +2110,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 +2140,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,
):
@@ -2173,10 +2159,15 @@ class LocalStreamEndPoint(StreamEndPoint, utils.EventEmitter):
async def close(self) -> None:
"""[Source Only] Handles when receiving close command."""
def on_reconfigure_command(self, command) -> Optional[Message]:
def on_reconfigure_command(
self, command: Iterable[ServiceCapabilities]
) -> Optional[Message]:
del command # unused.
return None
def on_set_configuration_command(self, configuration) -> Optional[Message]:
def on_set_configuration_command(
self, configuration: Iterable[ServiceCapabilities]
) -> Optional[Message]:
logger.debug(
'<<< received configuration: '
f'{",".join([str(capability) for capability in configuration])}'
@@ -2232,13 +2223,13 @@ class LocalSource(LocalStreamEndPoint):
protocol: Protocol,
seid: int,
codec_capabilities: MediaCodecCapabilities,
other_capabilitiles: Iterable[ServiceCapabilities],
other_capabilities: Iterable[ServiceCapabilities],
packet_pump: MediaPacketPump,
) -> None:
capabilities = [
ServiceCapabilities(AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY),
codec_capabilities,
] + list(other_capabilitiles)
] + list(other_capabilities)
super().__init__(
protocol,
seid,
@@ -2249,23 +2240,29 @@ class LocalSource(LocalStreamEndPoint):
)
self.packet_pump = packet_pump
@override
async def start(self) -> None:
if self.packet_pump and self.stream and self.stream.rtp_channel:
return await self.packet_pump.start(self.stream.rtp_channel)
self.emit(self.EVENT_START)
@override
async def stop(self) -> None:
if self.packet_pump:
return await self.packet_pump.stop()
self.emit(self.EVENT_STOP)
def on_start_command(self):
@override
def on_start_command(self) -> Optional[Message]:
asyncio.create_task(self.start())
return None
def on_suspend_command(self):
@override
def on_suspend_command(self) -> Optional[Message]:
asyncio.create_task(self.stop())
return None
# -----------------------------------------------------------------------------
@@ -2285,16 +2282,20 @@ class LocalSink(LocalStreamEndPoint):
capabilities,
)
def on_rtp_channel_open(self):
def on_rtp_channel_open(self) -> None:
logger.debug(color('<<< RTP channel open', 'magenta'))
if not self.stream:
raise InvalidStateError('Stream is None')
if not self.stream.rtp_channel:
raise InvalidStateError('RTP channel is None')
self.stream.rtp_channel.sink = self.on_avdtp_packet
super().on_rtp_channel_open()
def on_rtp_channel_close(self):
def on_rtp_channel_close(self) -> None:
logger.debug(color('<<< RTP channel close', 'magenta'))
super().on_rtp_channel_close()
def on_avdtp_packet(self, packet):
def on_avdtp_packet(self, packet: bytes) -> None:
rtp_packet = MediaPacket.from_bytes(packet)
logger.debug(
f'{color("<<< RTP Packet:", "green")} '

View File

@@ -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
# -----------------------------------------------------------------------------

View File

@@ -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,
)
]
),