mirror of
https://github.com/google/bumble.git
synced 2026-05-06 03:38:01 +00:00
Compare commits
16 Commits
gbg/hci-fi
...
v0.0.225
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
256a1a7405 | ||
|
|
116d9b26bb | ||
|
|
2d17a5f742 | ||
|
|
3894b14467 | ||
|
|
e62f947430 | ||
|
|
dcb8a4b607 | ||
|
|
7118328b07 | ||
|
|
5dc01d792a | ||
|
|
255f357975 | ||
|
|
c86920558b | ||
|
|
8e6efd0b2f | ||
|
|
34f5b81c7d | ||
|
|
d34d6a5c98 | ||
|
|
aedc971653 | ||
|
|
c6815fb820 | ||
|
|
85b78b46f8 |
12
apps/scan.py
12
apps/scan.py
@@ -22,7 +22,7 @@ import click
|
||||
import bumble.logging
|
||||
from bumble import data_types
|
||||
from bumble.colors import color
|
||||
from bumble.device import Advertisement, Device
|
||||
from bumble.device import Advertisement, Device, DeviceConfiguration
|
||||
from bumble.hci import HCI_LE_1M_PHY, HCI_LE_CODED_PHY, Address, HCI_Constant
|
||||
from bumble.keys import JsonKeyStore
|
||||
from bumble.smp import AddressResolver
|
||||
@@ -144,8 +144,14 @@ async def scan(
|
||||
device_config, hci_source, hci_sink
|
||||
)
|
||||
else:
|
||||
device = Device.with_hci(
|
||||
'Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink
|
||||
device = Device.from_config_with_hci(
|
||||
DeviceConfiguration(
|
||||
name='Bumble',
|
||||
address=Address('F0:F1:F2:F3:F4:F5'),
|
||||
keystore='JsonKeyStore',
|
||||
),
|
||||
hci_source,
|
||||
hci_sink,
|
||||
)
|
||||
|
||||
await device.power_on()
|
||||
|
||||
532
bumble/avrcp.py
532
bumble/avrcp.py
@@ -26,7 +26,7 @@ from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Sequen
|
||||
from dataclasses import dataclass, field
|
||||
from typing import ClassVar, SupportsBytes, TypeVar
|
||||
|
||||
from bumble import avc, avctp, core, hci, l2cap, utils
|
||||
from bumble import avc, avctp, core, hci, l2cap, sdp, utils
|
||||
from bumble.colors import color
|
||||
from bumble.device import Connection, Device
|
||||
from bumble.sdp import (
|
||||
@@ -194,82 +194,43 @@ class TargetFeatures(enum.IntFlag):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def make_controller_service_sdp_records(
|
||||
service_record_handle: int,
|
||||
avctp_version: tuple[int, int] = (1, 4),
|
||||
avrcp_version: tuple[int, int] = (1, 6),
|
||||
supported_features: int | ControllerFeatures = 1,
|
||||
) -> list[ServiceAttribute]:
|
||||
avctp_version_int = avctp_version[0] << 8 | avctp_version[1]
|
||||
avrcp_version_int = avrcp_version[0] << 8 | avrcp_version[1]
|
||||
@dataclass
|
||||
class ControllerServiceSdpRecord:
|
||||
service_record_handle: int
|
||||
avctp_version: tuple[int, int] = (1, 4)
|
||||
avrcp_version: tuple[int, int] = (1, 6)
|
||||
supported_features: int | ControllerFeatures = ControllerFeatures(1)
|
||||
|
||||
attributes = [
|
||||
ServiceAttribute(
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
DataElement.unsigned_integer_32(service_record_handle),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence([DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)]),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE),
|
||||
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_CONTROLLER_SERVICE),
|
||||
]
|
||||
),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(avctp.AVCTP_PSM),
|
||||
]
|
||||
),
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(core.BT_AVCTP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(avctp_version_int),
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE),
|
||||
DataElement.unsigned_integer_16(avrcp_version_int),
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
|
||||
DataElement.unsigned_integer_16(supported_features),
|
||||
),
|
||||
]
|
||||
if supported_features & ControllerFeatures.SUPPORTS_BROWSING:
|
||||
attributes.append(
|
||||
def to_service_attributes(self) -> list[ServiceAttribute]:
|
||||
avctp_version_int = self.avctp_version[0] << 8 | self.avctp_version[1]
|
||||
avrcp_version_int = self.avrcp_version[0] << 8 | self.avrcp_version[1]
|
||||
|
||||
attributes = [
|
||||
ServiceAttribute(
|
||||
SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
DataElement.unsigned_integer_32(self.service_record_handle),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence([DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)]),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE),
|
||||
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_CONTROLLER_SERVICE),
|
||||
]
|
||||
),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(
|
||||
avctp.AVCTP_BROWSING_PSM
|
||||
),
|
||||
DataElement.unsigned_integer_16(avctp.AVCTP_PSM),
|
||||
]
|
||||
),
|
||||
DataElement.sequence(
|
||||
@@ -281,87 +242,130 @@ def make_controller_service_sdp_records(
|
||||
]
|
||||
),
|
||||
),
|
||||
)
|
||||
return attributes
|
||||
ServiceAttribute(
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE),
|
||||
DataElement.unsigned_integer_16(avrcp_version_int),
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
|
||||
DataElement.unsigned_integer_16(self.supported_features),
|
||||
),
|
||||
]
|
||||
if self.supported_features & ControllerFeatures.SUPPORTS_BROWSING:
|
||||
attributes.append(
|
||||
ServiceAttribute(
|
||||
SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(
|
||||
avctp.AVCTP_BROWSING_PSM
|
||||
),
|
||||
]
|
||||
),
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(core.BT_AVCTP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(avctp_version_int),
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
)
|
||||
return attributes
|
||||
|
||||
@classmethod
|
||||
async def find(cls, connection: Connection) -> list[ControllerServiceSdpRecord]:
|
||||
async with sdp.Client(connection) as sdp_client:
|
||||
search_result = await sdp_client.search_attributes(
|
||||
uuids=[core.BT_AV_REMOTE_CONTROL_CONTROLLER_SERVICE],
|
||||
attribute_ids=[
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
|
||||
],
|
||||
)
|
||||
|
||||
records: list[ControllerServiceSdpRecord] = []
|
||||
for attribute_lists in search_result:
|
||||
record = cls(0)
|
||||
for attribute in attribute_lists:
|
||||
if attribute.id == SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID:
|
||||
record.service_record_handle = attribute.value.value
|
||||
elif attribute.id == SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID:
|
||||
# [[L2CAP, PSM], [AVCTP, version]]
|
||||
record.avctp_version = (
|
||||
attribute.value.value[1].value[1].value >> 8,
|
||||
attribute.value.value[1].value[1].value & 0xFF,
|
||||
)
|
||||
elif (
|
||||
attribute.id
|
||||
== SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID
|
||||
):
|
||||
# [[AV_REMOTE_CONTROL, version]]
|
||||
record.avrcp_version = (
|
||||
attribute.value.value[0].value[1].value >> 8,
|
||||
attribute.value.value[0].value[1].value & 0xFF,
|
||||
)
|
||||
elif attribute.id == SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID:
|
||||
record.supported_features = ControllerFeatures(
|
||||
attribute.value.value
|
||||
)
|
||||
records.append(record)
|
||||
return records
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def make_target_service_sdp_records(
|
||||
service_record_handle: int,
|
||||
avctp_version: tuple[int, int] = (1, 4),
|
||||
avrcp_version: tuple[int, int] = (1, 6),
|
||||
supported_features: int | TargetFeatures = 0x23,
|
||||
) -> list[ServiceAttribute]:
|
||||
# TODO: support a way to compute the supported features from a feature list
|
||||
avctp_version_int = avctp_version[0] << 8 | avctp_version[1]
|
||||
avrcp_version_int = avrcp_version[0] << 8 | avrcp_version[1]
|
||||
@dataclass
|
||||
class TargetServiceSdpRecord:
|
||||
service_record_handle: int
|
||||
avctp_version: tuple[int, int] = (1, 4)
|
||||
avrcp_version: tuple[int, int] = (1, 6)
|
||||
supported_features: int | TargetFeatures = TargetFeatures(0x23)
|
||||
|
||||
attributes = [
|
||||
ServiceAttribute(
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
DataElement.unsigned_integer_32(service_record_handle),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence([DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)]),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_TARGET_SERVICE),
|
||||
]
|
||||
),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(avctp.AVCTP_PSM),
|
||||
]
|
||||
),
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(core.BT_AVCTP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(avctp_version_int),
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE),
|
||||
DataElement.unsigned_integer_16(avrcp_version_int),
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
|
||||
DataElement.unsigned_integer_16(supported_features),
|
||||
),
|
||||
]
|
||||
if supported_features & TargetFeatures.SUPPORTS_BROWSING:
|
||||
attributes.append(
|
||||
def to_service_attributes(self) -> list[ServiceAttribute]:
|
||||
# TODO: support a way to compute the supported features from a feature list
|
||||
avctp_version_int = self.avctp_version[0] << 8 | self.avctp_version[1]
|
||||
avrcp_version_int = self.avrcp_version[0] << 8 | self.avrcp_version[1]
|
||||
|
||||
attributes = [
|
||||
ServiceAttribute(
|
||||
SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
DataElement.unsigned_integer_32(self.service_record_handle),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence([DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)]),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_TARGET_SERVICE),
|
||||
]
|
||||
),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(
|
||||
avctp.AVCTP_BROWSING_PSM
|
||||
),
|
||||
DataElement.unsigned_integer_16(avctp.AVCTP_PSM),
|
||||
]
|
||||
),
|
||||
DataElement.sequence(
|
||||
@@ -373,8 +377,90 @@ def make_target_service_sdp_records(
|
||||
]
|
||||
),
|
||||
),
|
||||
)
|
||||
return attributes
|
||||
ServiceAttribute(
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE),
|
||||
DataElement.unsigned_integer_16(avrcp_version_int),
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
|
||||
DataElement.unsigned_integer_16(self.supported_features),
|
||||
),
|
||||
]
|
||||
if self.supported_features & TargetFeatures.SUPPORTS_BROWSING:
|
||||
attributes.append(
|
||||
ServiceAttribute(
|
||||
SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(
|
||||
avctp.AVCTP_BROWSING_PSM
|
||||
),
|
||||
]
|
||||
),
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(core.BT_AVCTP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(avctp_version_int),
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
)
|
||||
return attributes
|
||||
|
||||
@classmethod
|
||||
async def find(cls, connection: Connection) -> list[TargetServiceSdpRecord]:
|
||||
async with sdp.Client(connection) as sdp_client:
|
||||
search_result = await sdp_client.search_attributes(
|
||||
uuids=[core.BT_AV_REMOTE_CONTROL_TARGET_SERVICE],
|
||||
attribute_ids=[
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
|
||||
],
|
||||
)
|
||||
|
||||
records: list[TargetServiceSdpRecord] = []
|
||||
for attribute_lists in search_result:
|
||||
record = cls(0)
|
||||
for attribute in attribute_lists:
|
||||
if attribute.id == SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID:
|
||||
record.service_record_handle = attribute.value.value
|
||||
elif attribute.id == SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID:
|
||||
# [[L2CAP, PSM], [AVCTP, version]]
|
||||
record.avctp_version = (
|
||||
attribute.value.value[1].value[1].value >> 8,
|
||||
attribute.value.value[1].value[1].value & 0xFF,
|
||||
)
|
||||
elif (
|
||||
attribute.id
|
||||
== SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID
|
||||
):
|
||||
# [[AV_REMOTE_CONTROL, version]]
|
||||
record.avrcp_version = (
|
||||
attribute.value.value[0].value[1].value >> 8,
|
||||
attribute.value.value[0].value[1].value & 0xFF,
|
||||
)
|
||||
elif attribute.id == SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID:
|
||||
record.supported_features = TargetFeatures(
|
||||
attribute.value.value
|
||||
)
|
||||
records.append(record)
|
||||
return records
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1204,6 +1290,10 @@ class InformBatteryStatusOfCtResponse(Response):
|
||||
@dataclass
|
||||
class GetPlayStatusResponse(Response):
|
||||
pdu_id = PduId.GET_PLAY_STATUS
|
||||
|
||||
# TG doesn't support Song Length or Position.
|
||||
UNAVAILABLE = 0xFFFFFFFF
|
||||
|
||||
song_length: int = field(metadata=hci.metadata(">4"))
|
||||
song_position: int = field(metadata=hci.metadata(">4"))
|
||||
play_status: PlayStatus = field(metadata=PlayStatus.type_metadata(1))
|
||||
@@ -1521,16 +1611,33 @@ class Delegate:
|
||||
def __init__(self, status_code: StatusCode) -> None:
|
||||
self.status_code = status_code
|
||||
|
||||
supported_events: list[EventId]
|
||||
volume: int
|
||||
class AvcError(Exception):
|
||||
"""The delegate AVC method failed, with a specified status code."""
|
||||
|
||||
def __init__(self, supported_events: Iterable[EventId] = ()) -> None:
|
||||
def __init__(self, status_code: avc.ResponseFrame.ResponseCode) -> None:
|
||||
self.status_code = status_code
|
||||
|
||||
supported_events: list[EventId]
|
||||
supported_company_ids: list[int]
|
||||
volume: int
|
||||
playback_status: PlayStatus
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
supported_events: Iterable[EventId] = (),
|
||||
supported_company_ids: Iterable[int] = (AVRCP_BLUETOOTH_SIG_COMPANY_ID,),
|
||||
) -> None:
|
||||
self.supported_company_ids = list(supported_company_ids)
|
||||
self.supported_events = list(supported_events)
|
||||
self.volume = 0
|
||||
self.playback_status = PlayStatus.STOPPED
|
||||
|
||||
async def get_supported_events(self) -> list[EventId]:
|
||||
return self.supported_events
|
||||
|
||||
async def get_supported_company_ids(self) -> list[int]:
|
||||
return self.supported_company_ids
|
||||
|
||||
async def set_absolute_volume(self, volume: int) -> None:
|
||||
"""
|
||||
Set the absolute volume.
|
||||
@@ -1543,6 +1650,19 @@ class Delegate:
|
||||
async def get_absolute_volume(self) -> int:
|
||||
return self.volume
|
||||
|
||||
async def on_key_event(
|
||||
self,
|
||||
key: avc.PassThroughFrame.OperationId,
|
||||
pressed: bool,
|
||||
data: bytes,
|
||||
) -> None:
|
||||
logger.debug(
|
||||
"@@@ on_key_event: key=%s, pressed=%s, data=%s", key, pressed, data.hex()
|
||||
)
|
||||
|
||||
async def get_playback_status(self) -> PlayStatus:
|
||||
return self.playback_status
|
||||
|
||||
# TODO add other delegate methods
|
||||
|
||||
|
||||
@@ -1756,6 +1876,19 @@ class Protocol(utils.EventEmitter):
|
||||
if isinstance(capability, EventId)
|
||||
)
|
||||
|
||||
async def get_supported_company_ids(self) -> list[int]:
|
||||
"""Get the list of events supported by the connected peer."""
|
||||
response_context = await self.send_avrcp_command(
|
||||
avc.CommandFrame.CommandType.STATUS,
|
||||
GetCapabilitiesCommand(GetCapabilitiesCommand.CapabilityId.COMPANY_ID),
|
||||
)
|
||||
response = self._check_response(response_context, GetCapabilitiesResponse)
|
||||
return list(
|
||||
int.from_bytes(capability, 'big')
|
||||
for capability in response.capabilities
|
||||
if isinstance(capability, bytes)
|
||||
)
|
||||
|
||||
async def get_play_status(self) -> SongAndPlayStatus:
|
||||
"""Get the play status of the connected peer."""
|
||||
response_context = await self.send_avrcp_command(
|
||||
@@ -2052,16 +2185,28 @@ class Protocol(utils.EventEmitter):
|
||||
return
|
||||
|
||||
if isinstance(command, avc.PassThroughCommandFrame):
|
||||
# TODO: delegate
|
||||
response = avc.PassThroughResponseFrame(
|
||||
avc.ResponseFrame.ResponseCode.ACCEPTED,
|
||||
command.subunit_type,
|
||||
command.subunit_id,
|
||||
command.state_flag,
|
||||
command.operation_id,
|
||||
command.operation_data,
|
||||
)
|
||||
self.send_response(transaction_label, response)
|
||||
|
||||
async def dispatch_key_event() -> None:
|
||||
try:
|
||||
await self.delegate.on_key_event(
|
||||
command.operation_id,
|
||||
command.state_flag == avc.PassThroughFrame.StateFlag.PRESSED,
|
||||
command.operation_data,
|
||||
)
|
||||
response_code = avc.ResponseFrame.ResponseCode.ACCEPTED
|
||||
except Delegate.AvcError as error:
|
||||
logger.exception("delegate method raised exception")
|
||||
response_code = error.status_code
|
||||
except Exception:
|
||||
logger.exception("delegate method raised exception")
|
||||
response_code = avc.ResponseFrame.ResponseCode.REJECTED
|
||||
self.send_passthrough_response(
|
||||
transaction_label=transaction_label,
|
||||
command=command,
|
||||
response_code=response_code,
|
||||
)
|
||||
|
||||
utils.AsyncRunner.spawn(dispatch_key_event())
|
||||
return
|
||||
|
||||
# TODO handle other types
|
||||
@@ -2141,6 +2286,8 @@ class Protocol(utils.EventEmitter):
|
||||
self._on_set_absolute_volume_command(transaction_label, command)
|
||||
elif isinstance(command, RegisterNotificationCommand):
|
||||
self._on_register_notification_command(transaction_label, command)
|
||||
elif isinstance(command, GetPlayStatusCommand):
|
||||
self._on_get_play_status_command(transaction_label, command)
|
||||
else:
|
||||
# Not supported.
|
||||
# TODO: check that this is the right way to respond in this case.
|
||||
@@ -2364,17 +2511,27 @@ class Protocol(utils.EventEmitter):
|
||||
logger.debug(f"<<< AVRCP command PDU: {command}")
|
||||
|
||||
async def get_supported_events() -> None:
|
||||
capabilities: Sequence[bytes | SupportsBytes]
|
||||
if (
|
||||
command.capability_id
|
||||
!= GetCapabilitiesCommand.CapabilityId.EVENTS_SUPPORTED
|
||||
== GetCapabilitiesCommand.CapabilityId.EVENTS_SUPPORTED
|
||||
):
|
||||
raise core.InvalidArgumentError()
|
||||
|
||||
supported_events = await self.delegate.get_supported_events()
|
||||
capabilities = await self.delegate.get_supported_events()
|
||||
elif (
|
||||
command.capability_id == GetCapabilitiesCommand.CapabilityId.COMPANY_ID
|
||||
):
|
||||
company_ids = await self.delegate.get_supported_company_ids()
|
||||
capabilities = [
|
||||
company_id.to_bytes(3, 'big') for company_id in company_ids
|
||||
]
|
||||
else:
|
||||
raise core.InvalidArgumentError(
|
||||
f"Unsupported capability: {command.capability_id}"
|
||||
)
|
||||
self.send_avrcp_response(
|
||||
transaction_label,
|
||||
avc.ResponseFrame.ResponseCode.IMPLEMENTED_OR_STABLE,
|
||||
GetCapabilitiesResponse(command.capability_id, supported_events),
|
||||
GetCapabilitiesResponse(command.capability_id, capabilities),
|
||||
)
|
||||
|
||||
self._delegate_command(transaction_label, command, get_supported_events())
|
||||
@@ -2395,6 +2552,26 @@ class Protocol(utils.EventEmitter):
|
||||
|
||||
self._delegate_command(transaction_label, command, set_absolute_volume())
|
||||
|
||||
def _on_get_play_status_command(
|
||||
self, transaction_label: int, command: GetPlayStatusCommand
|
||||
) -> None:
|
||||
logger.debug("<<< AVRCP command PDU: %s", command)
|
||||
|
||||
async def get_playback_status() -> None:
|
||||
play_status: PlayStatus = await self.delegate.get_playback_status()
|
||||
self.send_avrcp_response(
|
||||
transaction_label,
|
||||
avc.ResponseFrame.ResponseCode.IMPLEMENTED_OR_STABLE,
|
||||
GetPlayStatusResponse(
|
||||
# TODO: Delegate this.
|
||||
song_length=GetPlayStatusResponse.UNAVAILABLE,
|
||||
song_position=GetPlayStatusResponse.UNAVAILABLE,
|
||||
play_status=play_status,
|
||||
),
|
||||
)
|
||||
|
||||
self._delegate_command(transaction_label, command, get_playback_status())
|
||||
|
||||
def _on_register_notification_command(
|
||||
self, transaction_label: int, command: RegisterNotificationCommand
|
||||
) -> None:
|
||||
@@ -2410,28 +2587,27 @@ class Protocol(utils.EventEmitter):
|
||||
)
|
||||
return
|
||||
|
||||
response: Response
|
||||
if command.event_id == EventId.VOLUME_CHANGED:
|
||||
volume = await self.delegate.get_absolute_volume()
|
||||
response = RegisterNotificationResponse(VolumeChangedEvent(volume))
|
||||
self.send_avrcp_response(
|
||||
transaction_label,
|
||||
avc.ResponseFrame.ResponseCode.INTERIM,
|
||||
response,
|
||||
elif command.event_id == EventId.PLAYBACK_STATUS_CHANGED:
|
||||
playback_status = await self.delegate.get_playback_status()
|
||||
response = RegisterNotificationResponse(
|
||||
PlaybackStatusChangedEvent(play_status=playback_status)
|
||||
)
|
||||
self._register_notification_listener(transaction_label, command)
|
||||
elif command.event_id == EventId.NOW_PLAYING_CONTENT_CHANGED:
|
||||
playback_status = await self.delegate.get_playback_status()
|
||||
response = RegisterNotificationResponse(NowPlayingContentChangedEvent())
|
||||
else:
|
||||
logger.warning("Event supported but not handled %s", command.event_id)
|
||||
return
|
||||
|
||||
if command.event_id == EventId.PLAYBACK_STATUS_CHANGED:
|
||||
# TODO: testing only, use delegate
|
||||
response = RegisterNotificationResponse(
|
||||
PlaybackStatusChangedEvent(play_status=PlayStatus.PLAYING)
|
||||
)
|
||||
self.send_avrcp_response(
|
||||
transaction_label,
|
||||
avc.ResponseFrame.ResponseCode.INTERIM,
|
||||
response,
|
||||
)
|
||||
self._register_notification_listener(transaction_label, command)
|
||||
return
|
||||
self.send_avrcp_response(
|
||||
transaction_label,
|
||||
avc.ResponseFrame.ResponseCode.INTERIM,
|
||||
response,
|
||||
)
|
||||
self._register_notification_listener(transaction_label, command)
|
||||
|
||||
self._delegate_command(transaction_label, command, register_notification())
|
||||
|
||||
574
bumble/device.py
574
bumble/device.py
@@ -3748,6 +3748,292 @@ class Device(utils.CompositeEventEmitter):
|
||||
page_scan_enabled=self.connectable,
|
||||
)
|
||||
|
||||
async def connect_le(
|
||||
self,
|
||||
peer_address: hci.Address | str,
|
||||
connection_parameters_preferences: (
|
||||
dict[hci.Phy, ConnectionParametersPreferences] | None
|
||||
) = None,
|
||||
own_address_type: hci.OwnAddressType = hci.OwnAddressType.RANDOM,
|
||||
timeout: float | None = DEVICE_DEFAULT_CONNECT_TIMEOUT,
|
||||
) -> Connection:
|
||||
# Check that there isn't already a pending connection
|
||||
if self.is_le_connecting:
|
||||
raise InvalidStateError('connection already pending')
|
||||
|
||||
try_resolve = not self.address_resolution_offload
|
||||
if isinstance(peer_address, str):
|
||||
try:
|
||||
peer_address = hci.Address.from_string_for_transport(
|
||||
peer_address, PhysicalTransport.LE
|
||||
)
|
||||
except (InvalidArgumentError, ValueError):
|
||||
# If the address is not parsable, assume it is a name instead
|
||||
logger.debug('looking for peer by name')
|
||||
assert isinstance(peer_address, str)
|
||||
peer_address = await self.find_peer_by_name(
|
||||
peer_address, PhysicalTransport.LE
|
||||
) # TODO: timeout
|
||||
try_resolve = False
|
||||
|
||||
assert isinstance(peer_address, hci.Address)
|
||||
|
||||
if (
|
||||
try_resolve
|
||||
and self.address_resolver is not None
|
||||
and self.address_resolver.can_resolve_to(peer_address)
|
||||
):
|
||||
# If we have an IRK for this address, we should resolve.
|
||||
logger.debug('have IRK for address, resolving...')
|
||||
peer_address = await self.find_peer_by_identity_address(
|
||||
peer_address
|
||||
) # TODO: timeout
|
||||
|
||||
def on_connection(connection):
|
||||
pending_connection.set_result(connection)
|
||||
|
||||
def on_connection_failure(error: core.ConnectionError):
|
||||
pending_connection.set_exception(error)
|
||||
|
||||
# Create a future so that we can wait for the connection result
|
||||
pending_connection = asyncio.get_running_loop().create_future()
|
||||
self.on(self.EVENT_CONNECTION, on_connection)
|
||||
self.on(self.EVENT_CONNECTION_FAILURE, on_connection_failure)
|
||||
|
||||
try:
|
||||
# Tell the controller to connect
|
||||
if connection_parameters_preferences is None:
|
||||
connection_parameters_preferences = {
|
||||
hci.HCI_LE_1M_PHY: ConnectionParametersPreferences.default
|
||||
}
|
||||
|
||||
self.connect_own_address_type = own_address_type
|
||||
|
||||
if self.host.supports_command(
|
||||
hci.HCI_LE_EXTENDED_CREATE_CONNECTION_COMMAND
|
||||
):
|
||||
# Only keep supported PHYs
|
||||
phys = sorted(
|
||||
list(
|
||||
set(
|
||||
filter(
|
||||
self.supports_le_phy,
|
||||
connection_parameters_preferences.keys(),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
if not phys:
|
||||
raise InvalidArgumentError('at least one supported PHY needed')
|
||||
|
||||
phy_count = len(phys)
|
||||
initiating_phys = hci.phy_list_to_bits(phys)
|
||||
|
||||
connection_interval_mins = [
|
||||
int(
|
||||
connection_parameters_preferences[phy].connection_interval_min
|
||||
/ 1.25
|
||||
)
|
||||
for phy in phys
|
||||
]
|
||||
connection_interval_maxs = [
|
||||
int(
|
||||
connection_parameters_preferences[phy].connection_interval_max
|
||||
/ 1.25
|
||||
)
|
||||
for phy in phys
|
||||
]
|
||||
max_latencies = [
|
||||
connection_parameters_preferences[phy].max_latency for phy in phys
|
||||
]
|
||||
supervision_timeouts = [
|
||||
int(connection_parameters_preferences[phy].supervision_timeout / 10)
|
||||
for phy in phys
|
||||
]
|
||||
min_ce_lengths = [
|
||||
int(connection_parameters_preferences[phy].min_ce_length / 0.625)
|
||||
for phy in phys
|
||||
]
|
||||
max_ce_lengths = [
|
||||
int(connection_parameters_preferences[phy].max_ce_length / 0.625)
|
||||
for phy in phys
|
||||
]
|
||||
|
||||
await self.send_async_command(
|
||||
hci.HCI_LE_Extended_Create_Connection_Command(
|
||||
initiator_filter_policy=0,
|
||||
own_address_type=own_address_type,
|
||||
peer_address_type=peer_address.address_type,
|
||||
peer_address=peer_address,
|
||||
initiating_phys=initiating_phys,
|
||||
scan_intervals=(
|
||||
int(DEVICE_DEFAULT_CONNECT_SCAN_INTERVAL / 0.625),
|
||||
)
|
||||
* phy_count,
|
||||
scan_windows=(int(DEVICE_DEFAULT_CONNECT_SCAN_WINDOW / 0.625),)
|
||||
* phy_count,
|
||||
connection_interval_mins=connection_interval_mins,
|
||||
connection_interval_maxs=connection_interval_maxs,
|
||||
max_latencies=max_latencies,
|
||||
supervision_timeouts=supervision_timeouts,
|
||||
min_ce_lengths=min_ce_lengths,
|
||||
max_ce_lengths=max_ce_lengths,
|
||||
)
|
||||
)
|
||||
else:
|
||||
if hci.HCI_LE_1M_PHY not in connection_parameters_preferences:
|
||||
raise InvalidArgumentError('1M PHY preferences required')
|
||||
|
||||
prefs = connection_parameters_preferences[hci.HCI_LE_1M_PHY]
|
||||
await self.send_async_command(
|
||||
hci.HCI_LE_Create_Connection_Command(
|
||||
le_scan_interval=int(
|
||||
DEVICE_DEFAULT_CONNECT_SCAN_INTERVAL / 0.625
|
||||
),
|
||||
le_scan_window=int(DEVICE_DEFAULT_CONNECT_SCAN_WINDOW / 0.625),
|
||||
initiator_filter_policy=0,
|
||||
peer_address_type=peer_address.address_type,
|
||||
peer_address=peer_address,
|
||||
own_address_type=own_address_type,
|
||||
connection_interval_min=int(
|
||||
prefs.connection_interval_min / 1.25
|
||||
),
|
||||
connection_interval_max=int(
|
||||
prefs.connection_interval_max / 1.25
|
||||
),
|
||||
max_latency=prefs.max_latency,
|
||||
supervision_timeout=int(prefs.supervision_timeout / 10),
|
||||
min_ce_length=int(prefs.min_ce_length / 0.625),
|
||||
max_ce_length=int(prefs.max_ce_length / 0.625),
|
||||
)
|
||||
)
|
||||
|
||||
# Wait for the connection process to complete
|
||||
self.le_connecting = True
|
||||
|
||||
if timeout is None:
|
||||
return await utils.cancel_on_event(
|
||||
self, Device.EVENT_FLUSH, pending_connection
|
||||
)
|
||||
|
||||
try:
|
||||
return await asyncio.wait_for(
|
||||
asyncio.shield(pending_connection), timeout
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
await self.send_sync_command(
|
||||
hci.HCI_LE_Create_Connection_Cancel_Command()
|
||||
)
|
||||
|
||||
try:
|
||||
return await utils.cancel_on_event(
|
||||
self, Device.EVENT_FLUSH, pending_connection
|
||||
)
|
||||
except core.ConnectionError as error:
|
||||
raise core.TimeoutError() from error
|
||||
finally:
|
||||
self.remove_listener(self.EVENT_CONNECTION, on_connection)
|
||||
self.remove_listener(self.EVENT_CONNECTION_FAILURE, on_connection_failure)
|
||||
self.le_connecting = False
|
||||
self.connect_own_address_type = None
|
||||
|
||||
async def connect_classic(
|
||||
self,
|
||||
peer_address: hci.Address | str,
|
||||
timeout: float | None = DEVICE_DEFAULT_CONNECT_TIMEOUT,
|
||||
) -> Connection:
|
||||
if isinstance(peer_address, str):
|
||||
try:
|
||||
peer_address = hci.Address.from_string_for_transport(
|
||||
peer_address, PhysicalTransport.BR_EDR
|
||||
)
|
||||
except (InvalidArgumentError, ValueError):
|
||||
# If the address is not parsable, assume it is a name instead
|
||||
logger.debug('looking for peer by name')
|
||||
assert isinstance(peer_address, str)
|
||||
peer_address = await self.find_peer_by_name(
|
||||
peer_address, PhysicalTransport.BR_EDR
|
||||
) # TODO: timeout
|
||||
else:
|
||||
# All BR/EDR addresses should be public addresses
|
||||
if peer_address.address_type != hci.Address.PUBLIC_DEVICE_ADDRESS:
|
||||
raise InvalidArgumentError('BR/EDR addresses must be PUBLIC')
|
||||
|
||||
assert isinstance(peer_address, hci.Address)
|
||||
|
||||
def on_connection(connection):
|
||||
if (
|
||||
# match BR/EDR connection event against peer address
|
||||
connection.transport == PhysicalTransport.BR_EDR
|
||||
and connection.peer_address == peer_address
|
||||
):
|
||||
pending_connection.set_result(connection)
|
||||
|
||||
def on_connection_failure(error: core.ConnectionError):
|
||||
if (
|
||||
# match BR/EDR connection failure event against peer address
|
||||
error.transport == PhysicalTransport.BR_EDR
|
||||
and error.peer_address == peer_address
|
||||
):
|
||||
pending_connection.set_exception(error)
|
||||
|
||||
# Create a future so that we can wait for the connection result
|
||||
pending_connection = asyncio.get_running_loop().create_future()
|
||||
self.on(self.EVENT_CONNECTION, on_connection)
|
||||
self.on(self.EVENT_CONNECTION_FAILURE, on_connection_failure)
|
||||
|
||||
try:
|
||||
# Save pending connection
|
||||
self.pending_connections[peer_address] = Connection(
|
||||
device=self,
|
||||
handle=0,
|
||||
transport=core.PhysicalTransport.BR_EDR,
|
||||
self_address=self.public_address,
|
||||
self_resolvable_address=None,
|
||||
peer_address=peer_address,
|
||||
peer_resolvable_address=None,
|
||||
role=hci.Role.CENTRAL,
|
||||
parameters=Connection.Parameters(0, 0, 0),
|
||||
)
|
||||
|
||||
# TODO: allow passing other settings
|
||||
await self.send_async_command(
|
||||
hci.HCI_Create_Connection_Command(
|
||||
bd_addr=peer_address,
|
||||
packet_type=0xCC18, # FIXME: change
|
||||
page_scan_repetition_mode=hci.HCI_R2_PAGE_SCAN_REPETITION_MODE,
|
||||
clock_offset=0x0000,
|
||||
allow_role_switch=0x01,
|
||||
reserved=0,
|
||||
)
|
||||
)
|
||||
|
||||
# Wait for the connection process to complete
|
||||
if timeout is None:
|
||||
return await utils.cancel_on_event(
|
||||
self, Device.EVENT_FLUSH, pending_connection
|
||||
)
|
||||
|
||||
try:
|
||||
return await asyncio.wait_for(
|
||||
asyncio.shield(pending_connection), timeout
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
await self.send_sync_command(
|
||||
hci.HCI_Create_Connection_Cancel_Command(bd_addr=peer_address)
|
||||
)
|
||||
|
||||
try:
|
||||
return await utils.cancel_on_event(
|
||||
self, Device.EVENT_FLUSH, pending_connection
|
||||
)
|
||||
except core.ConnectionError as error:
|
||||
raise core.TimeoutError() from error
|
||||
finally:
|
||||
self.remove_listener(self.EVENT_CONNECTION, on_connection)
|
||||
self.remove_listener(self.EVENT_CONNECTION_FAILURE, on_connection_failure)
|
||||
self.pending_connections.pop(peer_address, None)
|
||||
|
||||
async def connect(
|
||||
self,
|
||||
peer_address: hci.Address | str,
|
||||
@@ -3769,9 +4055,9 @@ class Device(utils.CompositeEventEmitter):
|
||||
peer_address:
|
||||
hci.Address or name of the device to connect to.
|
||||
If a string is passed:
|
||||
If the string is an address followed by a `@` suffix, the `always_resolve`
|
||||
argument is implicitly set to True, so the connection is made to the
|
||||
address after resolution.
|
||||
[deprecated] If the string is an address followed by a `@` suffix, the
|
||||
`always_resolve`argument is implicitly set to True, so the connection is
|
||||
made to the address after resolution.
|
||||
If the string is any other address, the connection is made to that
|
||||
address (with or without address resolution, depending on the
|
||||
`always_resolve` argument).
|
||||
@@ -3795,271 +4081,29 @@ class Device(utils.CompositeEventEmitter):
|
||||
Pass None for an unlimited time.
|
||||
|
||||
always_resolve:
|
||||
(BLE only, ignored for BR/EDR)
|
||||
If True, always initiate a scan, resolving addresses, and connect to the
|
||||
address that resolves to `peer_address`.
|
||||
[deprecated] (ignore)
|
||||
'''
|
||||
|
||||
# Check parameters
|
||||
if transport not in (PhysicalTransport.LE, PhysicalTransport.BR_EDR):
|
||||
raise InvalidArgumentError('invalid transport')
|
||||
transport = core.PhysicalTransport(transport)
|
||||
# Connect using the appropriate transport
|
||||
# (auto-correct the transport based on declared capabilities)
|
||||
if transport == PhysicalTransport.LE or (
|
||||
self.le_enabled and not self.classic_enabled
|
||||
):
|
||||
return await self.connect_le(
|
||||
peer_address=peer_address,
|
||||
connection_parameters_preferences=connection_parameters_preferences,
|
||||
own_address_type=own_address_type,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
# Adjust the transport automatically if we need to
|
||||
if transport == PhysicalTransport.LE and not self.le_enabled:
|
||||
transport = PhysicalTransport.BR_EDR
|
||||
elif transport == PhysicalTransport.BR_EDR and not self.classic_enabled:
|
||||
transport = PhysicalTransport.LE
|
||||
if transport == PhysicalTransport.BR_EDR or (
|
||||
self.classic_enabled and not self.le_enabled
|
||||
):
|
||||
return await self.connect_classic(
|
||||
peer_address=peer_address, timeout=timeout
|
||||
)
|
||||
|
||||
# Check that there isn't already a pending connection
|
||||
if transport == PhysicalTransport.LE and self.is_le_connecting:
|
||||
raise InvalidStateError('connection already pending')
|
||||
|
||||
if isinstance(peer_address, str):
|
||||
try:
|
||||
if transport == PhysicalTransport.LE and peer_address.endswith('@'):
|
||||
peer_address = hci.Address.from_string_for_transport(
|
||||
peer_address[:-1], transport
|
||||
)
|
||||
always_resolve = True
|
||||
logger.debug('forcing address resolution')
|
||||
else:
|
||||
peer_address = hci.Address.from_string_for_transport(
|
||||
peer_address, transport
|
||||
)
|
||||
except (InvalidArgumentError, ValueError):
|
||||
# If the address is not parsable, assume it is a name instead
|
||||
always_resolve = False
|
||||
logger.debug('looking for peer by name')
|
||||
assert isinstance(peer_address, str)
|
||||
peer_address = await self.find_peer_by_name(
|
||||
peer_address, transport
|
||||
) # TODO: timeout
|
||||
else:
|
||||
# All BR/EDR addresses should be public addresses
|
||||
if (
|
||||
transport == PhysicalTransport.BR_EDR
|
||||
and peer_address.address_type != hci.Address.PUBLIC_DEVICE_ADDRESS
|
||||
):
|
||||
raise InvalidArgumentError('BR/EDR addresses must be PUBLIC')
|
||||
|
||||
assert isinstance(peer_address, hci.Address)
|
||||
|
||||
if transport == PhysicalTransport.LE and always_resolve:
|
||||
logger.debug('resolving address')
|
||||
peer_address = await self.find_peer_by_identity_address(
|
||||
peer_address
|
||||
) # TODO: timeout
|
||||
|
||||
def on_connection(connection):
|
||||
if transport == PhysicalTransport.LE or (
|
||||
# match BR/EDR connection event against peer address
|
||||
connection.transport == transport
|
||||
and connection.peer_address == peer_address
|
||||
):
|
||||
pending_connection.set_result(connection)
|
||||
|
||||
def on_connection_failure(error: core.ConnectionError):
|
||||
if transport == PhysicalTransport.LE or (
|
||||
# match BR/EDR connection failure event against peer address
|
||||
error.transport == transport
|
||||
and error.peer_address == peer_address
|
||||
):
|
||||
pending_connection.set_exception(error)
|
||||
|
||||
# Create a future so that we can wait for the connection's result
|
||||
pending_connection = asyncio.get_running_loop().create_future()
|
||||
self.on(self.EVENT_CONNECTION, on_connection)
|
||||
self.on(self.EVENT_CONNECTION_FAILURE, on_connection_failure)
|
||||
|
||||
try:
|
||||
# Tell the controller to connect
|
||||
if transport == PhysicalTransport.LE:
|
||||
if connection_parameters_preferences is None:
|
||||
if connection_parameters_preferences is None:
|
||||
connection_parameters_preferences = {
|
||||
hci.HCI_LE_1M_PHY: ConnectionParametersPreferences.default
|
||||
}
|
||||
|
||||
self.connect_own_address_type = own_address_type
|
||||
|
||||
if self.host.supports_command(
|
||||
hci.HCI_LE_EXTENDED_CREATE_CONNECTION_COMMAND
|
||||
):
|
||||
# Only keep supported PHYs
|
||||
phys = sorted(
|
||||
list(
|
||||
set(
|
||||
filter(
|
||||
self.supports_le_phy,
|
||||
connection_parameters_preferences.keys(),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
if not phys:
|
||||
raise InvalidArgumentError('at least one supported PHY needed')
|
||||
|
||||
phy_count = len(phys)
|
||||
initiating_phys = hci.phy_list_to_bits(phys)
|
||||
|
||||
connection_interval_mins = [
|
||||
int(
|
||||
connection_parameters_preferences[
|
||||
phy
|
||||
].connection_interval_min
|
||||
/ 1.25
|
||||
)
|
||||
for phy in phys
|
||||
]
|
||||
connection_interval_maxs = [
|
||||
int(
|
||||
connection_parameters_preferences[
|
||||
phy
|
||||
].connection_interval_max
|
||||
/ 1.25
|
||||
)
|
||||
for phy in phys
|
||||
]
|
||||
max_latencies = [
|
||||
connection_parameters_preferences[phy].max_latency
|
||||
for phy in phys
|
||||
]
|
||||
supervision_timeouts = [
|
||||
int(
|
||||
connection_parameters_preferences[phy].supervision_timeout
|
||||
/ 10
|
||||
)
|
||||
for phy in phys
|
||||
]
|
||||
min_ce_lengths = [
|
||||
int(
|
||||
connection_parameters_preferences[phy].min_ce_length / 0.625
|
||||
)
|
||||
for phy in phys
|
||||
]
|
||||
max_ce_lengths = [
|
||||
int(
|
||||
connection_parameters_preferences[phy].max_ce_length / 0.625
|
||||
)
|
||||
for phy in phys
|
||||
]
|
||||
|
||||
await self.send_async_command(
|
||||
hci.HCI_LE_Extended_Create_Connection_Command(
|
||||
initiator_filter_policy=0,
|
||||
own_address_type=own_address_type,
|
||||
peer_address_type=peer_address.address_type,
|
||||
peer_address=peer_address,
|
||||
initiating_phys=initiating_phys,
|
||||
scan_intervals=(
|
||||
int(DEVICE_DEFAULT_CONNECT_SCAN_INTERVAL / 0.625),
|
||||
)
|
||||
* phy_count,
|
||||
scan_windows=(
|
||||
int(DEVICE_DEFAULT_CONNECT_SCAN_WINDOW / 0.625),
|
||||
)
|
||||
* phy_count,
|
||||
connection_interval_mins=connection_interval_mins,
|
||||
connection_interval_maxs=connection_interval_maxs,
|
||||
max_latencies=max_latencies,
|
||||
supervision_timeouts=supervision_timeouts,
|
||||
min_ce_lengths=min_ce_lengths,
|
||||
max_ce_lengths=max_ce_lengths,
|
||||
)
|
||||
)
|
||||
else:
|
||||
if hci.HCI_LE_1M_PHY not in connection_parameters_preferences:
|
||||
raise InvalidArgumentError('1M PHY preferences required')
|
||||
|
||||
prefs = connection_parameters_preferences[hci.HCI_LE_1M_PHY]
|
||||
await self.send_async_command(
|
||||
hci.HCI_LE_Create_Connection_Command(
|
||||
le_scan_interval=int(
|
||||
DEVICE_DEFAULT_CONNECT_SCAN_INTERVAL / 0.625
|
||||
),
|
||||
le_scan_window=int(
|
||||
DEVICE_DEFAULT_CONNECT_SCAN_WINDOW / 0.625
|
||||
),
|
||||
initiator_filter_policy=0,
|
||||
peer_address_type=peer_address.address_type,
|
||||
peer_address=peer_address,
|
||||
own_address_type=own_address_type,
|
||||
connection_interval_min=int(
|
||||
prefs.connection_interval_min / 1.25
|
||||
),
|
||||
connection_interval_max=int(
|
||||
prefs.connection_interval_max / 1.25
|
||||
),
|
||||
max_latency=prefs.max_latency,
|
||||
supervision_timeout=int(prefs.supervision_timeout / 10),
|
||||
min_ce_length=int(prefs.min_ce_length / 0.625),
|
||||
max_ce_length=int(prefs.max_ce_length / 0.625),
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Save pending connection
|
||||
self.pending_connections[peer_address] = Connection(
|
||||
device=self,
|
||||
handle=0,
|
||||
transport=core.PhysicalTransport.BR_EDR,
|
||||
self_address=self.public_address,
|
||||
self_resolvable_address=None,
|
||||
peer_address=peer_address,
|
||||
peer_resolvable_address=None,
|
||||
role=hci.Role.CENTRAL,
|
||||
parameters=Connection.Parameters(0, 0, 0),
|
||||
)
|
||||
|
||||
# TODO: allow passing other settings
|
||||
await self.send_async_command(
|
||||
hci.HCI_Create_Connection_Command(
|
||||
bd_addr=peer_address,
|
||||
packet_type=0xCC18, # FIXME: change
|
||||
page_scan_repetition_mode=hci.HCI_R2_PAGE_SCAN_REPETITION_MODE,
|
||||
clock_offset=0x0000,
|
||||
allow_role_switch=0x01,
|
||||
reserved=0,
|
||||
)
|
||||
)
|
||||
|
||||
# Wait for the connection process to complete
|
||||
if transport == PhysicalTransport.LE:
|
||||
self.le_connecting = True
|
||||
|
||||
if timeout is None:
|
||||
return await utils.cancel_on_event(
|
||||
self, Device.EVENT_FLUSH, pending_connection
|
||||
)
|
||||
|
||||
try:
|
||||
return await asyncio.wait_for(
|
||||
asyncio.shield(pending_connection), timeout
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
if transport == PhysicalTransport.LE:
|
||||
await self.send_sync_command(
|
||||
hci.HCI_LE_Create_Connection_Cancel_Command()
|
||||
)
|
||||
else:
|
||||
await self.send_sync_command(
|
||||
hci.HCI_Create_Connection_Cancel_Command(bd_addr=peer_address)
|
||||
)
|
||||
|
||||
try:
|
||||
return await utils.cancel_on_event(
|
||||
self, Device.EVENT_FLUSH, pending_connection
|
||||
)
|
||||
except core.ConnectionError as error:
|
||||
raise core.TimeoutError() from error
|
||||
finally:
|
||||
self.remove_listener(self.EVENT_CONNECTION, on_connection)
|
||||
self.remove_listener(self.EVENT_CONNECTION_FAILURE, on_connection_failure)
|
||||
if transport == PhysicalTransport.LE:
|
||||
self.le_connecting = False
|
||||
self.connect_own_address_type = None
|
||||
else:
|
||||
self.pending_connections.pop(peer_address, None)
|
||||
raise ValueError('invalid transport')
|
||||
|
||||
async def accept(
|
||||
self,
|
||||
@@ -4706,6 +4750,8 @@ class Device(utils.CompositeEventEmitter):
|
||||
Scan for a peer with a resolvable address that can be resolved to a given
|
||||
identity address.
|
||||
"""
|
||||
if self.address_resolver is None:
|
||||
raise InvalidStateError('no resolver')
|
||||
|
||||
# Create a future to wait for an address to be found
|
||||
peer_address = asyncio.get_running_loop().create_future()
|
||||
|
||||
@@ -387,8 +387,8 @@ HCI_LE_PERIODIC_ADVERTISING_SYNC_TRANSFER_RECEIVED_V2_EVENT = 0X26
|
||||
HCI_LE_PERIODIC_ADVERTISING_SUBEVENT_DATA_REQUEST_EVENT = 0X27
|
||||
HCI_LE_PERIODIC_ADVERTISING_RESPONSE_REPORT_EVENT = 0X28
|
||||
HCI_LE_ENHANCED_CONNECTION_COMPLETE_V2_EVENT = 0X29
|
||||
HCI_LE_READ_ALL_REMOTE_FEATURES_COMPLETE_EVENT = 0x2A
|
||||
HCI_LE_CIS_ESTABLISHED_V2_EVENT = 0x2B
|
||||
HCI_LE_CIS_ESTABLISHED_V2_EVENT = 0x2A
|
||||
HCI_LE_READ_ALL_REMOTE_FEATURES_COMPLETE_EVENT = 0x2B
|
||||
HCI_LE_CS_READ_REMOTE_SUPPORTED_CAPABILITIES_COMPLETE_EVENT = 0x2C
|
||||
HCI_LE_CS_READ_REMOTE_FAE_TABLE_COMPLETE_EVENT = 0x2D
|
||||
HCI_LE_CS_SECURITY_ENABLE_COMPLETE_EVENT = 0x2E
|
||||
|
||||
@@ -616,22 +616,28 @@ class Host(utils.EventEmitter):
|
||||
if self.supports_command(
|
||||
hci.HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERTISING_SETS_COMMAND
|
||||
):
|
||||
response10 = await self.send_sync_command(
|
||||
hci.HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command()
|
||||
)
|
||||
self.number_of_supported_advertising_sets = (
|
||||
response10.num_supported_advertising_sets
|
||||
)
|
||||
try:
|
||||
response10 = await self.send_sync_command(
|
||||
hci.HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command()
|
||||
)
|
||||
self.number_of_supported_advertising_sets = (
|
||||
response10.num_supported_advertising_sets
|
||||
)
|
||||
except hci.HCI_Error:
|
||||
logger.warning('Failed to read number of supported advertising sets')
|
||||
|
||||
if self.supports_command(
|
||||
hci.HCI_LE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH_COMMAND
|
||||
):
|
||||
response11 = await self.send_sync_command(
|
||||
hci.HCI_LE_Read_Maximum_Advertising_Data_Length_Command()
|
||||
)
|
||||
self.maximum_advertising_data_length = (
|
||||
response11.max_advertising_data_length
|
||||
)
|
||||
try:
|
||||
response11 = await self.send_sync_command(
|
||||
hci.HCI_LE_Read_Maximum_Advertising_Data_Length_Command()
|
||||
)
|
||||
self.maximum_advertising_data_length = (
|
||||
response11.max_advertising_data_length
|
||||
)
|
||||
except hci.HCI_Error:
|
||||
logger.warning('Failed to read maximum advertising data length')
|
||||
|
||||
@property
|
||||
def controller(self) -> TransportSink | None:
|
||||
@@ -776,6 +782,20 @@ class Host(utils.EventEmitter):
|
||||
) -> hci.HCI_Command_Complete_Event[_RP]:
|
||||
response = await self._send_command(command, response_timeout)
|
||||
|
||||
# For unknown HCI commands, some controllers return Command Status instead of
|
||||
# Command Complete.
|
||||
if (
|
||||
isinstance(response, hci.HCI_Command_Status_Event)
|
||||
and response.status == hci.HCI_ErrorCode.UNKNOWN_HCI_COMMAND_ERROR
|
||||
):
|
||||
return hci.HCI_Command_Complete_Event(
|
||||
num_hci_command_packets=response.num_hci_command_packets,
|
||||
command_opcode=command.op_code,
|
||||
return_parameters=hci.HCI_StatusReturnParameters(
|
||||
status=hci.HCI_ErrorCode(response.status)
|
||||
), # type: ignore
|
||||
)
|
||||
|
||||
# Check that the response is of the expected type
|
||||
assert isinstance(response, hci.HCI_Command_Complete_Event)
|
||||
|
||||
@@ -789,19 +809,25 @@ class Host(utils.EventEmitter):
|
||||
) -> hci.HCI_ErrorCode:
|
||||
response = await self._send_command(command, response_timeout)
|
||||
|
||||
# Check that the response is of the expected type
|
||||
assert isinstance(response, hci.HCI_Command_Status_Event)
|
||||
# For unknown HCI commands, some controllers return Command Complete instead of
|
||||
# Command Status.
|
||||
if isinstance(response, hci.HCI_Command_Complete_Event):
|
||||
# Assume the first byte of the return parameters is the status
|
||||
if (
|
||||
status := hci.HCI_ErrorCode(response.parameters[3])
|
||||
) != hci.HCI_ErrorCode.UNKNOWN_HCI_COMMAND_ERROR:
|
||||
logger.warning(f'unexpected return paramerers status {status}')
|
||||
else:
|
||||
assert isinstance(response, hci.HCI_Command_Status_Event)
|
||||
status = hci.HCI_ErrorCode(response.status)
|
||||
|
||||
# Check the return parameters if required
|
||||
status = response.status
|
||||
# Check the status if required
|
||||
if check_status:
|
||||
if status != hci.HCI_CommandStatus.PENDING:
|
||||
logger.warning(
|
||||
f'{command.name} failed ' f'({hci.HCI_Constant.error_name(status)})'
|
||||
)
|
||||
logger.warning(f'{command.name} failed ' f'({status.name})')
|
||||
raise hci.HCI_Error(status)
|
||||
|
||||
return hci.HCI_ErrorCode(status)
|
||||
return status
|
||||
|
||||
@utils.deprecated("Use utils.AsyncRunner.spawn() instead.")
|
||||
def send_command_sync(self, command: hci.HCI_AsyncCommand) -> None:
|
||||
@@ -830,7 +856,9 @@ class Host(utils.EventEmitter):
|
||||
data=pdu,
|
||||
)
|
||||
logger.debug(
|
||||
'>>> ACL packet enqueue: (Handle=0x%04X) %s', connection_handle, pdu
|
||||
'>>> ACL packet enqueue: (handle=0x%04X) %s',
|
||||
connection_handle,
|
||||
pdu.hex(),
|
||||
)
|
||||
packet_queue.enqueue(acl_packet, connection_handle)
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import enum
|
||||
import logging
|
||||
from collections.abc import Awaitable, Callable
|
||||
from collections.abc import Awaitable, Callable, Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, ClassVar, TypeVar, cast
|
||||
|
||||
@@ -507,10 +507,15 @@ def smp_auth_req(bonding: bool, mitm: bool, sc: bool, keypress: bool, ct2: bool)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class AddressResolver:
|
||||
def __init__(self, resolving_keys):
|
||||
def __init__(self, resolving_keys: Sequence[tuple[bytes, Address]]) -> None:
|
||||
self.resolving_keys = resolving_keys
|
||||
|
||||
def resolve(self, address):
|
||||
def can_resolve_to(self, address: Address) -> bool:
|
||||
return any(
|
||||
resolved_address == address for _, resolved_address in self.resolving_keys
|
||||
)
|
||||
|
||||
def resolve(self, address: Address) -> Address | None:
|
||||
address_bytes = bytes(address)
|
||||
hash_part = address_bytes[0:3]
|
||||
prand = address_bytes[3:6]
|
||||
|
||||
@@ -25,7 +25,7 @@ import sys
|
||||
import websockets.asyncio.server
|
||||
|
||||
import bumble.logging
|
||||
from bumble import a2dp, avc, avdtp, avrcp, utils
|
||||
from bumble import a2dp, avc, avdtp, avrcp, sdp, utils
|
||||
from bumble.core import PhysicalTransport
|
||||
from bumble.device import Device
|
||||
from bumble.transport import open_transport
|
||||
@@ -34,7 +34,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def sdp_records():
|
||||
def sdp_records() -> dict[int, list[sdp.ServiceAttribute]]:
|
||||
a2dp_sink_service_record_handle = 0x00010001
|
||||
avrcp_controller_service_record_handle = 0x00010002
|
||||
avrcp_target_service_record_handle = 0x00010003
|
||||
@@ -43,17 +43,17 @@ def sdp_records():
|
||||
a2dp_sink_service_record_handle: a2dp.make_audio_sink_service_sdp_records(
|
||||
a2dp_sink_service_record_handle
|
||||
),
|
||||
avrcp_controller_service_record_handle: avrcp.make_controller_service_sdp_records(
|
||||
avrcp_controller_service_record_handle: avrcp.ControllerServiceSdpRecord(
|
||||
avrcp_controller_service_record_handle
|
||||
),
|
||||
avrcp_target_service_record_handle: avrcp.make_target_service_sdp_records(
|
||||
avrcp_controller_service_record_handle
|
||||
),
|
||||
).to_service_attributes(),
|
||||
avrcp_target_service_record_handle: avrcp.TargetServiceSdpRecord(
|
||||
avrcp_target_service_record_handle
|
||||
).to_service_attributes(),
|
||||
}
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def codec_capabilities():
|
||||
def codec_capabilities() -> avdtp.MediaCodecCapabilities:
|
||||
return avdtp.MediaCodecCapabilities(
|
||||
media_type=avdtp.AVDTP_AUDIO_MEDIA_TYPE,
|
||||
media_codec_type=a2dp.A2DP_SBC_CODEC_TYPE,
|
||||
@@ -81,20 +81,22 @@ def codec_capabilities():
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def on_avdtp_connection(server):
|
||||
def on_avdtp_connection(server: avdtp.Protocol) -> None:
|
||||
# Add a sink endpoint to the server
|
||||
sink = server.add_sink(codec_capabilities())
|
||||
sink.on('rtp_packet', on_rtp_packet)
|
||||
sink.on(sink.EVENT_RTP_PACKET, on_rtp_packet)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def on_rtp_packet(packet):
|
||||
def on_rtp_packet(packet: avdtp.MediaPacket) -> None:
|
||||
print(f'RTP: {packet}')
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def on_avrcp_start(avrcp_protocol: avrcp.Protocol, websocket_server: WebSocketServer):
|
||||
async def get_supported_events():
|
||||
def on_avrcp_start(
|
||||
avrcp_protocol: avrcp.Protocol, websocket_server: WebSocketServer
|
||||
) -> None:
|
||||
async def get_supported_events() -> None:
|
||||
events = await avrcp_protocol.get_supported_events()
|
||||
print("SUPPORTED EVENTS:", events)
|
||||
websocket_server.send_message(
|
||||
@@ -130,14 +132,14 @@ def on_avrcp_start(avrcp_protocol: avrcp.Protocol, websocket_server: WebSocketSe
|
||||
|
||||
utils.AsyncRunner.spawn(get_supported_events())
|
||||
|
||||
async def monitor_track_changed():
|
||||
async def monitor_track_changed() -> None:
|
||||
async for identifier in avrcp_protocol.monitor_track_changed():
|
||||
print("TRACK CHANGED:", identifier.hex())
|
||||
websocket_server.send_message(
|
||||
{"type": "track-changed", "params": {"identifier": identifier.hex()}}
|
||||
)
|
||||
|
||||
async def monitor_playback_status():
|
||||
async def monitor_playback_status() -> None:
|
||||
async for playback_status in avrcp_protocol.monitor_playback_status():
|
||||
print("PLAYBACK STATUS CHANGED:", playback_status.name)
|
||||
websocket_server.send_message(
|
||||
@@ -147,7 +149,7 @@ def on_avrcp_start(avrcp_protocol: avrcp.Protocol, websocket_server: WebSocketSe
|
||||
}
|
||||
)
|
||||
|
||||
async def monitor_playback_position():
|
||||
async def monitor_playback_position() -> None:
|
||||
async for playback_position in avrcp_protocol.monitor_playback_position(
|
||||
playback_interval=1
|
||||
):
|
||||
@@ -159,7 +161,7 @@ def on_avrcp_start(avrcp_protocol: avrcp.Protocol, websocket_server: WebSocketSe
|
||||
}
|
||||
)
|
||||
|
||||
async def monitor_player_application_settings():
|
||||
async def monitor_player_application_settings() -> None:
|
||||
async for settings in avrcp_protocol.monitor_player_application_settings():
|
||||
print("PLAYER APPLICATION SETTINGS:", settings)
|
||||
settings_as_dict = [
|
||||
@@ -173,14 +175,14 @@ def on_avrcp_start(avrcp_protocol: avrcp.Protocol, websocket_server: WebSocketSe
|
||||
}
|
||||
)
|
||||
|
||||
async def monitor_available_players():
|
||||
async def monitor_available_players() -> None:
|
||||
async for _ in avrcp_protocol.monitor_available_players():
|
||||
print("AVAILABLE PLAYERS CHANGED")
|
||||
websocket_server.send_message(
|
||||
{"type": "available-players-changed", "params": {}}
|
||||
)
|
||||
|
||||
async def monitor_addressed_player():
|
||||
async def monitor_addressed_player() -> None:
|
||||
async for player in avrcp_protocol.monitor_addressed_player():
|
||||
print("ADDRESSED PLAYER CHANGED")
|
||||
websocket_server.send_message(
|
||||
@@ -195,7 +197,7 @@ def on_avrcp_start(avrcp_protocol: avrcp.Protocol, websocket_server: WebSocketSe
|
||||
}
|
||||
)
|
||||
|
||||
async def monitor_uids():
|
||||
async def monitor_uids() -> None:
|
||||
async for uid_counter in avrcp_protocol.monitor_uids():
|
||||
print("UIDS CHANGED")
|
||||
websocket_server.send_message(
|
||||
@@ -207,7 +209,7 @@ def on_avrcp_start(avrcp_protocol: avrcp.Protocol, websocket_server: WebSocketSe
|
||||
}
|
||||
)
|
||||
|
||||
async def monitor_volume():
|
||||
async def monitor_volume() -> None:
|
||||
async for volume in avrcp_protocol.monitor_volume():
|
||||
print("VOLUME CHANGED:", volume)
|
||||
websocket_server.send_message(
|
||||
@@ -360,7 +362,7 @@ async def main() -> None:
|
||||
|
||||
# Create a listener to wait for AVDTP connections
|
||||
listener = avdtp.Listener(avdtp.Listener.create_registrar(device))
|
||||
listener.on('connection', on_avdtp_connection)
|
||||
listener.on(listener.EVENT_CONNECTION, on_avdtp_connection)
|
||||
|
||||
avrcp_delegate = Delegate()
|
||||
avrcp_protocol = avrcp.Protocol(avrcp_delegate)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import struct
|
||||
from collections.abc import Sequence
|
||||
|
||||
@@ -422,6 +423,47 @@ def test_passthrough_commands():
|
||||
assert bytes(parsed) == play_pressed_bytes
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_sdp_records():
|
||||
two_devices = await TwoDevices.create_with_avdtp()
|
||||
|
||||
# Add SDP records to device 1
|
||||
controller_record = avrcp.ControllerServiceSdpRecord(
|
||||
service_record_handle=0x10001,
|
||||
avctp_version=(1, 4),
|
||||
avrcp_version=(1, 6),
|
||||
supported_features=(
|
||||
avrcp.ControllerFeatures.CATEGORY_1
|
||||
| avrcp.ControllerFeatures.SUPPORTS_BROWSING
|
||||
),
|
||||
)
|
||||
target_record = avrcp.TargetServiceSdpRecord(
|
||||
service_record_handle=0x10002,
|
||||
avctp_version=(1, 4),
|
||||
avrcp_version=(1, 6),
|
||||
supported_features=(
|
||||
avrcp.TargetFeatures.CATEGORY_1 | avrcp.TargetFeatures.SUPPORTS_BROWSING
|
||||
),
|
||||
)
|
||||
|
||||
two_devices.devices[1].sdp_service_records = {
|
||||
0x10001: controller_record.to_service_attributes(),
|
||||
0x10002: target_record.to_service_attributes(),
|
||||
}
|
||||
|
||||
# Find records from device 0
|
||||
controller_records = await avrcp.ControllerServiceSdpRecord.find(
|
||||
two_devices.connections[0]
|
||||
)
|
||||
assert len(controller_records) == 1
|
||||
assert controller_records[0] == controller_record
|
||||
|
||||
target_records = await avrcp.TargetServiceSdpRecord.find(two_devices.connections[0])
|
||||
assert len(target_records) == 1
|
||||
assert target_records[0] == target_record
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_supported_events():
|
||||
@@ -436,6 +478,163 @@ async def test_get_supported_events():
|
||||
assert supported_events == [avrcp.EventId.VOLUME_CHANGED]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_passthrough_key_event():
|
||||
two_devices = await TwoDevices.create_with_avdtp()
|
||||
|
||||
q = asyncio.Queue[tuple[avc.PassThroughFrame.OperationId, bool, bytes]]()
|
||||
|
||||
class Delegate(avrcp.Delegate):
|
||||
async def on_key_event(
|
||||
self, key: avc.PassThroughFrame.OperationId, pressed: bool, data: bytes
|
||||
) -> None:
|
||||
q.put_nowait((key, pressed, data))
|
||||
|
||||
two_devices.protocols[1].delegate = Delegate()
|
||||
|
||||
for key, pressed in [
|
||||
(avc.PassThroughFrame.OperationId.PLAY, True),
|
||||
(avc.PassThroughFrame.OperationId.PLAY, False),
|
||||
(avc.PassThroughFrame.OperationId.PAUSE, True),
|
||||
(avc.PassThroughFrame.OperationId.PAUSE, False),
|
||||
]:
|
||||
await two_devices.protocols[0].send_key_event(key, pressed)
|
||||
assert (await q.get()) == (key, pressed, b'')
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_passthrough_key_event_rejected():
|
||||
two_devices = await TwoDevices.create_with_avdtp()
|
||||
|
||||
class Delegate(avrcp.Delegate):
|
||||
async def on_key_event(
|
||||
self, key: avc.PassThroughFrame.OperationId, pressed: bool, data: bytes
|
||||
) -> None:
|
||||
raise avrcp.Delegate.AvcError(avc.ResponseFrame.ResponseCode.REJECTED)
|
||||
|
||||
two_devices.protocols[1].delegate = Delegate()
|
||||
|
||||
response = await two_devices.protocols[0].send_key_event(
|
||||
avc.PassThroughFrame.OperationId.PLAY, True
|
||||
)
|
||||
assert response.response == avc.ResponseFrame.ResponseCode.REJECTED
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_passthrough_key_event_exception():
|
||||
two_devices = await TwoDevices.create_with_avdtp()
|
||||
|
||||
class Delegate(avrcp.Delegate):
|
||||
async def on_key_event(
|
||||
self, key: avc.PassThroughFrame.OperationId, pressed: bool, data: bytes
|
||||
) -> None:
|
||||
raise Exception()
|
||||
|
||||
two_devices.protocols[1].delegate = Delegate()
|
||||
|
||||
response = await two_devices.protocols[0].send_key_event(
|
||||
avc.PassThroughFrame.OperationId.PLAY, True
|
||||
)
|
||||
assert response.response == avc.ResponseFrame.ResponseCode.REJECTED
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_volume():
|
||||
two_devices = await TwoDevices.create_with_avdtp()
|
||||
|
||||
for volume in range(avrcp.SetAbsoluteVolumeCommand.MAXIMUM_VOLUME + 1):
|
||||
response = await two_devices.protocols[1].send_avrcp_command(
|
||||
avc.CommandFrame.CommandType.CONTROL, avrcp.SetAbsoluteVolumeCommand(volume)
|
||||
)
|
||||
assert isinstance(response.response, avrcp.SetAbsoluteVolumeResponse)
|
||||
assert response.response.volume == volume
|
||||
assert two_devices.protocols[0].delegate.volume == volume
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_playback_status():
|
||||
two_devices = await TwoDevices.create_with_avdtp()
|
||||
|
||||
for status in avrcp.PlayStatus:
|
||||
two_devices.protocols[0].delegate.playback_status = status
|
||||
response = await two_devices.protocols[1].get_play_status()
|
||||
assert response.play_status == status
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_supported_company_ids():
|
||||
two_devices = await TwoDevices.create_with_avdtp()
|
||||
|
||||
for status in avrcp.PlayStatus:
|
||||
two_devices.protocols[0].delegate = avrcp.Delegate(
|
||||
supported_company_ids=[avrcp.AVRCP_BLUETOOTH_SIG_COMPANY_ID]
|
||||
)
|
||||
supported_company_ids = await two_devices.protocols[
|
||||
1
|
||||
].get_supported_company_ids()
|
||||
assert supported_company_ids == [avrcp.AVRCP_BLUETOOTH_SIG_COMPANY_ID]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_monitor_volume():
|
||||
two_devices = await TwoDevices.create_with_avdtp()
|
||||
|
||||
two_devices.protocols[1].delegate = avrcp.Delegate([avrcp.EventId.VOLUME_CHANGED])
|
||||
volume_iter = two_devices.protocols[0].monitor_volume()
|
||||
|
||||
for volume in range(avrcp.SetAbsoluteVolumeCommand.MAXIMUM_VOLUME + 1):
|
||||
# Interim
|
||||
two_devices.protocols[1].delegate.volume = 0
|
||||
assert (await anext(volume_iter)) == 0
|
||||
# Changed
|
||||
two_devices.protocols[1].notify_volume_changed(volume)
|
||||
assert (await anext(volume_iter)) == volume
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_monitor_playback_status():
|
||||
two_devices = await TwoDevices.create_with_avdtp()
|
||||
|
||||
two_devices.protocols[1].delegate = avrcp.Delegate(
|
||||
[avrcp.EventId.PLAYBACK_STATUS_CHANGED]
|
||||
)
|
||||
playback_status_iter = two_devices.protocols[0].monitor_playback_status()
|
||||
|
||||
for playback_status in avrcp.PlayStatus:
|
||||
# Interim
|
||||
two_devices.protocols[1].delegate.playback_status = avrcp.PlayStatus.STOPPED
|
||||
assert (await anext(playback_status_iter)) == avrcp.PlayStatus.STOPPED
|
||||
# Changed
|
||||
two_devices.protocols[1].notify_playback_status_changed(playback_status)
|
||||
assert (await anext(playback_status_iter)) == playback_status
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_monitor_now_playing_content():
|
||||
two_devices = await TwoDevices.create_with_avdtp()
|
||||
|
||||
two_devices.protocols[1].delegate = avrcp.Delegate(
|
||||
[avrcp.EventId.NOW_PLAYING_CONTENT_CHANGED]
|
||||
)
|
||||
now_playing_iter = two_devices.protocols[0].monitor_now_playing_content()
|
||||
|
||||
for _ in range(2):
|
||||
# Interim
|
||||
await anext(now_playing_iter)
|
||||
# Changed
|
||||
two_devices.protocols[1].notify_now_playing_content_changed()
|
||||
await anext(now_playing_iter)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
test_frame_parser()
|
||||
|
||||
@@ -232,6 +232,14 @@ def test_return_parameters() -> None:
|
||||
assert len(params.local_name) == 248
|
||||
assert hci.map_null_terminated_utf8_string(params.local_name) == 'hello'
|
||||
|
||||
# Some return parameters may be shorter than the full length
|
||||
# (for Command Complete events with errors)
|
||||
params = hci.HCI_Read_BD_ADDR_Command.parse_return_parameters(
|
||||
bytes.fromhex('010011223344')
|
||||
)
|
||||
assert isinstance(params, hci.HCI_StatusReturnParameters)
|
||||
assert params.status == hci.HCI_ErrorCode.UNKNOWN_HCI_COMMAND_ERROR
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Command():
|
||||
|
||||
@@ -26,11 +26,14 @@ from bumble.controller import Controller
|
||||
from bumble.hci import (
|
||||
HCI_AclDataPacket,
|
||||
HCI_Command_Complete_Event,
|
||||
HCI_Command_Status_Event,
|
||||
HCI_CommandStatus,
|
||||
HCI_Disconnect_Command,
|
||||
HCI_Error,
|
||||
HCI_ErrorCode,
|
||||
HCI_Event,
|
||||
HCI_GenericReturnParameters,
|
||||
HCI_LE_Terminate_BIG_Command,
|
||||
HCI_Reset_Command,
|
||||
HCI_StatusReturnParameters,
|
||||
)
|
||||
@@ -229,3 +232,47 @@ async def test_send_sync_command() -> None:
|
||||
)
|
||||
response3 = await host.send_sync_command_raw(command) # type: ignore
|
||||
assert isinstance(response3.return_parameters, HCI_GenericReturnParameters)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_async_command() -> None:
|
||||
source = Source()
|
||||
sink = Sink(
|
||||
source,
|
||||
HCI_Command_Status_Event(
|
||||
HCI_CommandStatus.PENDING,
|
||||
1,
|
||||
HCI_Reset_Command.op_code,
|
||||
),
|
||||
)
|
||||
|
||||
host = Host(source, sink)
|
||||
host.ready = True
|
||||
|
||||
# Normal pending status
|
||||
response = await host.send_async_command(
|
||||
HCI_LE_Terminate_BIG_Command(big_handle=0, reason=0)
|
||||
)
|
||||
assert response == HCI_CommandStatus.PENDING
|
||||
|
||||
# Unknown HCI command result returned as a Command Status
|
||||
sink.response = HCI_Command_Status_Event(
|
||||
HCI_ErrorCode.UNKNOWN_HCI_COMMAND_ERROR,
|
||||
1,
|
||||
HCI_LE_Terminate_BIG_Command.op_code,
|
||||
)
|
||||
response = await host.send_async_command(
|
||||
HCI_LE_Terminate_BIG_Command(big_handle=0, reason=0), check_status=False
|
||||
)
|
||||
assert response == HCI_ErrorCode.UNKNOWN_HCI_COMMAND_ERROR
|
||||
|
||||
# Unknown HCI command result returned as a Command Complete
|
||||
sink.response = HCI_Command_Complete_Event(
|
||||
1,
|
||||
HCI_LE_Terminate_BIG_Command.op_code,
|
||||
HCI_StatusReturnParameters(HCI_ErrorCode.UNKNOWN_HCI_COMMAND_ERROR),
|
||||
)
|
||||
response = await host.send_async_command(
|
||||
HCI_LE_Terminate_BIG_Command(big_handle=0, reason=0), check_status=False
|
||||
)
|
||||
assert response == HCI_ErrorCode.UNKNOWN_HCI_COMMAND_ERROR
|
||||
|
||||
Reference in New Issue
Block a user