diff --git a/apps/auracast.py b/apps/auracast.py index 6e591eb..d96e021 100644 --- a/apps/auracast.py +++ b/apps/auracast.py @@ -495,6 +495,10 @@ async def run_assist( print(color('Scanning for any broadcast', 'cyan')) broadcast = await find_broadcast_by_name(device, broadcast_name) + if broadcast.broadcast_audio_announcement is None: + print(color('No broadcast audio announcement found', 'red')) + return + if ( broadcast.basic_audio_announcement is None or not broadcast.basic_audio_announcement.subgroups diff --git a/apps/lea_unicast/app.py b/apps/lea_unicast/app.py index b1f2b3d..5885dab 100644 --- a/apps/lea_unicast/app.py +++ b/apps/lea_unicast/app.py @@ -484,7 +484,7 @@ class Speaker: ) ) + bytes(bap.UnicastServerAdvertisingData()) - def on_pdu(pdu: HCI_IsoDataPacket, ase: bap.AseStateMachine): + def on_pdu(pdu: HCI_IsoDataPacket, ase: ascs.AseStateMachine): codec_config = ase.codec_specific_configuration assert isinstance(codec_config, bap.CodecSpecificConfiguration) pcm = decode( @@ -494,7 +494,7 @@ class Speaker: ) self.device.abort_on('disconnection', self.ui_server.send_audio(pcm)) - def on_ase_state_change(ase: bap.AseStateMachine) -> None: + def on_ase_state_change(ase: ascs.AseStateMachine) -> None: if ase.state == ascs.AseStateMachine.State.STREAMING: codec_config = ase.codec_specific_configuration assert isinstance(codec_config, bap.CodecSpecificConfiguration) diff --git a/bumble/device.py b/bumble/device.py index b60f296..b04f5dd 100644 --- a/bumble/device.py +++ b/bumble/device.py @@ -988,7 +988,8 @@ class PeriodicAdvertisingSync(EventEmitter): self.device.periodic_advertising_syncs.remove(self) async def transfer(self, connection: Connection, service_data: int = 0) -> None: - await connection.transfer_periodic_sync(self.sync_handle, service_data) + if self.sync_handle is not None: + await connection.transfer_periodic_sync(self.sync_handle, service_data) def on_establishment( self, @@ -3606,7 +3607,8 @@ class Device(CompositeEventEmitter): connection_handle=connection.handle, service_data=service_data, sync_handle=sync_handle, - ), check_result=True + ), + check_result=True, ) async def find_peer_by_name(self, name, transport=BT_LE_TRANSPORT): @@ -3837,6 +3839,7 @@ class Device(CompositeEventEmitter): if self.keystore is None: raise InvalidOperationError('no key store') + logger.debug(f'Looking up key for {connection.peer_address}') keys = await self.keystore.get(str(connection.peer_address)) if keys is None: raise InvalidOperationError('keys not found in key store') @@ -4315,6 +4318,12 @@ class Device(CompositeEventEmitter): role: int, connection_parameters: ConnectionParameters, ) -> None: + # Convert all-zeros addresses into None. + if self_resolvable_address == Address.ANY_RANDOM: + self_resolvable_address = None + if peer_resolvable_address == Address.ANY_RANDOM: + peer_resolvable_address = None + logger.debug( f'*** Connection: [0x{connection_handle:04X}] ' f'{peer_address} {"" if role is None else HCI_Constant.role_name(role)}' @@ -4374,12 +4383,6 @@ class Device(CompositeEventEmitter): else self.random_address ) - # Convert all-zeros addresses into None. - if self_resolvable_address == Address.ANY_RANDOM: - self_resolvable_address = None - if peer_resolvable_address == Address.ANY_RANDOM: - peer_resolvable_address = None - # Create a connection. connection = Connection( self, diff --git a/bumble/profiles/ascs.py b/bumble/profiles/ascs.py index 98b4b8b..35f4594 100644 --- a/bumble/profiles/ascs.py +++ b/bumble/profiles/ascs.py @@ -24,6 +24,7 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union from bumble import colors from bumble.profiles.bap import CodecSpecificConfiguration +from bumble.profiles import le_audio from bumble import device from bumble import gatt from bumble import gatt_client @@ -299,8 +300,7 @@ class AseStateMachine(gatt.Characteristic): presentation_delay = 0 # Additional parameters in ENABLING, STREAMING, DISABLING State - # TODO: Parse this - metadata = b'' + metadata = le_audio.Metadata() def __init__( self, @@ -447,7 +447,7 @@ class AseStateMachine(gatt.Characteristic): AseReasonCode.NONE, ) - self.metadata = metadata + self.metadata = le_audio.Metadata.from_bytes(metadata) self.state = self.State.ENABLING return (AseResponseCode.SUCCESS, AseReasonCode.NONE) @@ -499,7 +499,7 @@ class AseStateMachine(gatt.Characteristic): AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION, AseReasonCode.NONE, ) - self.metadata = metadata + self.metadata = le_audio.Metadata.from_bytes(metadata) return (AseResponseCode.SUCCESS, AseReasonCode.NONE) def on_release(self) -> Tuple[AseResponseCode, AseReasonCode]: @@ -576,8 +576,9 @@ class AseStateMachine(gatt.Characteristic): self.State.STREAMING, self.State.DISABLING, ): + metadata_bytes = bytes(self.metadata) additional_parameters = ( - bytes([self.cig_id, self.cis_id, len(self.metadata)]) + self.metadata + bytes([self.cig_id, self.cis_id, len(metadata_bytes)]) + metadata_bytes ) else: additional_parameters = b'' diff --git a/bumble/profiles/pacs.py b/bumble/profiles/pacs.py index b36477d..adab088 100644 --- a/bumble/profiles/pacs.py +++ b/bumble/profiles/pacs.py @@ -23,6 +23,7 @@ import struct from typing import Optional, Sequence, Union from bumble.profiles.bap import AudioLocation, CodecSpecificCapabilities, ContextType +from bumble.profiles import le_audio from bumble import gatt from bumble import gatt_client from bumble import hci @@ -37,10 +38,11 @@ logger = logging.getLogger(__name__) # ----------------------------------------------------------------------------- @dataclasses.dataclass class PacRecord: + '''Published Audio Capabilities Service, Table 3.2/3.4.''' + coding_format: hci.CodingFormat codec_specific_capabilities: Union[CodecSpecificCapabilities, bytes] - # TODO: Parse Metadata - metadata: bytes = b'' + metadata: le_audio.Metadata = dataclasses.field(default_factory=le_audio.Metadata) @classmethod def from_bytes(cls, data: bytes) -> PacRecord: @@ -53,7 +55,8 @@ class PacRecord: ] offset += codec_specific_capabilities_size metadata_size = data[offset] - metadata = data[offset : offset + metadata_size] + offset += 1 + metadata = le_audio.Metadata.from_bytes(data[offset : offset + metadata_size]) codec_specific_capabilities: Union[CodecSpecificCapabilities, bytes] if coding_format.codec_id == hci.CodecID.VENDOR_SPECIFIC: @@ -71,12 +74,13 @@ class PacRecord: def __bytes__(self) -> bytes: capabilities_bytes = bytes(self.codec_specific_capabilities) + metadata_bytes = bytes(self.metadata) return ( bytes(self.coding_format) + bytes([len(capabilities_bytes)]) + capabilities_bytes - + bytes([len(self.metadata)]) - + self.metadata + + bytes([len(metadata_bytes)]) + + metadata_bytes ) diff --git a/examples/run_mcp_client.py b/examples/run_mcp_client.py index f0c8162..83dad5b 100644 --- a/examples/run_mcp_client.py +++ b/examples/run_mcp_client.py @@ -35,15 +35,13 @@ from bumble.hci import ( CodingFormat, OwnAddressType, ) +from bumble.profiles.ascs import AudioStreamControlService from bumble.profiles.bap import ( CodecSpecificCapabilities, ContextType, AudioLocation, SupportedSamplingFrequency, SupportedFrameDuration, - PacRecord, - PublishedAudioCapabilitiesService, - AudioStreamControlService, UnicastServerAdvertisingData, ) from bumble.profiles.mcp import ( @@ -52,7 +50,7 @@ from bumble.profiles.mcp import ( MediaState, MediaControlPointOpcode, ) - +from bumble.profiles.pacs import PacRecord, PublishedAudioCapabilitiesService from bumble.transport import open_transport_or_link from typing import Optional diff --git a/examples/run_unicast_server.py b/examples/run_unicast_server.py index 95ae551..3ff1c96 100644 --- a/examples/run_unicast_server.py +++ b/examples/run_unicast_server.py @@ -34,8 +34,8 @@ from bumble.hci import ( CodingFormat, HCI_IsoDataPacket, ) +from bumble.profiles.ascs import AseStateMachine, AudioStreamControlService from bumble.profiles.bap import ( - AseStateMachine, UnicastServerAdvertisingData, CodecSpecificConfiguration, CodecSpecificCapabilities, @@ -43,13 +43,10 @@ from bumble.profiles.bap import ( AudioLocation, SupportedSamplingFrequency, SupportedFrameDuration, - PacRecord, - PublishedAudioCapabilitiesService, - AudioStreamControlService, ) from bumble.profiles.cap import CommonAudioServiceService from bumble.profiles.csip import CoordinatedSetIdentificationService, SirkType - +from bumble.profiles.pacs import PacRecord, PublishedAudioCapabilitiesService from bumble.transport import open_transport_or_link diff --git a/examples/run_vcp_renderer.py b/examples/run_vcp_renderer.py index 0cffbae..ba9c840 100644 --- a/examples/run_vcp_renderer.py +++ b/examples/run_vcp_renderer.py @@ -30,6 +30,7 @@ from bumble.hci import ( CodingFormat, OwnAddressType, ) +from bumble.profiles.ascs import AudioStreamControlService from bumble.profiles.bap import ( UnicastServerAdvertisingData, CodecSpecificCapabilities, @@ -37,10 +38,8 @@ from bumble.profiles.bap import ( AudioLocation, SupportedSamplingFrequency, SupportedFrameDuration, - PacRecord, - PublishedAudioCapabilitiesService, - AudioStreamControlService, ) +from bumble.profiles.pacs import PacRecord, PublishedAudioCapabilitiesService from bumble.profiles.cap import CommonAudioServiceService from bumble.profiles.csip import CoordinatedSetIdentificationService, SirkType from bumble.profiles.vcp import VolumeControlService diff --git a/tests/bass_test.py b/tests/bass_test.py index 4cf7ec4..b893555 100644 --- a/tests/bass_test.py +++ b/tests/bass_test.py @@ -99,6 +99,7 @@ def test_operations() -> None: def basic_broadcast_receive_state_check(brs: bass.BroadcastReceiveState) -> None: serialized = bytes(brs) parsed = bass.BroadcastReceiveState.from_bytes(serialized) + assert parsed is not None assert bytes(parsed) == serialized