forked from auracaster/bumble_mirror
Compare commits
34 Commits
gbg/data-t
...
gbg/nrf-ua
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a3eaa457f | ||
|
|
aa1d7933da | ||
|
|
34e0f293c2 | ||
|
|
85215df2c3 | ||
|
|
f8223ca81f | ||
|
|
2b0b1ad726 | ||
|
|
58debcd8bb | ||
|
|
6eba81e3dd | ||
|
|
768bbd95cc | ||
|
|
502b80af0d | ||
|
|
a25427305c | ||
|
|
3c47739029 | ||
|
|
8fc1330948 | ||
|
|
83c5061700 | ||
|
|
b80b790dc1 | ||
|
|
21bf69592c | ||
|
|
7d8addb849 | ||
|
|
d86d69d816 | ||
|
|
bb08a1c70b | ||
|
|
dc93f32a9a | ||
|
|
9838908a26 | ||
|
|
613519f0b3 | ||
|
|
a943ea57ef | ||
|
|
14401910bb | ||
|
|
5d35ed471c | ||
|
|
c720ad5fdc | ||
|
|
f02183f95d | ||
|
|
d903937a51 | ||
|
|
6381ee0ab1 | ||
|
|
59d99780e1 | ||
|
|
9f3d8c9b49 | ||
|
|
31961febe5 | ||
|
|
dab0993cba | ||
|
|
3333ba472b |
4
.github/workflows/python-build-test.yml
vendored
4
.github/workflows/python-build-test.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
rust-version: [ "1.76.0", "stable" ]
|
||||
rust-version: [ "1.80.0", "stable" ]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- name: Check out from Git
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
- name: Check License Headers
|
||||
run: cd rust && cargo run --features dev-tools --bin file-header check-all
|
||||
- name: Rust Build
|
||||
run: cd rust && cargo build --all-targets && cargo build-all-features --all-targets
|
||||
run: cd rust && cargo build --all-targets && cargo build-all-features
|
||||
# Lints after build so what clippy needs is already built
|
||||
- name: Rust Lints
|
||||
run: cd rust && cargo fmt --check && cargo clippy --all-targets -- --deny warnings && cargo clippy --all-features --all-targets -- --deny warnings
|
||||
|
||||
1735
bumble/avrcp.py
1735
bumble/avrcp.py
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,6 @@ from bumble.hci import (
|
||||
HCI_COMMAND_DISALLOWED_ERROR,
|
||||
HCI_COMMAND_PACKET,
|
||||
HCI_COMMAND_STATUS_PENDING,
|
||||
HCI_CONNECTION_TIMEOUT_ERROR,
|
||||
HCI_CONTROLLER_BUSY_ERROR,
|
||||
HCI_EVENT_PACKET,
|
||||
HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR,
|
||||
@@ -88,6 +87,7 @@ class CisLink:
|
||||
cis_id: int
|
||||
cig_id: int
|
||||
acl_connection: Optional[Connection] = None
|
||||
data_paths: set[int] = dataclasses.field(default_factory=set)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -381,6 +381,11 @@ class Controller:
|
||||
return connection
|
||||
return None
|
||||
|
||||
def find_iso_link_by_handle(self, handle: int) -> Optional[CisLink]:
|
||||
return self.central_cis_links.get(handle) or self.peripheral_cis_links.get(
|
||||
handle
|
||||
)
|
||||
|
||||
def on_link_central_connected(self, central_address):
|
||||
'''
|
||||
Called when an incoming connection occurs from a central on the link
|
||||
@@ -1853,16 +1858,51 @@ class Controller:
|
||||
)
|
||||
)
|
||||
|
||||
def on_hci_le_setup_iso_data_path_command(self, command):
|
||||
def on_hci_le_setup_iso_data_path_command(
|
||||
self, command: hci.HCI_LE_Setup_ISO_Data_Path_Command
|
||||
) -> bytes:
|
||||
'''
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.109 LE Setup ISO Data Path Command
|
||||
'''
|
||||
if not (iso_link := self.find_iso_link_by_handle(command.connection_handle)):
|
||||
return struct.pack(
|
||||
'<BH',
|
||||
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
||||
command.connection_handle,
|
||||
)
|
||||
if command.data_path_direction in iso_link.data_paths:
|
||||
return struct.pack(
|
||||
'<BH',
|
||||
HCI_COMMAND_DISALLOWED_ERROR,
|
||||
command.connection_handle,
|
||||
)
|
||||
iso_link.data_paths.add(command.data_path_direction)
|
||||
return struct.pack('<BH', HCI_SUCCESS, command.connection_handle)
|
||||
|
||||
def on_hci_le_remove_iso_data_path_command(self, command):
|
||||
def on_hci_le_remove_iso_data_path_command(
|
||||
self, command: hci.HCI_LE_Remove_ISO_Data_Path_Command
|
||||
) -> bytes:
|
||||
'''
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.110 LE Remove ISO Data Path Command
|
||||
'''
|
||||
if not (iso_link := self.find_iso_link_by_handle(command.connection_handle)):
|
||||
return struct.pack(
|
||||
'<BH',
|
||||
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
||||
command.connection_handle,
|
||||
)
|
||||
data_paths: set[int] = set(
|
||||
direction
|
||||
for direction in hci.HCI_LE_Setup_ISO_Data_Path_Command.Direction
|
||||
if (1 << direction) & command.data_path_direction
|
||||
)
|
||||
if not data_paths.issubset(iso_link.data_paths):
|
||||
return struct.pack(
|
||||
'<BH',
|
||||
HCI_COMMAND_DISALLOWED_ERROR,
|
||||
command.connection_handle,
|
||||
)
|
||||
iso_link.data_paths.difference_update(data_paths)
|
||||
return struct.pack('<BH', HCI_SUCCESS, command.connection_handle)
|
||||
|
||||
def on_hci_le_set_host_feature_command(
|
||||
|
||||
@@ -2110,23 +2110,6 @@ class AdvertisingData:
|
||||
return self.to_string()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Connection Parameters
|
||||
# -----------------------------------------------------------------------------
|
||||
class ConnectionParameters:
|
||||
def __init__(self, connection_interval, peripheral_latency, supervision_timeout):
|
||||
self.connection_interval = connection_interval
|
||||
self.peripheral_latency = peripheral_latency
|
||||
self.supervision_timeout = supervision_timeout
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f'ConnectionParameters(connection_interval={self.connection_interval}, '
|
||||
f'peripheral_latency={self.peripheral_latency}, '
|
||||
f'supervision_timeout={self.supervision_timeout}'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Connection PHY
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
444
bumble/device.py
444
bumble/device.py
@@ -48,6 +48,7 @@ from typing_extensions import Self
|
||||
from bumble import (
|
||||
core,
|
||||
data_types,
|
||||
gatt,
|
||||
gatt_client,
|
||||
gatt_server,
|
||||
hci,
|
||||
@@ -264,7 +265,7 @@ class ExtendedAdvertisement(Advertisement):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class AdvertisementDataAccumulator:
|
||||
def __init__(self, passive=False):
|
||||
def __init__(self, passive: bool = False):
|
||||
self.passive = passive
|
||||
self.last_advertisement = None
|
||||
self.last_data = b''
|
||||
@@ -1243,7 +1244,7 @@ class LePhyOptions:
|
||||
PREFER_S_2_CODED_PHY = 1
|
||||
PREFER_S_8_CODED_PHY = 2
|
||||
|
||||
def __init__(self, coded_phy_preference=0):
|
||||
def __init__(self, coded_phy_preference: int = 0):
|
||||
self.coded_phy_preference = coded_phy_preference
|
||||
|
||||
def __int__(self):
|
||||
@@ -1452,6 +1453,8 @@ class _IsoLink:
|
||||
handle: int
|
||||
device: Device
|
||||
sink: Callable[[hci.HCI_IsoDataPacket], Any] | None = None
|
||||
data_paths: set[_IsoLink.Direction]
|
||||
_data_path_lock: asyncio.Lock
|
||||
|
||||
class Direction(IntEnum):
|
||||
HOST_TO_CONTROLLER = (
|
||||
@@ -1461,6 +1464,10 @@ class _IsoLink:
|
||||
hci.HCI_LE_Setup_ISO_Data_Path_Command.Direction.CONTROLLER_TO_HOST
|
||||
)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._data_path_lock = asyncio.Lock()
|
||||
self.data_paths = set()
|
||||
|
||||
async def setup_data_path(
|
||||
self,
|
||||
direction: _IsoLink.Direction,
|
||||
@@ -1481,37 +1488,45 @@ class _IsoLink:
|
||||
Raises:
|
||||
HCI_Error: When command complete status is not HCI_SUCCESS.
|
||||
"""
|
||||
await self.device.send_command(
|
||||
hci.HCI_LE_Setup_ISO_Data_Path_Command(
|
||||
connection_handle=self.handle,
|
||||
data_path_direction=direction,
|
||||
data_path_id=data_path_id,
|
||||
codec_id=codec_id or hci.CodingFormat(hci.CodecID.TRANSPARENT),
|
||||
controller_delay=controller_delay,
|
||||
codec_configuration=codec_configuration,
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
async with self._data_path_lock:
|
||||
if direction in self.data_paths:
|
||||
return
|
||||
await self.device.send_command(
|
||||
hci.HCI_LE_Setup_ISO_Data_Path_Command(
|
||||
connection_handle=self.handle,
|
||||
data_path_direction=direction,
|
||||
data_path_id=data_path_id,
|
||||
codec_id=codec_id or hci.CodingFormat(hci.CodecID.TRANSPARENT),
|
||||
controller_delay=controller_delay,
|
||||
codec_configuration=codec_configuration,
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
self.data_paths.add(direction)
|
||||
|
||||
async def remove_data_path(self, directions: Iterable[_IsoLink.Direction]) -> int:
|
||||
async def remove_data_path(self, directions: Iterable[_IsoLink.Direction]) -> None:
|
||||
"""Remove a data path with controller on given direction.
|
||||
|
||||
Args:
|
||||
direction: Direction of data path.
|
||||
|
||||
Returns:
|
||||
Command status.
|
||||
Raises:
|
||||
HCI_Error: When command complete status is not HCI_SUCCESS.
|
||||
"""
|
||||
response = await self.device.send_command(
|
||||
hci.HCI_LE_Remove_ISO_Data_Path_Command(
|
||||
connection_handle=self.handle,
|
||||
data_path_direction=sum(
|
||||
1 << direction for direction in set(directions)
|
||||
async with self._data_path_lock:
|
||||
directions_to_remove = set(directions).intersection(self.data_paths)
|
||||
if not directions_to_remove:
|
||||
return
|
||||
await self.device.send_command(
|
||||
hci.HCI_LE_Remove_ISO_Data_Path_Command(
|
||||
connection_handle=self.handle,
|
||||
data_path_direction=sum(
|
||||
1 << direction for direction in directions_to_remove
|
||||
),
|
||||
),
|
||||
),
|
||||
check_result=False,
|
||||
)
|
||||
return response.return_parameters.status
|
||||
check_result=True,
|
||||
)
|
||||
self.data_paths.difference_update(directions_to_remove)
|
||||
|
||||
def write(self, sdu: bytes) -> None:
|
||||
"""Write an ISO SDU."""
|
||||
@@ -1621,7 +1636,8 @@ class CisLink(utils.EventEmitter, _IsoLink):
|
||||
EVENT_ESTABLISHMENT_FAILURE: ClassVar[str] = "establishment_failure"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
super().__init__()
|
||||
utils.EventEmitter.__init__(self)
|
||||
_IsoLink.__init__(self)
|
||||
|
||||
async def disconnect(
|
||||
self, reason: int = hci.HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR
|
||||
@@ -1637,6 +1653,7 @@ class BisLink(_IsoLink):
|
||||
sink: Callable[[hci.HCI_IsoDataPacket], Any] | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
super().__init__()
|
||||
self.device = self.big.device
|
||||
|
||||
|
||||
@@ -1691,6 +1708,7 @@ class Connection(utils.CompositeEventEmitter):
|
||||
self_address: hci.Address
|
||||
self_resolvable_address: Optional[hci.Address]
|
||||
peer_address: hci.Address
|
||||
peer_name: Optional[str]
|
||||
peer_resolvable_address: Optional[hci.Address]
|
||||
peer_le_features: Optional[hci.LeFeatureMask]
|
||||
role: hci.Role
|
||||
@@ -1780,15 +1798,22 @@ class Connection(utils.CompositeEventEmitter):
|
||||
|
||||
@dataclass
|
||||
class Parameters:
|
||||
connection_interval: float # Connection interval, in milliseconds. [LE only]
|
||||
peripheral_latency: int # Peripheral latency, in number of intervals. [LE only]
|
||||
supervision_timeout: float # Supervision timeout, in milliseconds.
|
||||
subrate_factor: int = (
|
||||
1 # See Bluetooth spec Vol 6, Part B - 4.5.1 Connection events
|
||||
)
|
||||
continuation_number: int = (
|
||||
0 # See Bluetooth spec Vol 6, Part B - 4.5.1 Connection events
|
||||
)
|
||||
"""
|
||||
LE connection parameters.
|
||||
|
||||
Attributes:
|
||||
connection_interval: Connection interval, in milliseconds.
|
||||
peripheral_latency: Peripheral latency, in number of intervals.
|
||||
supervision_timeout: Supervision timeout, in milliseconds.
|
||||
subrate_factor: See Bluetooth spec Vol 6, Part B - 4.5.1 Connection events
|
||||
continuation_number: See Bluetooth spec Vol 6, Part B - 4.5.1 Connection events
|
||||
"""
|
||||
|
||||
connection_interval: float
|
||||
peripheral_latency: int
|
||||
supervision_timeout: float
|
||||
subrate_factor: int = 1
|
||||
continuation_number: int = 0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -1829,36 +1854,6 @@ class Connection(utils.CompositeEventEmitter):
|
||||
self.cs_configs = {}
|
||||
self.cs_procedures = {}
|
||||
|
||||
# [Classic only]
|
||||
@classmethod
|
||||
def incomplete(cls, device, peer_address, role):
|
||||
"""
|
||||
Instantiate an incomplete connection (ie. one waiting for a HCI Connection
|
||||
Complete event).
|
||||
Once received it shall be completed using the `.complete` method.
|
||||
"""
|
||||
return cls(
|
||||
device,
|
||||
None,
|
||||
PhysicalTransport.BR_EDR,
|
||||
device.public_address,
|
||||
None,
|
||||
peer_address,
|
||||
None,
|
||||
role,
|
||||
None,
|
||||
)
|
||||
|
||||
# [Classic only]
|
||||
def complete(self, handle, parameters):
|
||||
"""
|
||||
Finish an incomplete connection upon completion.
|
||||
"""
|
||||
assert self.handle is None
|
||||
assert self.transport == PhysicalTransport.BR_EDR
|
||||
self.handle = handle
|
||||
self.parameters = parameters
|
||||
|
||||
@property
|
||||
def role_name(self):
|
||||
if self.role is None:
|
||||
@@ -1870,7 +1865,7 @@ class Connection(utils.CompositeEventEmitter):
|
||||
return f'UNKNOWN[{self.role}]'
|
||||
|
||||
@property
|
||||
def is_encrypted(self):
|
||||
def is_encrypted(self) -> bool:
|
||||
return self.encryption != 0
|
||||
|
||||
@property
|
||||
@@ -1931,7 +1926,7 @@ class Connection(utils.CompositeEventEmitter):
|
||||
self.remove_listener(self.EVENT_DISCONNECTION, abort.set_result)
|
||||
self.remove_listener(self.EVENT_DISCONNECTION_FAILURE, abort.set_exception)
|
||||
|
||||
async def set_data_length(self, tx_octets, tx_time) -> None:
|
||||
async def set_data_length(self, tx_octets: int, tx_time: int) -> None:
|
||||
return await self.device.set_data_length(self, tx_octets, tx_time)
|
||||
|
||||
async def update_parameters(
|
||||
@@ -1961,7 +1956,12 @@ class Connection(utils.CompositeEventEmitter):
|
||||
use_l2cap=use_l2cap,
|
||||
)
|
||||
|
||||
async def set_phy(self, tx_phys=None, rx_phys=None, phy_options=None):
|
||||
async def set_phy(
|
||||
self,
|
||||
tx_phys: Optional[Iterable[hci.Phy]] = None,
|
||||
rx_phys: Optional[Iterable[hci.Phy]] = None,
|
||||
phy_options: int = 0,
|
||||
):
|
||||
return await self.device.set_connection_phy(self, tx_phys, rx_phys, phy_options)
|
||||
|
||||
async def get_phy(self) -> ConnectionPHY:
|
||||
@@ -2156,7 +2156,7 @@ class DeviceConfiguration:
|
||||
# Decorator that converts the first argument from a connection handle to a connection
|
||||
def with_connection_from_handle(function):
|
||||
@functools.wraps(function)
|
||||
def wrapper(self, connection_handle, *args, **kwargs):
|
||||
def wrapper(self, connection_handle: int, *args, **kwargs):
|
||||
if (connection := self.lookup_connection(connection_handle)) is None:
|
||||
raise ObjectLookupError(
|
||||
f'no connection for handle: 0x{connection_handle:04x}'
|
||||
@@ -2169,12 +2169,12 @@ def with_connection_from_handle(function):
|
||||
# Decorator that converts the first argument from a bluetooth address to a connection
|
||||
def with_connection_from_address(function):
|
||||
@functools.wraps(function)
|
||||
def wrapper(self, address, *args, **kwargs):
|
||||
if connection := self.pending_connections.get(address, False):
|
||||
return function(self, connection, *args, **kwargs)
|
||||
for connection in self.connections.values():
|
||||
def wrapper(device: Device, address: hci.Address, *args, **kwargs):
|
||||
if connection := device.pending_connections.get(address):
|
||||
return function(device, connection, *args, **kwargs)
|
||||
for connection in device.connections.values():
|
||||
if connection.peer_address == address:
|
||||
return function(self, connection, *args, **kwargs)
|
||||
return function(device, connection, *args, **kwargs)
|
||||
raise ObjectLookupError('no connection for address')
|
||||
|
||||
return wrapper
|
||||
@@ -2184,13 +2184,13 @@ def with_connection_from_address(function):
|
||||
# connection
|
||||
def try_with_connection_from_address(function):
|
||||
@functools.wraps(function)
|
||||
def wrapper(self, address, *args, **kwargs):
|
||||
if connection := self.pending_connections.get(address, False):
|
||||
return function(self, connection, address, *args, **kwargs)
|
||||
for connection in self.connections.values():
|
||||
def wrapper(device: Device, address: hci.Address, *args, **kwargs):
|
||||
if connection := device.pending_connections.get(address):
|
||||
return function(device, connection, address, *args, **kwargs)
|
||||
for connection in device.connections.values():
|
||||
if connection.peer_address == address:
|
||||
return function(self, connection, address, *args, **kwargs)
|
||||
return function(self, None, address, *args, **kwargs)
|
||||
return function(device, connection, address, *args, **kwargs)
|
||||
return function(device, None, address, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -2360,7 +2360,9 @@ class Device(utils.CompositeEventEmitter):
|
||||
self.le_connecting = False
|
||||
self.disconnecting = False
|
||||
self.connections = {} # Connections, by connection handle
|
||||
self.pending_connections = {} # Connections, by BD address (BR/EDR only)
|
||||
self.pending_connections = (
|
||||
{}
|
||||
) # Pending connections, by BD address (BR/EDR only)
|
||||
self.sco_links = {} # ScoLinks, by connection handle (BR/EDR only)
|
||||
self.cis_links = {} # CisLinks, by connection handle (LE only)
|
||||
self._pending_cis = {} # (CIS_ID, CIG_ID), by CIS_handle
|
||||
@@ -2655,7 +2657,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None:
|
||||
self.host.send_l2cap_pdu(connection_handle, cid, pdu)
|
||||
|
||||
async def send_command(self, command, check_result=False):
|
||||
async def send_command(self, command: hci.HCI_Command, check_result: bool = False):
|
||||
try:
|
||||
return await asyncio.wait_for(
|
||||
self.host.send_command(command, check_result), self.command_timeout
|
||||
@@ -2912,13 +2914,13 @@ class Device(utils.CompositeEventEmitter):
|
||||
def supports_le_features(self, feature: hci.LeFeatureMask) -> bool:
|
||||
return self.host.supports_le_features(feature)
|
||||
|
||||
def supports_le_phy(self, phy: int) -> bool:
|
||||
if phy == hci.HCI_LE_1M_PHY:
|
||||
def supports_le_phy(self, phy: hci.Phy) -> bool:
|
||||
if phy == hci.Phy.LE_1M:
|
||||
return True
|
||||
|
||||
feature_map: dict[int, hci.LeFeatureMask] = {
|
||||
hci.HCI_LE_2M_PHY: hci.LeFeatureMask.LE_2M_PHY,
|
||||
hci.HCI_LE_CODED_PHY: hci.LeFeatureMask.LE_CODED_PHY,
|
||||
feature_map: dict[hci.Phy, hci.LeFeatureMask] = {
|
||||
hci.Phy.LE_2M: hci.LeFeatureMask.LE_2M_PHY,
|
||||
hci.Phy.LE_CODED: hci.LeFeatureMask.LE_CODED_PHY,
|
||||
}
|
||||
if phy not in feature_map:
|
||||
raise InvalidArgumentError('invalid PHY')
|
||||
@@ -3522,7 +3524,9 @@ class Device(utils.CompositeEventEmitter):
|
||||
self.discovering = False
|
||||
|
||||
@host_event_handler
|
||||
def on_inquiry_result(self, address, class_of_device, data, rssi):
|
||||
def on_inquiry_result(
|
||||
self, address: hci.Address, class_of_device: int, data: bytes, rssi: int
|
||||
):
|
||||
self.emit(
|
||||
self.EVENT_INQUIRY_RESULT,
|
||||
address,
|
||||
@@ -3531,7 +3535,9 @@ class Device(utils.CompositeEventEmitter):
|
||||
rssi,
|
||||
)
|
||||
|
||||
async def set_scan_enable(self, inquiry_scan_enabled, page_scan_enabled):
|
||||
async def set_scan_enable(
|
||||
self, inquiry_scan_enabled: bool, page_scan_enabled: bool
|
||||
):
|
||||
if inquiry_scan_enabled and page_scan_enabled:
|
||||
scan_enable = 0x03
|
||||
elif page_scan_enabled:
|
||||
@@ -3657,6 +3663,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
# 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
|
||||
@@ -3684,7 +3691,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
):
|
||||
pending_connection.set_result(connection)
|
||||
|
||||
def on_connection_failure(error):
|
||||
def on_connection_failure(error: core.ConnectionError):
|
||||
if transport == PhysicalTransport.LE or (
|
||||
# match BR/EDR connection failure event against peer address
|
||||
error.transport == transport
|
||||
@@ -3824,8 +3831,16 @@ class Device(utils.CompositeEventEmitter):
|
||||
)
|
||||
else:
|
||||
# Save pending connection
|
||||
self.pending_connections[peer_address] = Connection.incomplete(
|
||||
self, peer_address, hci.Role.CENTRAL
|
||||
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
|
||||
@@ -3904,6 +3919,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
except InvalidArgumentError:
|
||||
# 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
|
||||
@@ -3962,7 +3978,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
):
|
||||
pending_connection.set_result(connection)
|
||||
|
||||
def on_connection_failure(error):
|
||||
def on_connection_failure(error: core.ConnectionError):
|
||||
if (
|
||||
error.transport == PhysicalTransport.BR_EDR
|
||||
and error.peer_address == peer_address
|
||||
@@ -3972,12 +3988,20 @@ class Device(utils.CompositeEventEmitter):
|
||||
self.on(self.EVENT_CONNECTION, on_connection)
|
||||
self.on(self.EVENT_CONNECTION_FAILURE, on_connection_failure)
|
||||
|
||||
# Save pending connection, with the Peripheral hci.role.
|
||||
# Save Peripheral hci.role.
|
||||
# Even if we requested a role switch in the hci.HCI_Accept_Connection_Request
|
||||
# command, this connection is still considered Peripheral until an eventual
|
||||
# role change event.
|
||||
self.pending_connections[peer_address] = Connection.incomplete(
|
||||
self, peer_address, hci.Role.PERIPHERAL
|
||||
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.PERIPHERAL,
|
||||
parameters=Connection.Parameters(0, 0, 0),
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -3999,7 +4023,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
self.pending_connections.pop(peer_address, None)
|
||||
|
||||
@asynccontextmanager
|
||||
async def connect_as_gatt(self, peer_address):
|
||||
async def connect_as_gatt(self, peer_address: Union[hci.Address, str]):
|
||||
async with AsyncExitStack() as stack:
|
||||
connection = await stack.enter_async_context(
|
||||
await self.connect(peer_address)
|
||||
@@ -4035,6 +4059,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
except InvalidArgumentError:
|
||||
# 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
|
||||
@@ -4080,7 +4105,9 @@ class Device(utils.CompositeEventEmitter):
|
||||
)
|
||||
self.disconnecting = False
|
||||
|
||||
async def set_data_length(self, connection, tx_octets, tx_time) -> None:
|
||||
async def set_data_length(
|
||||
self, connection: Connection, tx_octets: int, tx_time: int
|
||||
) -> None:
|
||||
if tx_octets < 0x001B or tx_octets > 0x00FB:
|
||||
raise InvalidArgumentError('tx_octets must be between 0x001B and 0x00FB')
|
||||
|
||||
@@ -4183,7 +4210,11 @@ class Device(utils.CompositeEventEmitter):
|
||||
)
|
||||
|
||||
async def set_connection_phy(
|
||||
self, connection, tx_phys=None, rx_phys=None, phy_options=None
|
||||
self,
|
||||
connection: Connection,
|
||||
tx_phys: Optional[Iterable[hci.Phy]] = None,
|
||||
rx_phys: Optional[Iterable[hci.Phy]] = None,
|
||||
phy_options: int = 0,
|
||||
):
|
||||
if not self.host.supports_command(hci.HCI_LE_SET_PHY_COMMAND):
|
||||
logger.warning('ignoring request, command not supported')
|
||||
@@ -4199,7 +4230,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
all_phys=all_phys_bits,
|
||||
tx_phys=hci.phy_list_to_bits(tx_phys),
|
||||
rx_phys=hci.phy_list_to_bits(rx_phys),
|
||||
phy_options=0 if phy_options is None else int(phy_options),
|
||||
phy_options=phy_options,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -4210,7 +4241,11 @@ class Device(utils.CompositeEventEmitter):
|
||||
)
|
||||
raise hci.HCI_StatusError(result)
|
||||
|
||||
async def set_default_phy(self, tx_phys=None, rx_phys=None):
|
||||
async def set_default_phy(
|
||||
self,
|
||||
tx_phys: Optional[Iterable[hci.Phy]] = None,
|
||||
rx_phys: Optional[Iterable[hci.Phy]] = None,
|
||||
):
|
||||
all_phys_bits = (1 if tx_phys is None else 0) | (
|
||||
(1 if rx_phys is None else 0) << 1
|
||||
)
|
||||
@@ -4248,7 +4283,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
async def find_peer_by_name(self, name, transport=PhysicalTransport.LE):
|
||||
async def find_peer_by_name(self, name: str, transport=PhysicalTransport.LE):
|
||||
"""
|
||||
Scan for a peer with a given name and return its address.
|
||||
"""
|
||||
@@ -4263,7 +4298,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
if local_name == name:
|
||||
peer_address.set_result(address)
|
||||
|
||||
listener = None
|
||||
listener: Optional[Callable[..., None]] = None
|
||||
was_scanning = self.scanning
|
||||
was_discovering = self.discovering
|
||||
try:
|
||||
@@ -4369,10 +4404,10 @@ class Device(utils.CompositeEventEmitter):
|
||||
def smp_session_proxy(self, session_proxy: type[smp.Session]) -> None:
|
||||
self.smp_manager.session_proxy = session_proxy
|
||||
|
||||
async def pair(self, connection):
|
||||
async def pair(self, connection: Connection):
|
||||
return await self.smp_manager.pair(connection)
|
||||
|
||||
def request_pairing(self, connection):
|
||||
def request_pairing(self, connection: Connection):
|
||||
return self.smp_manager.request_pairing(connection)
|
||||
|
||||
async def get_long_term_key(
|
||||
@@ -4460,7 +4495,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
on_authentication_failure,
|
||||
)
|
||||
|
||||
async def encrypt(self, connection, enable=True):
|
||||
async def encrypt(self, connection: Connection, enable: bool = True):
|
||||
if not enable and connection.transport == PhysicalTransport.LE:
|
||||
raise InvalidArgumentError('`enable` parameter is classic only.')
|
||||
|
||||
@@ -4470,7 +4505,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
def on_encryption_change():
|
||||
pending_encryption.set_result(None)
|
||||
|
||||
def on_encryption_failure(error_code):
|
||||
def on_encryption_failure(error_code: int):
|
||||
pending_encryption.set_exception(hci.HCI_Error(error_code))
|
||||
|
||||
connection.on(
|
||||
@@ -4562,10 +4597,10 @@ class Device(utils.CompositeEventEmitter):
|
||||
async def switch_role(self, connection: Connection, role: hci.Role):
|
||||
pending_role_change = asyncio.get_running_loop().create_future()
|
||||
|
||||
def on_role_change(new_role):
|
||||
def on_role_change(new_role: hci.Role):
|
||||
pending_role_change.set_result(new_role)
|
||||
|
||||
def on_role_change_failure(error_code):
|
||||
def on_role_change_failure(error_code: int):
|
||||
pending_role_change.set_exception(hci.HCI_Error(error_code))
|
||||
|
||||
connection.on(connection.EVENT_ROLE_CHANGE, on_role_change)
|
||||
@@ -5155,10 +5190,10 @@ class Device(utils.CompositeEventEmitter):
|
||||
):
|
||||
connection.emit(connection.EVENT_LINK_KEY)
|
||||
|
||||
def add_service(self, service):
|
||||
def add_service(self, service: gatt.Service):
|
||||
self.gatt_server.add_service(service)
|
||||
|
||||
def add_services(self, services):
|
||||
def add_services(self, services: Iterable[gatt.Service]):
|
||||
self.gatt_server.add_services(services)
|
||||
|
||||
def add_default_services(
|
||||
@@ -5254,10 +5289,10 @@ class Device(utils.CompositeEventEmitter):
|
||||
@host_event_handler
|
||||
def on_advertising_set_termination(
|
||||
self,
|
||||
status,
|
||||
advertising_handle,
|
||||
connection_handle,
|
||||
number_of_completed_extended_advertising_events,
|
||||
status: int,
|
||||
advertising_handle: int,
|
||||
connection_handle: int,
|
||||
number_of_completed_extended_advertising_events: int,
|
||||
):
|
||||
# Legacy advertising set is also one of extended advertising sets.
|
||||
if not (
|
||||
@@ -5425,15 +5460,47 @@ class Device(utils.CompositeEventEmitter):
|
||||
self.emit(self.EVENT_CONNECTION, connection)
|
||||
|
||||
@host_event_handler
|
||||
def on_connection(
|
||||
def on_classic_connection(
|
||||
self,
|
||||
connection_handle: int,
|
||||
peer_address: hci.Address,
|
||||
) -> None:
|
||||
if connection := self.pending_connections.pop(peer_address, None):
|
||||
connection.handle = connection_handle
|
||||
else:
|
||||
# Create a new connection
|
||||
connection = Connection(
|
||||
device=self,
|
||||
handle=connection_handle,
|
||||
transport=PhysicalTransport.BR_EDR,
|
||||
self_address=self.public_address,
|
||||
self_resolvable_address=None,
|
||||
peer_address=peer_address,
|
||||
peer_resolvable_address=None,
|
||||
role=hci.Role.PERIPHERAL,
|
||||
parameters=Connection.Parameters(0.0, 0, 0.0),
|
||||
)
|
||||
|
||||
logger.debug('*** %s', connection)
|
||||
if connection_handle in self.connections:
|
||||
logger.warning(
|
||||
'new connection reuses the same handle as a previous connection'
|
||||
)
|
||||
self.connections[connection_handle] = connection
|
||||
|
||||
self.emit(self.EVENT_CONNECTION, connection)
|
||||
|
||||
@host_event_handler
|
||||
def on_le_connection(
|
||||
self,
|
||||
connection_handle: int,
|
||||
transport: core.PhysicalTransport,
|
||||
peer_address: hci.Address,
|
||||
self_resolvable_address: Optional[hci.Address],
|
||||
peer_resolvable_address: Optional[hci.Address],
|
||||
role: hci.Role,
|
||||
connection_parameters: Optional[core.ConnectionParameters],
|
||||
connection_interval: int,
|
||||
peripheral_latency: int,
|
||||
supervision_timeout: int,
|
||||
) -> None:
|
||||
# Convert all-zeros addresses into None.
|
||||
if self_resolvable_address == hci.Address.ANY_RANDOM:
|
||||
@@ -5453,19 +5520,6 @@ class Device(utils.CompositeEventEmitter):
|
||||
'new connection reuses the same handle as a previous connection'
|
||||
)
|
||||
|
||||
if transport == PhysicalTransport.BR_EDR:
|
||||
# Create a new connection
|
||||
connection = self.pending_connections.pop(peer_address)
|
||||
connection.complete(connection_handle, connection_parameters)
|
||||
self.connections[connection_handle] = connection
|
||||
|
||||
# Emit an event to notify listeners of the new connection
|
||||
self.emit(self.EVENT_CONNECTION, connection)
|
||||
|
||||
return
|
||||
|
||||
assert connection_parameters is not None
|
||||
|
||||
if peer_resolvable_address is None:
|
||||
# Resolve the peer address if we can
|
||||
if self.address_resolver:
|
||||
@@ -5515,16 +5569,16 @@ class Device(utils.CompositeEventEmitter):
|
||||
connection = Connection(
|
||||
self,
|
||||
connection_handle,
|
||||
transport,
|
||||
PhysicalTransport.LE,
|
||||
self_address,
|
||||
self_resolvable_address,
|
||||
peer_address,
|
||||
peer_resolvable_address,
|
||||
role,
|
||||
Connection.Parameters(
|
||||
connection_parameters.connection_interval * 1.25,
|
||||
connection_parameters.peripheral_latency,
|
||||
connection_parameters.supervision_timeout * 10.0,
|
||||
connection_interval * 1.25,
|
||||
peripheral_latency,
|
||||
supervision_timeout * 10.0,
|
||||
),
|
||||
)
|
||||
self.connections[connection_handle] = connection
|
||||
@@ -5556,7 +5610,12 @@ class Device(utils.CompositeEventEmitter):
|
||||
)
|
||||
|
||||
@host_event_handler
|
||||
def on_connection_failure(self, transport, peer_address, error_code):
|
||||
def on_connection_failure(
|
||||
self,
|
||||
transport: hci.PhysicalTransport,
|
||||
peer_address: hci.Address,
|
||||
error_code: int,
|
||||
):
|
||||
logger.debug(
|
||||
f'*** Connection failed: {hci.HCI_Constant.error_name(error_code)}'
|
||||
)
|
||||
@@ -5581,7 +5640,9 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
# FIXME: Explore a delegate-model for BR/EDR wait connection #56.
|
||||
@host_event_handler
|
||||
def on_connection_request(self, bd_addr, class_of_device, link_type):
|
||||
def on_connection_request(
|
||||
self, bd_addr: hci.Address, class_of_device: int, link_type: int
|
||||
):
|
||||
logger.debug(f'*** Connection request: {bd_addr}')
|
||||
|
||||
# Handle SCO request.
|
||||
@@ -5610,8 +5671,16 @@ class Device(utils.CompositeEventEmitter):
|
||||
# device configuration is set to accept any incoming connection
|
||||
elif self.classic_accept_any:
|
||||
# Save pending connection
|
||||
self.pending_connections[bd_addr] = Connection.incomplete(
|
||||
self, bd_addr, hci.Role.PERIPHERAL
|
||||
self.pending_connections[bd_addr] = Connection(
|
||||
device=self,
|
||||
handle=0,
|
||||
transport=core.PhysicalTransport.BR_EDR,
|
||||
self_address=self.public_address,
|
||||
self_resolvable_address=None,
|
||||
peer_address=bd_addr,
|
||||
peer_resolvable_address=None,
|
||||
role=hci.Role.PERIPHERAL,
|
||||
parameters=Connection.Parameters(0, 0, 0),
|
||||
)
|
||||
|
||||
self.host.send_command_sync(
|
||||
@@ -5675,7 +5744,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_authentication(self, connection):
|
||||
def on_connection_authentication(self, connection: Connection):
|
||||
logger.debug(
|
||||
f'*** Connection Authentication: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}'
|
||||
@@ -5685,7 +5754,9 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_authentication_failure(self, connection, error):
|
||||
def on_connection_authentication_failure(
|
||||
self, connection: Connection, error: core.ConnectionError
|
||||
):
|
||||
logger.debug(
|
||||
f'*** Connection Authentication Failure: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}, error={error}'
|
||||
@@ -5727,10 +5798,13 @@ class Device(utils.CompositeEventEmitter):
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_authentication_io_capability_response(
|
||||
self, connection, io_capability, authentication_requirements
|
||||
self,
|
||||
connection: Connection,
|
||||
io_capability: int,
|
||||
authentication_requirements: int,
|
||||
):
|
||||
connection.peer_pairing_io_capability = io_capability
|
||||
connection.peer_pairing_authentication_requirements = (
|
||||
connection.pairing_peer_io_capability = io_capability
|
||||
connection.pairing_peer_authentication_requirements = (
|
||||
authentication_requirements
|
||||
)
|
||||
|
||||
@@ -5741,7 +5815,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
# Ask what the pairing config should be for this connection
|
||||
pairing_config = self.pairing_config_factory(connection)
|
||||
io_capability = pairing_config.delegate.classic_io_capability
|
||||
peer_io_capability = connection.peer_pairing_io_capability
|
||||
peer_io_capability = connection.pairing_peer_io_capability
|
||||
|
||||
async def confirm() -> bool:
|
||||
# Ask the user to confirm the pairing, without display
|
||||
@@ -5816,7 +5890,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_authentication_user_passkey_request(self, connection) -> None:
|
||||
def on_authentication_user_passkey_request(self, connection: Connection) -> None:
|
||||
# Ask what the pairing config should be for this connection
|
||||
pairing_config = self.pairing_config_factory(connection)
|
||||
|
||||
@@ -5859,7 +5933,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_pin_code_request(self, connection):
|
||||
def on_pin_code_request(self, connection: Connection):
|
||||
# Classic legacy pairing
|
||||
# Ask what the pairing config should be for this connection
|
||||
pairing_config = self.pairing_config_factory(connection)
|
||||
@@ -5903,7 +5977,9 @@ class Device(utils.CompositeEventEmitter):
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_authentication_user_passkey_notification(self, connection, passkey):
|
||||
def on_authentication_user_passkey_notification(
|
||||
self, connection: Connection, passkey: int
|
||||
):
|
||||
# Ask what the pairing config should be for this connection
|
||||
pairing_config = self.pairing_config_factory(connection)
|
||||
|
||||
@@ -5915,14 +5991,15 @@ class Device(utils.CompositeEventEmitter):
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@try_with_connection_from_address
|
||||
def on_remote_name(self, connection: Connection, address, remote_name):
|
||||
def on_remote_name(
|
||||
self, connection: Optional[Connection], address: hci.Address, remote_name: bytes
|
||||
):
|
||||
# Try to decode the name
|
||||
try:
|
||||
remote_name = remote_name.decode('utf-8')
|
||||
if connection:
|
||||
connection.peer_name = remote_name
|
||||
connection.peer_name = remote_name.decode('utf-8')
|
||||
connection.emit(connection.EVENT_REMOTE_NAME)
|
||||
self.emit(self.EVENT_REMOTE_NAME, address, remote_name)
|
||||
self.emit(self.EVENT_REMOTE_NAME, address, remote_name.decode('utf-8'))
|
||||
except UnicodeDecodeError as error:
|
||||
logger.warning('peer name is not valid UTF-8')
|
||||
if connection:
|
||||
@@ -5933,7 +6010,9 @@ class Device(utils.CompositeEventEmitter):
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@try_with_connection_from_address
|
||||
def on_remote_name_failure(self, connection: Connection, address, error):
|
||||
def on_remote_name_failure(
|
||||
self, connection: Optional[Connection], address: hci.Address, error: int
|
||||
):
|
||||
if connection:
|
||||
connection.emit(connection.EVENT_REMOTE_NAME_FAILURE, error)
|
||||
self.emit(self.EVENT_REMOTE_NAME_FAILURE, address, error)
|
||||
@@ -6134,7 +6213,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_encryption_key_refresh(self, connection):
|
||||
def on_connection_encryption_key_refresh(self, connection: Connection):
|
||||
logger.debug(
|
||||
f'*** Connection Key Refresh: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}'
|
||||
@@ -6144,27 +6223,27 @@ class Device(utils.CompositeEventEmitter):
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_parameters_update(
|
||||
self, connection: Connection, connection_parameters: core.ConnectionParameters
|
||||
self,
|
||||
connection: Connection,
|
||||
connection_interval: int,
|
||||
peripheral_latency: int,
|
||||
supervision_timeout: int,
|
||||
):
|
||||
logger.debug(
|
||||
f'*** Connection Parameters Update: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}, '
|
||||
f'{connection_parameters}'
|
||||
)
|
||||
if (
|
||||
connection.parameters.connection_interval
|
||||
!= connection_parameters.connection_interval * 1.25
|
||||
):
|
||||
if connection.parameters.connection_interval != connection_interval * 1.25:
|
||||
connection.parameters = Connection.Parameters(
|
||||
connection_parameters.connection_interval * 1.25,
|
||||
connection_parameters.peripheral_latency,
|
||||
connection_parameters.supervision_timeout * 10.0,
|
||||
connection_interval * 1.25,
|
||||
peripheral_latency,
|
||||
supervision_timeout * 10.0,
|
||||
)
|
||||
else:
|
||||
connection.parameters = Connection.Parameters(
|
||||
connection_parameters.connection_interval * 1.25,
|
||||
connection_parameters.peripheral_latency,
|
||||
connection_parameters.supervision_timeout * 10.0,
|
||||
connection_interval * 1.25,
|
||||
peripheral_latency,
|
||||
supervision_timeout * 10.0,
|
||||
connection.parameters.subrate_factor,
|
||||
connection.parameters.continuation_number,
|
||||
)
|
||||
@@ -6172,7 +6251,9 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_parameters_update_failure(self, connection, error):
|
||||
def on_connection_parameters_update_failure(
|
||||
self, connection: Connection, error: int
|
||||
):
|
||||
logger.debug(
|
||||
f'*** Connection Parameters Update Failed: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}, '
|
||||
@@ -6182,7 +6263,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_phy_update(self, connection, phy):
|
||||
def on_connection_phy_update(self, connection: Connection, phy: core.ConnectionPHY):
|
||||
logger.debug(
|
||||
f'*** Connection PHY Update: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}, '
|
||||
@@ -6192,7 +6273,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_phy_update_failure(self, connection, error):
|
||||
def on_connection_phy_update_failure(self, connection: Connection, error: int):
|
||||
logger.debug(
|
||||
f'*** Connection PHY Update Failed: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}, '
|
||||
@@ -6221,7 +6302,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_att_mtu_update(self, connection, att_mtu):
|
||||
def on_connection_att_mtu_update(self, connection: Connection, att_mtu: int):
|
||||
logger.debug(
|
||||
f'*** Connection ATT MTU Update: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}, '
|
||||
@@ -6233,7 +6314,12 @@ class Device(utils.CompositeEventEmitter):
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_data_length_change(
|
||||
self, connection, max_tx_octets, max_tx_time, max_rx_octets, max_rx_time
|
||||
self,
|
||||
connection: Connection,
|
||||
max_tx_octets: int,
|
||||
max_tx_time: int,
|
||||
max_rx_octets: int,
|
||||
max_rx_time: int,
|
||||
):
|
||||
logger.debug(
|
||||
f'*** Connection Data Length Change: [0x{connection.handle:04X}] '
|
||||
@@ -6358,14 +6444,20 @@ class Device(utils.CompositeEventEmitter):
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_role_change(self, connection, new_role):
|
||||
def on_role_change(
|
||||
self,
|
||||
connection: Connection,
|
||||
new_role: hci.Role,
|
||||
):
|
||||
connection.role = new_role
|
||||
connection.emit(connection.EVENT_ROLE_CHANGE, new_role)
|
||||
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@try_with_connection_from_address
|
||||
def on_role_change_failure(self, connection, address, error):
|
||||
def on_role_change_failure(
|
||||
self, connection: Optional[Connection], address: hci.Address, error: int
|
||||
):
|
||||
if connection:
|
||||
connection.emit(connection.EVENT_ROLE_CHANGE_FAILURE, error)
|
||||
self.emit(self.EVENT_ROLE_CHANGE_FAILURE, address, error)
|
||||
@@ -6379,7 +6471,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_classic_pairing_failure(self, connection: Connection, status) -> None:
|
||||
def on_classic_pairing_failure(self, connection: Connection, status: int) -> None:
|
||||
connection.emit(connection.EVENT_CLASSIC_PAIRING_FAILURE, status)
|
||||
|
||||
def on_pairing_start(self, connection: Connection) -> None:
|
||||
@@ -6403,7 +6495,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
connection.emit(connection.EVENT_PAIRING_FAILURE, reason)
|
||||
|
||||
@with_connection_from_handle
|
||||
def on_gatt_pdu(self, connection, pdu):
|
||||
def on_gatt_pdu(self, connection: Connection, pdu: bytes):
|
||||
# Parse the L2CAP payload into an ATT PDU object
|
||||
att_pdu = ATT_PDU.from_bytes(pdu)
|
||||
|
||||
@@ -6425,7 +6517,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
connection.gatt_server.on_gatt_pdu(connection, att_pdu)
|
||||
|
||||
@with_connection_from_handle
|
||||
def on_smp_pdu(self, connection, pdu):
|
||||
def on_smp_pdu(self, connection: Connection, pdu: bytes):
|
||||
self.smp_manager.on_smp_pdu(connection, pdu)
|
||||
|
||||
@host_event_handler
|
||||
|
||||
@@ -26,7 +26,17 @@ import secrets
|
||||
import struct
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import field
|
||||
from typing import Any, Callable, ClassVar, Iterable, Optional, TypeVar, Union, cast
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Iterable,
|
||||
Literal,
|
||||
Optional,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -111,23 +121,57 @@ def phy_list_to_bits(phys: Optional[Iterable[Phy]]) -> int:
|
||||
class SpecableEnum(utils.OpenIntEnum):
|
||||
|
||||
@classmethod
|
||||
def type_spec(cls, size: int):
|
||||
return {'size': size, 'mapper': lambda x: cls(x).name}
|
||||
def type_spec(cls, size: int, byteorder: Literal['little', 'big'] = 'little'):
|
||||
return {
|
||||
'serializer': lambda x: x.to_bytes(size, byteorder),
|
||||
'parser': lambda data, offset: (
|
||||
offset + size,
|
||||
cls(int.from_bytes(data[offset : offset + size], byteorder)),
|
||||
),
|
||||
'mapper': lambda x: cls(x).name,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def type_metadata(cls, size: int, list_begin: bool = False, list_end: bool = False):
|
||||
return metadata(cls.type_spec(size), list_begin=list_begin, list_end=list_end)
|
||||
def type_metadata(
|
||||
cls,
|
||||
size: int,
|
||||
list_begin: bool = False,
|
||||
list_end: bool = False,
|
||||
byteorder: Literal['little', 'big'] = 'little',
|
||||
):
|
||||
return metadata(
|
||||
cls.type_spec(size, byteorder),
|
||||
list_begin=list_begin,
|
||||
list_end=list_end,
|
||||
)
|
||||
|
||||
|
||||
class SpecableFlag(enum.IntFlag):
|
||||
|
||||
@classmethod
|
||||
def type_spec(cls, size: int):
|
||||
return {'size': size, 'mapper': lambda x: cls(x).name}
|
||||
def type_spec(cls, size: int, byteorder: Literal['little', 'big'] = 'little'):
|
||||
return {
|
||||
'serializer': lambda x: x.to_bytes(size, byteorder),
|
||||
'parser': lambda data, offset: (
|
||||
offset + size,
|
||||
cls(int.from_bytes(data[offset : offset + size], byteorder)),
|
||||
),
|
||||
'mapper': lambda x: cls(x).name,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def type_metadata(cls, size: int, list_begin: bool = False, list_end: bool = False):
|
||||
return metadata(cls.type_spec(size), list_begin=list_begin, list_end=list_end)
|
||||
def type_metadata(
|
||||
cls,
|
||||
size: int,
|
||||
list_begin: bool = False,
|
||||
list_end: bool = False,
|
||||
byteorder: Literal['little', 'big'] = 'little',
|
||||
):
|
||||
return metadata(
|
||||
cls.type_spec(size, byteorder),
|
||||
list_begin=list_begin,
|
||||
list_end=list_end,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -6422,7 +6466,9 @@ class HCI_LE_Create_BIG_Complete_Event(HCI_LE_Meta_Event):
|
||||
irc: int = field(metadata=metadata(1))
|
||||
max_pdu: int = field(metadata=metadata(2))
|
||||
iso_interval: int = field(metadata=metadata(2))
|
||||
connection_handle: int = field(metadata=metadata(2, list_begin=True, list_end=True))
|
||||
connection_handle: Sequence[int] = field(
|
||||
metadata=metadata(2, list_begin=True, list_end=True)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -6454,7 +6500,9 @@ class HCI_LE_BIG_Sync_Established_Event(HCI_LE_Meta_Event):
|
||||
irc: int = field(metadata=metadata(1))
|
||||
max_pdu: int = field(metadata=metadata(2))
|
||||
iso_interval: int = field(metadata=metadata(2))
|
||||
connection_handle: int = field(metadata=metadata(2, list_begin=True, list_end=True))
|
||||
connection_handle: Sequence[int] = field(
|
||||
metadata=metadata(2, list_begin=True, list_end=True)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
260
bumble/host.py
260
bumble/host.py
@@ -22,11 +22,11 @@ import collections
|
||||
import dataclasses
|
||||
import logging
|
||||
import struct
|
||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, cast
|
||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, Union, cast
|
||||
|
||||
from bumble import drivers, hci, utils
|
||||
from bumble.colors import color
|
||||
from bumble.core import ConnectionParameters, ConnectionPHY, PhysicalTransport
|
||||
from bumble.core import ConnectionPHY, InvalidStateError, PhysicalTransport
|
||||
from bumble.l2cap import L2CAP_PDU
|
||||
from bumble.snoop import Snooper
|
||||
from bumble.transport.common import TransportLostError
|
||||
@@ -550,7 +550,7 @@ class Host(utils.EventEmitter):
|
||||
logger.debug(
|
||||
'HCI LE flow control: '
|
||||
f'le_acl_data_packet_length={le_acl_data_packet_length},'
|
||||
f'total_num_le_acl_data_packets={total_num_le_acl_data_packets}'
|
||||
f'total_num_le_acl_data_packets={total_num_le_acl_data_packets},'
|
||||
f'iso_data_packet_length={iso_data_packet_length},'
|
||||
f'total_num_iso_data_packets={total_num_iso_data_packets}'
|
||||
)
|
||||
@@ -902,10 +902,14 @@ class Host(utils.EventEmitter):
|
||||
def on_l2cap_pdu(self, connection: Connection, cid: int, pdu: bytes) -> None:
|
||||
self.emit('l2cap_pdu', connection.handle, cid, pdu)
|
||||
|
||||
def on_command_processed(self, event):
|
||||
def on_command_processed(
|
||||
self, event: Union[hci.HCI_Command_Complete_Event, hci.HCI_Command_Status_Event]
|
||||
):
|
||||
if self.pending_response:
|
||||
# Check that it is what we were expecting
|
||||
if self.pending_command.op_code != event.command_opcode:
|
||||
if self.pending_command is None:
|
||||
logger.warning('!!! pending_command is None ')
|
||||
elif self.pending_command.op_code != event.command_opcode:
|
||||
logger.warning(
|
||||
'!!! command result mismatch, expected '
|
||||
f'0x{self.pending_command.op_code:X} but got '
|
||||
@@ -919,10 +923,10 @@ class Host(utils.EventEmitter):
|
||||
############################################################
|
||||
# HCI handlers
|
||||
############################################################
|
||||
def on_hci_event(self, event):
|
||||
def on_hci_event(self, event: hci.HCI_Event):
|
||||
logger.warning(f'{color(f"--- Ignoring event {event}", "red")}')
|
||||
|
||||
def on_hci_command_complete_event(self, event):
|
||||
def on_hci_command_complete_event(self, event: hci.HCI_Command_Complete_Event):
|
||||
if event.command_opcode == 0:
|
||||
# This is used just for the Num_HCI_Command_Packets field, not related to
|
||||
# an actual command
|
||||
@@ -931,7 +935,7 @@ class Host(utils.EventEmitter):
|
||||
|
||||
return self.on_command_processed(event)
|
||||
|
||||
def on_hci_command_status_event(self, event):
|
||||
def on_hci_command_status_event(self, event: hci.HCI_Command_Status_Event):
|
||||
return self.on_command_processed(event)
|
||||
|
||||
def on_hci_number_of_completed_packets_event(
|
||||
@@ -951,7 +955,7 @@ class Host(utils.EventEmitter):
|
||||
)
|
||||
|
||||
# Classic only
|
||||
def on_hci_connection_request_event(self, event):
|
||||
def on_hci_connection_request_event(self, event: hci.HCI_Connection_Request_Event):
|
||||
# Notify the listeners
|
||||
self.emit(
|
||||
'connection_request',
|
||||
@@ -960,7 +964,14 @@ class Host(utils.EventEmitter):
|
||||
event.link_type,
|
||||
)
|
||||
|
||||
def on_hci_le_connection_complete_event(self, event):
|
||||
def on_hci_le_connection_complete_event(
|
||||
self,
|
||||
event: Union[
|
||||
hci.HCI_LE_Connection_Complete_Event,
|
||||
hci.HCI_LE_Enhanced_Connection_Complete_Event,
|
||||
hci.HCI_LE_Enhanced_Connection_Complete_V2_Event,
|
||||
],
|
||||
):
|
||||
# Check if this is a cancellation
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
# Create/update the connection
|
||||
@@ -980,20 +991,16 @@ class Host(utils.EventEmitter):
|
||||
self.connections[event.connection_handle] = connection
|
||||
|
||||
# Notify the client
|
||||
connection_parameters = ConnectionParameters(
|
||||
event.connection_interval,
|
||||
event.peripheral_latency,
|
||||
event.supervision_timeout,
|
||||
)
|
||||
self.emit(
|
||||
'connection',
|
||||
'le_connection',
|
||||
event.connection_handle,
|
||||
PhysicalTransport.LE,
|
||||
event.peer_address,
|
||||
getattr(event, 'local_resolvable_private_address', None),
|
||||
getattr(event, 'peer_resolvable_private_address', None),
|
||||
hci.Role(event.role),
|
||||
connection_parameters,
|
||||
event.connection_interval,
|
||||
event.peripheral_latency,
|
||||
event.supervision_timeout,
|
||||
)
|
||||
else:
|
||||
logger.debug(f'### CONNECTION FAILED: {event.status}')
|
||||
@@ -1006,15 +1013,25 @@ class Host(utils.EventEmitter):
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_le_enhanced_connection_complete_event(self, event):
|
||||
def on_hci_le_enhanced_connection_complete_event(
|
||||
self,
|
||||
event: Union[
|
||||
hci.HCI_LE_Enhanced_Connection_Complete_Event,
|
||||
hci.HCI_LE_Enhanced_Connection_Complete_V2_Event,
|
||||
],
|
||||
):
|
||||
# Just use the same implementation as for the non-enhanced event for now
|
||||
self.on_hci_le_connection_complete_event(event)
|
||||
|
||||
def on_hci_le_enhanced_connection_complete_v2_event(self, event):
|
||||
def on_hci_le_enhanced_connection_complete_v2_event(
|
||||
self, event: hci.HCI_LE_Enhanced_Connection_Complete_V2_Event
|
||||
):
|
||||
# Just use the same implementation as for the v1 event for now
|
||||
self.on_hci_le_enhanced_connection_complete_event(event)
|
||||
|
||||
def on_hci_connection_complete_event(self, event):
|
||||
def on_hci_connection_complete_event(
|
||||
self, event: hci.HCI_Connection_Complete_Event
|
||||
):
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
# Create/update the connection
|
||||
logger.debug(
|
||||
@@ -1034,14 +1051,9 @@ class Host(utils.EventEmitter):
|
||||
|
||||
# Notify the client
|
||||
self.emit(
|
||||
'connection',
|
||||
'classic_connection',
|
||||
event.connection_handle,
|
||||
PhysicalTransport.BR_EDR,
|
||||
event.bd_addr,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
else:
|
||||
logger.debug(f'### BR/EDR CONNECTION FAILED: {event.status}')
|
||||
@@ -1054,7 +1066,9 @@ class Host(utils.EventEmitter):
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_disconnection_complete_event(self, event):
|
||||
def on_hci_disconnection_complete_event(
|
||||
self, event: hci.HCI_Disconnection_Complete_Event
|
||||
):
|
||||
# Find the connection
|
||||
handle = event.connection_handle
|
||||
if (
|
||||
@@ -1093,27 +1107,30 @@ class Host(utils.EventEmitter):
|
||||
# Notify the listeners
|
||||
self.emit('disconnection_failure', handle, event.status)
|
||||
|
||||
def on_hci_le_connection_update_complete_event(self, event):
|
||||
def on_hci_le_connection_update_complete_event(
|
||||
self, event: hci.HCI_LE_Connection_Update_Complete_Event
|
||||
):
|
||||
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||
logger.warning('!!! CONNECTION PARAMETERS UPDATE COMPLETE: unknown handle')
|
||||
return
|
||||
|
||||
# Notify the client
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
connection_parameters = ConnectionParameters(
|
||||
self.emit(
|
||||
'connection_parameters_update',
|
||||
connection.handle,
|
||||
event.connection_interval,
|
||||
event.peripheral_latency,
|
||||
event.supervision_timeout,
|
||||
)
|
||||
self.emit(
|
||||
'connection_parameters_update', connection.handle, connection_parameters
|
||||
)
|
||||
else:
|
||||
self.emit(
|
||||
'connection_parameters_update_failure', connection.handle, event.status
|
||||
)
|
||||
|
||||
def on_hci_le_phy_update_complete_event(self, event):
|
||||
def on_hci_le_phy_update_complete_event(
|
||||
self, event: hci.HCI_LE_PHY_Update_Complete_Event
|
||||
):
|
||||
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||
logger.warning('!!! CONNECTION PHY UPDATE COMPLETE: unknown handle')
|
||||
return
|
||||
@@ -1143,7 +1160,9 @@ class Host(utils.EventEmitter):
|
||||
):
|
||||
self.on_hci_le_advertising_report_event(event)
|
||||
|
||||
def on_hci_le_advertising_set_terminated_event(self, event):
|
||||
def on_hci_le_advertising_set_terminated_event(
|
||||
self, event: hci.HCI_LE_Advertising_Set_Terminated_Event
|
||||
):
|
||||
self.emit(
|
||||
'advertising_set_termination',
|
||||
event.status,
|
||||
@@ -1152,7 +1171,9 @@ class Host(utils.EventEmitter):
|
||||
event.num_completed_extended_advertising_events,
|
||||
)
|
||||
|
||||
def on_hci_le_periodic_advertising_sync_established_event(self, event):
|
||||
def on_hci_le_periodic_advertising_sync_established_event(
|
||||
self, event: hci.HCI_LE_Periodic_Advertising_Sync_Established_Event
|
||||
):
|
||||
self.emit(
|
||||
'periodic_advertising_sync_establishment',
|
||||
event.status,
|
||||
@@ -1164,16 +1185,22 @@ class Host(utils.EventEmitter):
|
||||
event.advertiser_clock_accuracy,
|
||||
)
|
||||
|
||||
def on_hci_le_periodic_advertising_sync_lost_event(self, event):
|
||||
def on_hci_le_periodic_advertising_sync_lost_event(
|
||||
self, event: hci.HCI_LE_Periodic_Advertising_Sync_Lost_Event
|
||||
):
|
||||
self.emit('periodic_advertising_sync_loss', event.sync_handle)
|
||||
|
||||
def on_hci_le_periodic_advertising_report_event(self, event):
|
||||
def on_hci_le_periodic_advertising_report_event(
|
||||
self, event: hci.HCI_LE_Periodic_Advertising_Report_Event
|
||||
):
|
||||
self.emit('periodic_advertising_report', event.sync_handle, event)
|
||||
|
||||
def on_hci_le_biginfo_advertising_report_event(self, event):
|
||||
def on_hci_le_biginfo_advertising_report_event(
|
||||
self, event: hci.HCI_LE_BIGInfo_Advertising_Report_Event
|
||||
):
|
||||
self.emit('biginfo_advertising_report', event.sync_handle, event)
|
||||
|
||||
def on_hci_le_cis_request_event(self, event):
|
||||
def on_hci_le_cis_request_event(self, event: hci.HCI_LE_CIS_Request_Event):
|
||||
self.emit(
|
||||
'cis_request',
|
||||
event.acl_connection_handle,
|
||||
@@ -1182,10 +1209,12 @@ class Host(utils.EventEmitter):
|
||||
event.cis_id,
|
||||
)
|
||||
|
||||
def on_hci_le_create_big_complete_event(self, event):
|
||||
def on_hci_le_create_big_complete_event(
|
||||
self, event: hci.HCI_LE_Create_BIG_Complete_Event
|
||||
):
|
||||
self.bigs[event.big_handle] = set(event.connection_handle)
|
||||
if self.iso_packet_queue is None:
|
||||
logger.warning("BIS established but ISO packets not supported")
|
||||
raise InvalidStateError("BIS established but ISO packets not supported")
|
||||
|
||||
for connection_handle in event.connection_handle:
|
||||
self.bis_links[connection_handle] = IsoLink(
|
||||
@@ -1208,8 +1237,13 @@ class Host(utils.EventEmitter):
|
||||
event.iso_interval,
|
||||
)
|
||||
|
||||
def on_hci_le_big_sync_established_event(self, event):
|
||||
def on_hci_le_big_sync_established_event(
|
||||
self, event: hci.HCI_LE_BIG_Sync_Established_Event
|
||||
):
|
||||
self.bigs[event.big_handle] = set(event.connection_handle)
|
||||
if self.iso_packet_queue is None:
|
||||
raise InvalidStateError("BIS established but ISO packets not supported")
|
||||
|
||||
for connection_handle in event.connection_handle:
|
||||
self.bis_links[connection_handle] = IsoLink(
|
||||
connection_handle, self.iso_packet_queue
|
||||
@@ -1229,15 +1263,19 @@ class Host(utils.EventEmitter):
|
||||
event.connection_handle,
|
||||
)
|
||||
|
||||
def on_hci_le_big_sync_lost_event(self, event):
|
||||
def on_hci_le_big_sync_lost_event(self, event: hci.HCI_LE_BIG_Sync_Lost_Event):
|
||||
self.remove_big(event.big_handle)
|
||||
self.emit('big_sync_lost', event.big_handle, event.reason)
|
||||
|
||||
def on_hci_le_terminate_big_complete_event(self, event):
|
||||
def on_hci_le_terminate_big_complete_event(
|
||||
self, event: hci.HCI_LE_Terminate_BIG_Complete_Event
|
||||
):
|
||||
self.remove_big(event.big_handle)
|
||||
self.emit('big_termination', event.reason, event.big_handle)
|
||||
|
||||
def on_hci_le_periodic_advertising_sync_transfer_received_event(self, event):
|
||||
def on_hci_le_periodic_advertising_sync_transfer_received_event(
|
||||
self, event: hci.HCI_LE_Periodic_Advertising_Sync_Transfer_Received_Event
|
||||
):
|
||||
self.emit(
|
||||
'periodic_advertising_sync_transfer',
|
||||
event.status,
|
||||
@@ -1250,7 +1288,9 @@ class Host(utils.EventEmitter):
|
||||
event.advertiser_clock_accuracy,
|
||||
)
|
||||
|
||||
def on_hci_le_periodic_advertising_sync_transfer_received_v2_event(self, event):
|
||||
def on_hci_le_periodic_advertising_sync_transfer_received_v2_event(
|
||||
self, event: hci.HCI_LE_Periodic_Advertising_Sync_Transfer_Received_V2_Event
|
||||
):
|
||||
self.emit(
|
||||
'periodic_advertising_sync_transfer',
|
||||
event.status,
|
||||
@@ -1263,11 +1303,11 @@ class Host(utils.EventEmitter):
|
||||
event.advertiser_clock_accuracy,
|
||||
)
|
||||
|
||||
def on_hci_le_cis_established_event(self, event):
|
||||
def on_hci_le_cis_established_event(self, event: hci.HCI_LE_CIS_Established_Event):
|
||||
# The remaining parameters are unused for now.
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
if self.iso_packet_queue is None:
|
||||
logger.warning("CIS established but ISO packets not supported")
|
||||
raise InvalidStateError("CIS established but ISO packets not supported")
|
||||
self.cis_links[event.connection_handle] = IsoLink(
|
||||
handle=event.connection_handle, packet_queue=self.iso_packet_queue
|
||||
)
|
||||
@@ -1294,7 +1334,9 @@ class Host(utils.EventEmitter):
|
||||
'cis_establishment_failure', event.connection_handle, event.status
|
||||
)
|
||||
|
||||
def on_hci_le_remote_connection_parameter_request_event(self, event):
|
||||
def on_hci_le_remote_connection_parameter_request_event(
|
||||
self, event: hci.HCI_LE_Remote_Connection_Parameter_Request_Event
|
||||
):
|
||||
if event.connection_handle not in self.connections:
|
||||
logger.warning('!!! REMOTE CONNECTION PARAMETER REQUEST: unknown handle')
|
||||
return
|
||||
@@ -1313,7 +1355,9 @@ class Host(utils.EventEmitter):
|
||||
)
|
||||
)
|
||||
|
||||
def on_hci_le_long_term_key_request_event(self, event):
|
||||
def on_hci_le_long_term_key_request_event(
|
||||
self, event: hci.HCI_LE_Long_Term_Key_Request_Event
|
||||
):
|
||||
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||
logger.warning('!!! LE LONG TERM KEY REQUEST: unknown handle')
|
||||
return
|
||||
@@ -1347,7 +1391,9 @@ class Host(utils.EventEmitter):
|
||||
|
||||
asyncio.create_task(send_long_term_key())
|
||||
|
||||
def on_hci_synchronous_connection_complete_event(self, event):
|
||||
def on_hci_synchronous_connection_complete_event(
|
||||
self, event: hci.HCI_Synchronous_Connection_Complete_Event
|
||||
):
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
# Create/update the connection
|
||||
logger.debug(
|
||||
@@ -1373,7 +1419,9 @@ class Host(utils.EventEmitter):
|
||||
# Notify the client
|
||||
self.emit('sco_connection_failure', event.bd_addr, event.status)
|
||||
|
||||
def on_hci_synchronous_connection_changed_event(self, event):
|
||||
def on_hci_synchronous_connection_changed_event(
|
||||
self, event: hci.HCI_Synchronous_Connection_Changed_Event
|
||||
):
|
||||
pass
|
||||
|
||||
def on_hci_mode_change_event(self, event: hci.HCI_Mode_Change_Event):
|
||||
@@ -1385,7 +1433,7 @@ class Host(utils.EventEmitter):
|
||||
event.interval,
|
||||
)
|
||||
|
||||
def on_hci_role_change_event(self, event):
|
||||
def on_hci_role_change_event(self, event: hci.HCI_Role_Change_Event):
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
logger.debug(
|
||||
f'role change for {event.bd_addr}: '
|
||||
@@ -1399,7 +1447,9 @@ class Host(utils.EventEmitter):
|
||||
)
|
||||
self.emit('role_change_failure', event.bd_addr, event.status)
|
||||
|
||||
def on_hci_le_data_length_change_event(self, event):
|
||||
def on_hci_le_data_length_change_event(
|
||||
self, event: hci.HCI_LE_Data_Length_Change_Event
|
||||
):
|
||||
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||
logger.warning('!!! DATA LENGTH CHANGE: unknown handle')
|
||||
return
|
||||
@@ -1413,7 +1463,9 @@ class Host(utils.EventEmitter):
|
||||
event.max_rx_time,
|
||||
)
|
||||
|
||||
def on_hci_authentication_complete_event(self, event):
|
||||
def on_hci_authentication_complete_event(
|
||||
self, event: hci.HCI_Authentication_Complete_Event
|
||||
):
|
||||
# Notify the client
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
self.emit('connection_authentication', event.connection_handle)
|
||||
@@ -1454,7 +1506,9 @@ class Host(utils.EventEmitter):
|
||||
'connection_encryption_failure', event.connection_handle, event.status
|
||||
)
|
||||
|
||||
def on_hci_encryption_key_refresh_complete_event(self, event):
|
||||
def on_hci_encryption_key_refresh_complete_event(
|
||||
self, event: hci.HCI_Encryption_Key_Refresh_Complete_Event
|
||||
):
|
||||
# Notify the client
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
self.emit('connection_encryption_key_refresh', event.connection_handle)
|
||||
@@ -1465,7 +1519,7 @@ class Host(utils.EventEmitter):
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_qos_setup_complete_event(self, event):
|
||||
def on_hci_qos_setup_complete_event(self, event: hci.HCI_QOS_Setup_Complete_Event):
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
self.emit(
|
||||
'connection_qos_setup', event.connection_handle, event.service_type
|
||||
@@ -1477,23 +1531,31 @@ class Host(utils.EventEmitter):
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_link_supervision_timeout_changed_event(self, event):
|
||||
def on_hci_link_supervision_timeout_changed_event(
|
||||
self, event: hci.HCI_Link_Supervision_Timeout_Changed_Event
|
||||
):
|
||||
pass
|
||||
|
||||
def on_hci_max_slots_change_event(self, event):
|
||||
def on_hci_max_slots_change_event(self, event: hci.HCI_Max_Slots_Change_Event):
|
||||
pass
|
||||
|
||||
def on_hci_page_scan_repetition_mode_change_event(self, event):
|
||||
def on_hci_page_scan_repetition_mode_change_event(
|
||||
self, event: hci.HCI_Page_Scan_Repetition_Mode_Change_Event
|
||||
):
|
||||
pass
|
||||
|
||||
def on_hci_link_key_notification_event(self, event):
|
||||
def on_hci_link_key_notification_event(
|
||||
self, event: hci.HCI_Link_Key_Notification_Event
|
||||
):
|
||||
logger.debug(
|
||||
f'link key for {event.bd_addr}: {event.link_key.hex()}, '
|
||||
f'type={hci.HCI_Constant.link_key_type_name(event.key_type)}'
|
||||
)
|
||||
self.emit('link_key', event.bd_addr, event.link_key, event.key_type)
|
||||
|
||||
def on_hci_simple_pairing_complete_event(self, event):
|
||||
def on_hci_simple_pairing_complete_event(
|
||||
self, event: hci.HCI_Simple_Pairing_Complete_Event
|
||||
):
|
||||
logger.debug(
|
||||
f'simple pairing complete for {event.bd_addr}: '
|
||||
f'status={hci.HCI_Constant.status_name(event.status)}'
|
||||
@@ -1503,10 +1565,10 @@ class Host(utils.EventEmitter):
|
||||
else:
|
||||
self.emit('classic_pairing_failure', event.bd_addr, event.status)
|
||||
|
||||
def on_hci_pin_code_request_event(self, event):
|
||||
def on_hci_pin_code_request_event(self, event: hci.HCI_PIN_Code_Request_Event):
|
||||
self.emit('pin_code_request', event.bd_addr)
|
||||
|
||||
def on_hci_link_key_request_event(self, event):
|
||||
def on_hci_link_key_request_event(self, event: hci.HCI_Link_Key_Request_Event):
|
||||
async def send_link_key():
|
||||
if self.link_key_provider is None:
|
||||
logger.debug('no link key provider')
|
||||
@@ -1531,10 +1593,14 @@ class Host(utils.EventEmitter):
|
||||
|
||||
asyncio.create_task(send_link_key())
|
||||
|
||||
def on_hci_io_capability_request_event(self, event):
|
||||
def on_hci_io_capability_request_event(
|
||||
self, event: hci.HCI_IO_Capability_Request_Event
|
||||
):
|
||||
self.emit('authentication_io_capability_request', event.bd_addr)
|
||||
|
||||
def on_hci_io_capability_response_event(self, event):
|
||||
def on_hci_io_capability_response_event(
|
||||
self, event: hci.HCI_IO_Capability_Response_Event
|
||||
):
|
||||
self.emit(
|
||||
'authentication_io_capability_response',
|
||||
event.bd_addr,
|
||||
@@ -1542,25 +1608,33 @@ class Host(utils.EventEmitter):
|
||||
event.authentication_requirements,
|
||||
)
|
||||
|
||||
def on_hci_user_confirmation_request_event(self, event):
|
||||
def on_hci_user_confirmation_request_event(
|
||||
self, event: hci.HCI_User_Confirmation_Request_Event
|
||||
):
|
||||
self.emit(
|
||||
'authentication_user_confirmation_request',
|
||||
event.bd_addr,
|
||||
event.numeric_value,
|
||||
)
|
||||
|
||||
def on_hci_user_passkey_request_event(self, event):
|
||||
def on_hci_user_passkey_request_event(
|
||||
self, event: hci.HCI_User_Passkey_Request_Event
|
||||
):
|
||||
self.emit('authentication_user_passkey_request', event.bd_addr)
|
||||
|
||||
def on_hci_user_passkey_notification_event(self, event):
|
||||
def on_hci_user_passkey_notification_event(
|
||||
self, event: hci.HCI_User_Passkey_Notification_Event
|
||||
):
|
||||
self.emit(
|
||||
'authentication_user_passkey_notification', event.bd_addr, event.passkey
|
||||
)
|
||||
|
||||
def on_hci_inquiry_complete_event(self, _event):
|
||||
def on_hci_inquiry_complete_event(self, _event: hci.HCI_Inquiry_Complete_Event):
|
||||
self.emit('inquiry_complete')
|
||||
|
||||
def on_hci_inquiry_result_with_rssi_event(self, event):
|
||||
def on_hci_inquiry_result_with_rssi_event(
|
||||
self, event: hci.HCI_Inquiry_Result_With_RSSI_Event
|
||||
):
|
||||
for bd_addr, class_of_device, rssi in zip(
|
||||
event.bd_addr, event.class_of_device, event.rssi
|
||||
):
|
||||
@@ -1572,7 +1646,9 @@ class Host(utils.EventEmitter):
|
||||
rssi,
|
||||
)
|
||||
|
||||
def on_hci_extended_inquiry_result_event(self, event):
|
||||
def on_hci_extended_inquiry_result_event(
|
||||
self, event: hci.HCI_Extended_Inquiry_Result_Event
|
||||
):
|
||||
self.emit(
|
||||
'inquiry_result',
|
||||
event.bd_addr,
|
||||
@@ -1581,7 +1657,9 @@ class Host(utils.EventEmitter):
|
||||
event.rssi,
|
||||
)
|
||||
|
||||
def on_hci_remote_name_request_complete_event(self, event):
|
||||
def on_hci_remote_name_request_complete_event(
|
||||
self, event: hci.HCI_Remote_Name_Request_Complete_Event
|
||||
):
|
||||
if event.status != hci.HCI_SUCCESS:
|
||||
self.emit('remote_name_failure', event.bd_addr, event.status)
|
||||
else:
|
||||
@@ -1592,14 +1670,18 @@ class Host(utils.EventEmitter):
|
||||
|
||||
self.emit('remote_name', event.bd_addr, utf8_name)
|
||||
|
||||
def on_hci_remote_host_supported_features_notification_event(self, event):
|
||||
def on_hci_remote_host_supported_features_notification_event(
|
||||
self, event: hci.HCI_Remote_Host_Supported_Features_Notification_Event
|
||||
):
|
||||
self.emit(
|
||||
'remote_host_supported_features',
|
||||
event.bd_addr,
|
||||
event.host_supported_features,
|
||||
)
|
||||
|
||||
def on_hci_le_read_remote_features_complete_event(self, event):
|
||||
def on_hci_le_read_remote_features_complete_event(
|
||||
self, event: hci.HCI_LE_Read_Remote_Features_Complete_Event
|
||||
):
|
||||
if event.status != hci.HCI_SUCCESS:
|
||||
self.emit(
|
||||
'le_remote_features_failure', event.connection_handle, event.status
|
||||
@@ -1611,22 +1693,34 @@ class Host(utils.EventEmitter):
|
||||
int.from_bytes(event.le_features, 'little'),
|
||||
)
|
||||
|
||||
def on_hci_le_cs_read_remote_supported_capabilities_complete_event(self, event):
|
||||
def on_hci_le_cs_read_remote_supported_capabilities_complete_event(
|
||||
self, event: hci.HCI_LE_CS_Read_Remote_Supported_Capabilities_Complete_Event
|
||||
):
|
||||
self.emit('cs_remote_supported_capabilities', event)
|
||||
|
||||
def on_hci_le_cs_security_enable_complete_event(self, event):
|
||||
def on_hci_le_cs_security_enable_complete_event(
|
||||
self, event: hci.HCI_LE_CS_Security_Enable_Complete_Event
|
||||
):
|
||||
self.emit('cs_security', event)
|
||||
|
||||
def on_hci_le_cs_config_complete_event(self, event):
|
||||
def on_hci_le_cs_config_complete_event(
|
||||
self, event: hci.HCI_LE_CS_Config_Complete_Event
|
||||
):
|
||||
self.emit('cs_config', event)
|
||||
|
||||
def on_hci_le_cs_procedure_enable_complete_event(self, event):
|
||||
def on_hci_le_cs_procedure_enable_complete_event(
|
||||
self, event: hci.HCI_LE_CS_Procedure_Enable_Complete_Event
|
||||
):
|
||||
self.emit('cs_procedure', event)
|
||||
|
||||
def on_hci_le_cs_subevent_result_event(self, event):
|
||||
def on_hci_le_cs_subevent_result_event(
|
||||
self, event: hci.HCI_LE_CS_Subevent_Result_Event
|
||||
):
|
||||
self.emit('cs_subevent_result', event)
|
||||
|
||||
def on_hci_le_cs_subevent_result_continue_event(self, event):
|
||||
def on_hci_le_cs_subevent_result_continue_event(
|
||||
self, event: hci.HCI_LE_CS_Subevent_Result_Continue_Event
|
||||
):
|
||||
self.emit('cs_subevent_result_continue', event)
|
||||
|
||||
def on_hci_le_subrate_change_event(self, event: hci.HCI_LE_Subrate_Change_Event):
|
||||
@@ -1639,5 +1733,5 @@ class Host(utils.EventEmitter):
|
||||
event.supervision_timeout,
|
||||
)
|
||||
|
||||
def on_hci_vendor_event(self, event):
|
||||
def on_hci_vendor_event(self, event: hci.HCI_Vendor_Event):
|
||||
self.emit('vendor_event', event)
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Optional, Union
|
||||
@@ -272,7 +271,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
def on_connection(connection: Connection) -> None:
|
||||
@connection.on(connection.EVENT_DISCONNECTION)
|
||||
def on_disconnection(_reason) -> None:
|
||||
self.currently_connected_clients.remove(connection)
|
||||
self.currently_connected_clients.discard(connection)
|
||||
|
||||
@connection.on(connection.EVENT_PAIRING)
|
||||
def on_pairing(*_: Any) -> None:
|
||||
@@ -373,8 +372,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
self.preset_records[key]
|
||||
for key in sorted(self.preset_records.keys())
|
||||
if self.preset_records[key].index >= start_index
|
||||
]
|
||||
del presets[num_presets:]
|
||||
][:num_presets]
|
||||
if len(presets) == 0:
|
||||
raise att.ATT_Error(att.ErrorCode.OUT_OF_RANGE)
|
||||
|
||||
@@ -383,7 +381,10 @@ class HearingAccessService(gatt.TemplateService):
|
||||
async def _read_preset_response(
|
||||
self, connection: Connection, presets: list[PresetRecord]
|
||||
):
|
||||
# If the ATT bearer is terminated before all notifications or indications are sent, then the server shall consider the Read Presets Request operation aborted and shall not either continue or restart the operation when the client reconnects.
|
||||
# If the ATT bearer is terminated before all notifications or indications are
|
||||
# sent, then the server shall consider the Read Presets Request operation
|
||||
# aborted and shall not either continue or restart the operation when the client
|
||||
# reconnects.
|
||||
try:
|
||||
for i, preset in enumerate(presets):
|
||||
await connection.device.indicate_subscriber(
|
||||
@@ -404,7 +405,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
|
||||
async def generic_update(self, op: PresetChangedOperation) -> None:
|
||||
'''Server API to perform a generic update. It is the responsibility of the caller to modify the preset_records to match the PresetChangedOperation being sent'''
|
||||
await self._notifyPresetOperations(op)
|
||||
await self._notify_preset_operations(op)
|
||||
|
||||
async def delete_preset(self, index: int) -> None:
|
||||
'''Server API to delete a preset. It should not be the current active preset'''
|
||||
@@ -413,14 +414,14 @@ class HearingAccessService(gatt.TemplateService):
|
||||
raise InvalidStateError('Cannot delete active preset')
|
||||
|
||||
del self.preset_records[index]
|
||||
await self._notifyPresetOperations(PresetChangedOperationDeleted(index))
|
||||
await self._notify_preset_operations(PresetChangedOperationDeleted(index))
|
||||
|
||||
async def available_preset(self, index: int) -> None:
|
||||
'''Server API to make a preset available'''
|
||||
|
||||
preset = self.preset_records[index]
|
||||
preset.properties.is_available = PresetRecord.Property.IsAvailable.IS_AVAILABLE
|
||||
await self._notifyPresetOperations(PresetChangedOperationAvailable(index))
|
||||
await self._notify_preset_operations(PresetChangedOperationAvailable(index))
|
||||
|
||||
async def unavailable_preset(self, index: int) -> None:
|
||||
'''Server API to make a preset unavailable. It should not be the current active preset'''
|
||||
@@ -432,7 +433,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
preset.properties.is_available = (
|
||||
PresetRecord.Property.IsAvailable.IS_UNAVAILABLE
|
||||
)
|
||||
await self._notifyPresetOperations(PresetChangedOperationUnavailable(index))
|
||||
await self._notify_preset_operations(PresetChangedOperationUnavailable(index))
|
||||
|
||||
async def _preset_changed_operation(self, connection: Connection) -> None:
|
||||
'''Send all PresetChangedOperation saved for a given connection'''
|
||||
@@ -447,8 +448,10 @@ class HearingAccessService(gatt.TemplateService):
|
||||
return op.additional_parameters
|
||||
|
||||
op_list.sort(key=get_op_index)
|
||||
# If the ATT bearer is terminated before all notifications or indications are sent, then the server shall consider the Preset Changed operation aborted and shall continue the operation when the client reconnects.
|
||||
while len(op_list) > 0:
|
||||
# If the ATT bearer is terminated before all notifications or indications are
|
||||
# sent, then the server shall consider the Preset Changed operation aborted and
|
||||
# shall continue the operation when the client reconnects.
|
||||
while op_list:
|
||||
try:
|
||||
await connection.device.indicate_subscriber(
|
||||
connection,
|
||||
@@ -460,14 +463,15 @@ class HearingAccessService(gatt.TemplateService):
|
||||
except TimeoutError:
|
||||
break
|
||||
|
||||
async def _notifyPresetOperations(self, op: PresetChangedOperation) -> None:
|
||||
for historyList in self.preset_changed_operations_history_per_device.values():
|
||||
historyList.append(op)
|
||||
async def _notify_preset_operations(self, op: PresetChangedOperation) -> None:
|
||||
for history_list in self.preset_changed_operations_history_per_device.values():
|
||||
history_list.append(op)
|
||||
|
||||
for connection in self.currently_connected_clients:
|
||||
await self._preset_changed_operation(connection)
|
||||
|
||||
async def _on_write_preset_name(self, connection: Connection, value: bytes):
|
||||
del connection # Unused
|
||||
|
||||
if self.read_presets_request_in_progress:
|
||||
raise att.ATT_Error(att.ErrorCode.PROCEDURE_ALREADY_IN_PROGRESS)
|
||||
@@ -532,48 +536,51 @@ class HearingAccessService(gatt.TemplateService):
|
||||
self.active_preset_index = index
|
||||
await self.notify_active_preset()
|
||||
|
||||
async def _on_set_active_preset(self, _: Connection, value: bytes):
|
||||
async def _on_set_active_preset(self, connection: Connection, value: bytes):
|
||||
del connection # Unused
|
||||
await self.set_active_preset(value)
|
||||
|
||||
async def set_next_or_previous_preset(self, is_previous):
|
||||
async def set_next_or_previous_preset(self, is_previous: bool) -> None:
|
||||
'''Set the next or the previous preset as active'''
|
||||
|
||||
if self.active_preset_index == 0x00:
|
||||
raise att.ATT_Error(ErrorCode.PRESET_OPERATION_NOT_POSSIBLE)
|
||||
|
||||
first_preset: Optional[PresetRecord] = None # To loop to first preset
|
||||
next_preset: Optional[PresetRecord] = None
|
||||
for index, record in sorted(self.preset_records.items(), reverse=is_previous):
|
||||
if not record.is_available():
|
||||
continue
|
||||
if first_preset == None:
|
||||
first_preset = record
|
||||
if is_previous:
|
||||
if index >= self.active_preset_index:
|
||||
continue
|
||||
elif index <= self.active_preset_index:
|
||||
continue
|
||||
next_preset = record
|
||||
break
|
||||
presets = sorted(
|
||||
[
|
||||
record
|
||||
for record in self.preset_records.values()
|
||||
if record.is_available()
|
||||
],
|
||||
key=lambda record: record.index,
|
||||
)
|
||||
current_preset = self.preset_records[self.active_preset_index]
|
||||
current_preset_pos = presets.index(current_preset)
|
||||
if is_previous:
|
||||
new_preset = presets[(current_preset_pos - 1) % len(presets)]
|
||||
else:
|
||||
new_preset = presets[(current_preset_pos + 1) % len(presets)]
|
||||
|
||||
if not first_preset: # If no other preset are available
|
||||
if current_preset == new_preset: # If no other preset are available
|
||||
raise att.ATT_Error(ErrorCode.PRESET_OPERATION_NOT_POSSIBLE)
|
||||
|
||||
if next_preset:
|
||||
self.active_preset_index = next_preset.index
|
||||
else:
|
||||
self.active_preset_index = first_preset.index
|
||||
self.active_preset_index = new_preset.index
|
||||
await self.notify_active_preset()
|
||||
|
||||
async def _on_set_next_preset(self, _: Connection, __value__: bytes) -> None:
|
||||
async def _on_set_next_preset(self, connection: Connection, value: bytes) -> None:
|
||||
del connection, value # Unused.
|
||||
await self.set_next_or_previous_preset(False)
|
||||
|
||||
async def _on_set_previous_preset(self, _: Connection, __value__: bytes) -> None:
|
||||
async def _on_set_previous_preset(
|
||||
self, connection: Connection, value: bytes
|
||||
) -> None:
|
||||
del connection, value # Unused.
|
||||
await self.set_next_or_previous_preset(True)
|
||||
|
||||
async def _on_set_active_preset_synchronized_locally(
|
||||
self, _: Connection, value: bytes
|
||||
self, connection: Connection, value: bytes
|
||||
):
|
||||
del connection # Unused.
|
||||
if (
|
||||
self.server_features.preset_synchronization_support
|
||||
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
|
||||
@@ -584,8 +591,9 @@ class HearingAccessService(gatt.TemplateService):
|
||||
await self.other_server_in_binaural_set.set_active_preset(value)
|
||||
|
||||
async def _on_set_next_preset_synchronized_locally(
|
||||
self, _: Connection, __value__: bytes
|
||||
self, connection: Connection, value: bytes
|
||||
):
|
||||
del connection, value # Unused.
|
||||
if (
|
||||
self.server_features.preset_synchronization_support
|
||||
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
|
||||
@@ -596,8 +604,9 @@ class HearingAccessService(gatt.TemplateService):
|
||||
await self.other_server_in_binaural_set.set_next_or_previous_preset(False)
|
||||
|
||||
async def _on_set_previous_preset_synchronized_locally(
|
||||
self, _: Connection, __value__: bytes
|
||||
self, connection: Connection, value: bytes
|
||||
):
|
||||
del connection, value # Unused.
|
||||
if (
|
||||
self.server_features.preset_synchronization_support
|
||||
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
|
||||
@@ -615,11 +624,13 @@ class HearingAccessServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
SERVICE_CLASS = HearingAccessService
|
||||
|
||||
hearing_aid_preset_control_point: gatt_client.CharacteristicProxy
|
||||
preset_control_point_indications: asyncio.Queue
|
||||
active_preset_index_notification: asyncio.Queue
|
||||
preset_control_point_indications: asyncio.Queue[bytes]
|
||||
active_preset_index_notification: asyncio.Queue[bytes]
|
||||
|
||||
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
||||
self.service_proxy = service_proxy
|
||||
self.preset_control_point_indications = asyncio.Queue()
|
||||
self.active_preset_index_notification = asyncio.Queue()
|
||||
|
||||
self.server_features = gatt_adapters.PackedCharacteristicProxyAdapter(
|
||||
service_proxy.get_characteristics_by_uuid(
|
||||
@@ -641,20 +652,12 @@ class HearingAccessServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
'B',
|
||||
)
|
||||
|
||||
async def setup_subscription(self):
|
||||
self.preset_control_point_indications = asyncio.Queue()
|
||||
self.active_preset_index_notification = asyncio.Queue()
|
||||
|
||||
def on_active_preset_index_notification(data: bytes):
|
||||
self.active_preset_index_notification.put_nowait(data)
|
||||
|
||||
def on_preset_control_point_indication(data: bytes):
|
||||
self.preset_control_point_indications.put_nowait(data)
|
||||
|
||||
async def setup_subscription(self) -> None:
|
||||
await self.hearing_aid_preset_control_point.subscribe(
|
||||
functools.partial(on_preset_control_point_indication), prefer_notify=False
|
||||
self.preset_control_point_indications.put_nowait,
|
||||
prefer_notify=False,
|
||||
)
|
||||
|
||||
await self.active_preset_index.subscribe(
|
||||
functools.partial(on_active_preset_index_notification)
|
||||
self.active_preset_index_notification.put_nowait
|
||||
)
|
||||
|
||||
@@ -12,13 +12,12 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from contextlib import asynccontextmanager
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from bumble import utils
|
||||
@@ -85,12 +84,14 @@ async def open_transport(name: str) -> Transport:
|
||||
scheme, *tail = name.split(':', 1)
|
||||
spec = tail[0] if tail else None
|
||||
metadata = None
|
||||
if spec:
|
||||
# Metadata may precede the spec
|
||||
if spec.startswith('['):
|
||||
metadata_str, *tail = spec[1:].split(']')
|
||||
spec = tail[0] if tail else None
|
||||
metadata = dict([entry.split('=') for entry in metadata_str.split(',')])
|
||||
if spec and (m := re.search(r'\[(\w+=\w+(?:,\w+=\w+)*,?)\]', spec)):
|
||||
metadata_str = m.group(1)
|
||||
if m.start() == 0:
|
||||
# <metadata><spec>
|
||||
spec = spec[m.end() :]
|
||||
else:
|
||||
spec = spec[: m.start()]
|
||||
metadata = dict([entry.split('=') for entry in metadata_str.split(',')])
|
||||
|
||||
transport = await _open_transport(scheme, spec)
|
||||
if metadata:
|
||||
|
||||
@@ -131,7 +131,11 @@ def publish_grpc_port(grpc_port: int, instance_number: int) -> bool:
|
||||
|
||||
def cleanup():
|
||||
logger.debug("removing .ini file")
|
||||
ini_file.unlink()
|
||||
try:
|
||||
ini_file.unlink()
|
||||
except OSError as error:
|
||||
# Don't log at exception level, since this may happen normally.
|
||||
logger.debug(f'failed to remove .ini file ({error})')
|
||||
|
||||
atexit.register(cleanup)
|
||||
return True
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import serial_asyncio
|
||||
|
||||
@@ -28,25 +29,56 @@ from bumble.transport.common import StreamPacketSink, StreamPacketSource, Transp
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
DEFAULT_POST_OPEN_DELAY = 0.5 # in seconds
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes and Functions
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class SerialPacketSource(StreamPacketSource):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._ready = asyncio.Event()
|
||||
|
||||
async def wait_until_ready(self) -> None:
|
||||
await self._ready.wait()
|
||||
|
||||
def connection_made(self, transport: asyncio.BaseTransport) -> None:
|
||||
logger.debug('connection made')
|
||||
self._ready.set()
|
||||
|
||||
def connection_lost(self, exc: Optional[Exception]) -> None:
|
||||
logger.debug('connection lost')
|
||||
self.on_transport_lost()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_serial_transport(spec: str) -> Transport:
|
||||
'''
|
||||
Open a serial port transport.
|
||||
The parameter string has this syntax:
|
||||
<device-path>[,<speed>][,rtscts][,dsrdtr]
|
||||
<device-path>[,<speed>][,rtscts][,dsrdtr][,delay]
|
||||
When <speed> is omitted, the default value of 1000000 is used
|
||||
When "rtscts" is specified, RTS/CTS hardware flow control is enabled
|
||||
When "dsrdtr" is specified, DSR/DTR hardware flow control is enabled
|
||||
When "delay" is specified, a short delay is added after opening the port
|
||||
|
||||
Examples:
|
||||
/dev/tty.usbmodem0006839912172
|
||||
/dev/tty.usbmodem0006839912172,1000000
|
||||
/dev/tty.usbmodem0006839912172,rtscts
|
||||
/dev/tty.usbmodem0006839912172,rtscts,delay
|
||||
'''
|
||||
|
||||
speed = 1000000
|
||||
rtscts = False
|
||||
dsrdtr = False
|
||||
delay = 0.0
|
||||
if ',' in spec:
|
||||
parts = spec.split(',')
|
||||
device = parts[0]
|
||||
@@ -55,13 +87,16 @@ async def open_serial_transport(spec: str) -> Transport:
|
||||
rtscts = True
|
||||
elif part == 'dsrdtr':
|
||||
dsrdtr = True
|
||||
elif part == 'delay':
|
||||
delay = DEFAULT_POST_OPEN_DELAY
|
||||
elif part.isnumeric():
|
||||
speed = int(part)
|
||||
else:
|
||||
device = spec
|
||||
|
||||
serial_transport, packet_source = await serial_asyncio.create_serial_connection(
|
||||
asyncio.get_running_loop(),
|
||||
StreamPacketSource,
|
||||
SerialPacketSource,
|
||||
device,
|
||||
baudrate=speed,
|
||||
rtscts=rtscts,
|
||||
@@ -69,4 +104,23 @@ async def open_serial_transport(spec: str) -> Transport:
|
||||
)
|
||||
packet_sink = StreamPacketSink(serial_transport)
|
||||
|
||||
logger.debug('waiting for the port to be ready')
|
||||
await packet_source.wait_until_ready()
|
||||
logger.debug('port is ready')
|
||||
|
||||
# Try to assert DTR
|
||||
assert serial_transport.serial is not None
|
||||
try:
|
||||
serial_transport.serial.dtr = True
|
||||
logger.debug(
|
||||
f"DSR={serial_transport.serial.dsr}, DTR={serial_transport.serial.dtr}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f'could not assert DTR: {e}')
|
||||
|
||||
# Wait a bit after opening the port, if requested
|
||||
if delay > 0.0:
|
||||
logger.debug(f'waiting {delay} seconds after opening the port')
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
return Transport(packet_source, packet_sink)
|
||||
|
||||
@@ -4,9 +4,18 @@ SERIAL TRANSPORT
|
||||
The serial transport implements sending/receiving HCI packets over a UART (a.k.a serial port).
|
||||
|
||||
## Moniker
|
||||
The moniker syntax for a serial transport is: `serial:<device-path>[,<speed>]`
|
||||
When `<speed>` is omitted, the default value of 1000000 is used
|
||||
The moniker syntax for a serial transport is:
|
||||
`<device-path>[,<speed>][,rtscts][,dsrdtr][,delay]`
|
||||
|
||||
When `<speed>` is omitted, the default value of 1000000 is used.
|
||||
When `rtscts` is specified, RTS/CTS hardware flow control is enabled.
|
||||
When `dsrdtr` is specified, DSR/DTR hardware flow control is enabled.
|
||||
When `delay` is specified, a short delay is added after opening the port.
|
||||
|
||||
!!! example
|
||||
`serial:/dev/tty.usbmodem0006839912172,1000000`
|
||||
Opens the serial port `/dev/tty.usbmodem0006839912172` at `1000000`bps
|
||||
```
|
||||
/dev/tty.usbmodem0006839912172
|
||||
/dev/tty.usbmodem0006839912172,1000000
|
||||
/dev/tty.usbmodem0006839912172,rtscts
|
||||
/dev/tty.usbmodem0006839912172,rtscts,delay
|
||||
```
|
||||
196
rust/Cargo.lock
generated
196
rust/Cargo.lock
generated
@@ -61,7 +61,7 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -71,7 +71,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -249,7 +249,7 @@ dependencies = [
|
||||
"atty",
|
||||
"bitflags 1.3.2",
|
||||
"clap_lex 0.2.4",
|
||||
"indexmap",
|
||||
"indexmap 1.9.3",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
@@ -451,7 +451,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -483,24 +483,19 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.3"
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
|
||||
dependencies = [
|
||||
"errno-dragonfly",
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno-dragonfly"
|
||||
version = "0.1.2"
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -683,9 +678,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.21"
|
||||
version = "0.3.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833"
|
||||
checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
@@ -693,7 +688,7 @@ dependencies = [
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http",
|
||||
"indexmap",
|
||||
"indexmap 2.11.3",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -706,6 +701,12 @@ version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
@@ -827,7 +828,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
"hashbrown 0.12.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92119844f513ffa41556430369ab02c295a3578af21cf945caa3e9e0c2481ac3"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -856,7 +867,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.2",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -891,9 +902,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.147"
|
||||
version = "0.2.175"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
|
||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
|
||||
[[package]]
|
||||
name = "libusb1-sys"
|
||||
@@ -920,9 +931,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.5"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
|
||||
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
@@ -996,13 +1007,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.8"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1073,9 +1084,9 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.60"
|
||||
version = "0.10.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800"
|
||||
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"cfg-if",
|
||||
@@ -1105,9 +1116,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.96"
|
||||
version = "0.9.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f"
|
||||
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -1153,7 +1164,7 @@ dependencies = [
|
||||
"libc",
|
||||
"redox_syscall 0.3.5",
|
||||
"smallvec",
|
||||
"windows-targets",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1542,15 +1553,15 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.10"
|
||||
version = "0.38.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed6248e1caa625eb708e266e06159f135e8c26f2bb7ceb72dc4b2766d0340964"
|
||||
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1580,7 +1591,7 @@ version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1702,12 +1713,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.3"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877"
|
||||
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1773,7 +1784,7 @@ dependencies = [
|
||||
"fastrand",
|
||||
"redox_syscall 0.3.5",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1828,9 +1839,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.32.0"
|
||||
version = "1.38.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
|
||||
checksum = "68722da18b0fc4a05fdc1120b302b82051265792a1e1b399086e9b204b10ad3d"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@@ -1840,16 +1851,16 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.5.3",
|
||||
"socket2 0.5.10",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.1.0"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
|
||||
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2130,7 +2141,16 @@ version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2139,13 +2159,29 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
"windows_aarch64_gnullvm 0.48.5",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm 0.48.5",
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2154,42 +2190,90 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
@@ -2197,5 +2281,5 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
@@ -10,7 +10,7 @@ documentation = "https://docs.rs/crate/bumble"
|
||||
authors = ["Marshall Pierce <marshallpierce@google.com>"]
|
||||
keywords = ["bluetooth", "ble"]
|
||||
categories = ["api-bindings", "network-programming"]
|
||||
rust-version = "1.76.0"
|
||||
rust-version = "1.80.0"
|
||||
|
||||
# https://github.com/frewsxcv/cargo-all-features#options
|
||||
[package.metadata.cargo-all-features]
|
||||
@@ -22,7 +22,7 @@ always_include_features = ["anyhow", "pyo3-asyncio-attributes", "dev-tools", "bu
|
||||
[dependencies]
|
||||
pyo3 = { version = "0.18.3", features = ["macros"] }
|
||||
pyo3-asyncio = { version = "0.18.0", features = ["tokio-runtime"] }
|
||||
tokio = { version = "1.28.2", features = ["macros", "signal"] }
|
||||
tokio = { version = "1.38.2", features = ["macros", "signal"] }
|
||||
nom = "7.1.3"
|
||||
strum = "0.25.0"
|
||||
strum_macros = "0.25.0"
|
||||
@@ -50,7 +50,7 @@ reqwest = { version = "0.11.20", features = ["blocking"], optional = true }
|
||||
rusb = { version = "0.9.2", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.28.2", features = ["full"] }
|
||||
tokio = { version = "1.38.2", features = ["full"] }
|
||||
tempfile = "3.6.0"
|
||||
nix = "0.26.2"
|
||||
anyhow = "1.0.71"
|
||||
|
||||
@@ -18,14 +18,12 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
from typing import Awaitable
|
||||
|
||||
import pytest
|
||||
|
||||
from bumble.a2dp import (
|
||||
AacMediaCodecInformation,
|
||||
OpusMediaCodecInformation,
|
||||
SbcMediaCodecInformation,
|
||||
)
|
||||
from bumble import a2dp
|
||||
from bumble.avdtp import (
|
||||
A2DP_SBC_CODEC_TYPE,
|
||||
AVDTP_AUDIO_MEDIA_TYPE,
|
||||
@@ -82,6 +80,24 @@ class TwoDevices:
|
||||
self.paired[which] = keys
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Data:
|
||||
pointer: int = 0
|
||||
data: bytes
|
||||
|
||||
def __init__(self, data: bytes):
|
||||
self.data = data
|
||||
|
||||
async def read(self, length: int) -> Awaitable[bytes]:
|
||||
def generate_read():
|
||||
end = min(self.pointer + length, len(self.data))
|
||||
chunk = self.data[self.pointer : end]
|
||||
self.pointer = end
|
||||
return chunk
|
||||
|
||||
return generate_read()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_self_connection():
|
||||
@@ -122,12 +138,12 @@ def source_codec_capabilities():
|
||||
return MediaCodecCapabilities(
|
||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||
media_codec_information=SbcMediaCodecInformation(
|
||||
sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_44100,
|
||||
channel_mode=SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||
block_length=SbcMediaCodecInformation.BlockLength.BL_16,
|
||||
subbands=SbcMediaCodecInformation.Subbands.S_8,
|
||||
allocation_method=SbcMediaCodecInformation.AllocationMethod.LOUDNESS,
|
||||
media_codec_information=a2dp.SbcMediaCodecInformation(
|
||||
sampling_frequency=a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_44100,
|
||||
channel_mode=a2dp.SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||
block_length=a2dp.SbcMediaCodecInformation.BlockLength.BL_16,
|
||||
subbands=a2dp.SbcMediaCodecInformation.Subbands.S_8,
|
||||
allocation_method=a2dp.SbcMediaCodecInformation.AllocationMethod.LOUDNESS,
|
||||
minimum_bitpool_value=2,
|
||||
maximum_bitpool_value=53,
|
||||
),
|
||||
@@ -139,23 +155,23 @@ def sink_codec_capabilities():
|
||||
return MediaCodecCapabilities(
|
||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||
media_codec_information=SbcMediaCodecInformation(
|
||||
sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_48000
|
||||
| SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||
| SbcMediaCodecInformation.SamplingFrequency.SF_32000
|
||||
| SbcMediaCodecInformation.SamplingFrequency.SF_16000,
|
||||
channel_mode=SbcMediaCodecInformation.ChannelMode.MONO
|
||||
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||
| SbcMediaCodecInformation.ChannelMode.STEREO
|
||||
| SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||
block_length=SbcMediaCodecInformation.BlockLength.BL_4
|
||||
| SbcMediaCodecInformation.BlockLength.BL_8
|
||||
| SbcMediaCodecInformation.BlockLength.BL_12
|
||||
| SbcMediaCodecInformation.BlockLength.BL_16,
|
||||
subbands=SbcMediaCodecInformation.Subbands.S_4
|
||||
| SbcMediaCodecInformation.Subbands.S_8,
|
||||
allocation_method=SbcMediaCodecInformation.AllocationMethod.LOUDNESS
|
||||
| SbcMediaCodecInformation.AllocationMethod.SNR,
|
||||
media_codec_information=a2dp.SbcMediaCodecInformation(
|
||||
sampling_frequency=a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_48000
|
||||
| a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||
| a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_32000
|
||||
| a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_16000,
|
||||
channel_mode=a2dp.SbcMediaCodecInformation.ChannelMode.MONO
|
||||
| a2dp.SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||
| a2dp.SbcMediaCodecInformation.ChannelMode.STEREO
|
||||
| a2dp.SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||
block_length=a2dp.SbcMediaCodecInformation.BlockLength.BL_4
|
||||
| a2dp.SbcMediaCodecInformation.BlockLength.BL_8
|
||||
| a2dp.SbcMediaCodecInformation.BlockLength.BL_12
|
||||
| a2dp.SbcMediaCodecInformation.BlockLength.BL_16,
|
||||
subbands=a2dp.SbcMediaCodecInformation.Subbands.S_4
|
||||
| a2dp.SbcMediaCodecInformation.Subbands.S_8,
|
||||
allocation_method=a2dp.SbcMediaCodecInformation.AllocationMethod.LOUDNESS
|
||||
| a2dp.SbcMediaCodecInformation.AllocationMethod.SNR,
|
||||
minimum_bitpool_value=2,
|
||||
maximum_bitpool_value=53,
|
||||
),
|
||||
@@ -274,52 +290,54 @@ async def test_source_sink_1():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_sbc_codec_specific_information():
|
||||
sbc_info = SbcMediaCodecInformation.from_bytes(bytes.fromhex("3fff0235"))
|
||||
sbc_info = a2dp.SbcMediaCodecInformation.from_bytes(bytes.fromhex("3fff0235"))
|
||||
assert (
|
||||
sbc_info.sampling_frequency
|
||||
== SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||
| SbcMediaCodecInformation.SamplingFrequency.SF_48000
|
||||
== a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||
| a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_48000
|
||||
)
|
||||
assert (
|
||||
sbc_info.channel_mode
|
||||
== SbcMediaCodecInformation.ChannelMode.MONO
|
||||
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||
| SbcMediaCodecInformation.ChannelMode.STEREO
|
||||
| SbcMediaCodecInformation.ChannelMode.JOINT_STEREO
|
||||
== a2dp.SbcMediaCodecInformation.ChannelMode.MONO
|
||||
| a2dp.SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||
| a2dp.SbcMediaCodecInformation.ChannelMode.STEREO
|
||||
| a2dp.SbcMediaCodecInformation.ChannelMode.JOINT_STEREO
|
||||
)
|
||||
assert (
|
||||
sbc_info.block_length
|
||||
== SbcMediaCodecInformation.BlockLength.BL_4
|
||||
| SbcMediaCodecInformation.BlockLength.BL_8
|
||||
| SbcMediaCodecInformation.BlockLength.BL_12
|
||||
| SbcMediaCodecInformation.BlockLength.BL_16
|
||||
== a2dp.SbcMediaCodecInformation.BlockLength.BL_4
|
||||
| a2dp.SbcMediaCodecInformation.BlockLength.BL_8
|
||||
| a2dp.SbcMediaCodecInformation.BlockLength.BL_12
|
||||
| a2dp.SbcMediaCodecInformation.BlockLength.BL_16
|
||||
)
|
||||
assert (
|
||||
sbc_info.subbands
|
||||
== SbcMediaCodecInformation.Subbands.S_4 | SbcMediaCodecInformation.Subbands.S_8
|
||||
== a2dp.SbcMediaCodecInformation.Subbands.S_4
|
||||
| a2dp.SbcMediaCodecInformation.Subbands.S_8
|
||||
)
|
||||
assert (
|
||||
sbc_info.allocation_method
|
||||
== SbcMediaCodecInformation.AllocationMethod.SNR
|
||||
| SbcMediaCodecInformation.AllocationMethod.LOUDNESS
|
||||
== a2dp.SbcMediaCodecInformation.AllocationMethod.SNR
|
||||
| a2dp.SbcMediaCodecInformation.AllocationMethod.LOUDNESS
|
||||
)
|
||||
assert sbc_info.minimum_bitpool_value == 2
|
||||
assert sbc_info.maximum_bitpool_value == 53
|
||||
|
||||
sbc_info2 = SbcMediaCodecInformation(
|
||||
SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||
| SbcMediaCodecInformation.SamplingFrequency.SF_48000,
|
||||
SbcMediaCodecInformation.ChannelMode.MONO
|
||||
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||
| SbcMediaCodecInformation.ChannelMode.STEREO
|
||||
| SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||
SbcMediaCodecInformation.BlockLength.BL_4
|
||||
| SbcMediaCodecInformation.BlockLength.BL_8
|
||||
| SbcMediaCodecInformation.BlockLength.BL_12
|
||||
| SbcMediaCodecInformation.BlockLength.BL_16,
|
||||
SbcMediaCodecInformation.Subbands.S_4 | SbcMediaCodecInformation.Subbands.S_8,
|
||||
SbcMediaCodecInformation.AllocationMethod.SNR
|
||||
| SbcMediaCodecInformation.AllocationMethod.LOUDNESS,
|
||||
sbc_info2 = a2dp.SbcMediaCodecInformation(
|
||||
a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||
| a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_48000,
|
||||
a2dp.SbcMediaCodecInformation.ChannelMode.MONO
|
||||
| a2dp.SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||
| a2dp.SbcMediaCodecInformation.ChannelMode.STEREO
|
||||
| a2dp.SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||
a2dp.SbcMediaCodecInformation.BlockLength.BL_4
|
||||
| a2dp.SbcMediaCodecInformation.BlockLength.BL_8
|
||||
| a2dp.SbcMediaCodecInformation.BlockLength.BL_12
|
||||
| a2dp.SbcMediaCodecInformation.BlockLength.BL_16,
|
||||
a2dp.SbcMediaCodecInformation.Subbands.S_4
|
||||
| a2dp.SbcMediaCodecInformation.Subbands.S_8,
|
||||
a2dp.SbcMediaCodecInformation.AllocationMethod.SNR
|
||||
| a2dp.SbcMediaCodecInformation.AllocationMethod.LOUDNESS,
|
||||
2,
|
||||
53,
|
||||
)
|
||||
@@ -329,36 +347,36 @@ def test_sbc_codec_specific_information():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_aac_codec_specific_information():
|
||||
aac_info = AacMediaCodecInformation.from_bytes(bytes.fromhex("f0018c83e800"))
|
||||
aac_info = a2dp.AacMediaCodecInformation.from_bytes(bytes.fromhex("f0018c83e800"))
|
||||
assert (
|
||||
aac_info.object_type
|
||||
== AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC
|
||||
| AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LC
|
||||
| AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LTP
|
||||
| AacMediaCodecInformation.ObjectType.MPEG_4_AAC_SCALABLE
|
||||
== a2dp.AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC
|
||||
| a2dp.AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LC
|
||||
| a2dp.AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LTP
|
||||
| a2dp.AacMediaCodecInformation.ObjectType.MPEG_4_AAC_SCALABLE
|
||||
)
|
||||
assert (
|
||||
aac_info.sampling_frequency
|
||||
== AacMediaCodecInformation.SamplingFrequency.SF_44100
|
||||
| AacMediaCodecInformation.SamplingFrequency.SF_48000
|
||||
== a2dp.AacMediaCodecInformation.SamplingFrequency.SF_44100
|
||||
| a2dp.AacMediaCodecInformation.SamplingFrequency.SF_48000
|
||||
)
|
||||
assert (
|
||||
aac_info.channels
|
||||
== AacMediaCodecInformation.Channels.MONO
|
||||
| AacMediaCodecInformation.Channels.STEREO
|
||||
== a2dp.AacMediaCodecInformation.Channels.MONO
|
||||
| a2dp.AacMediaCodecInformation.Channels.STEREO
|
||||
)
|
||||
assert aac_info.vbr == 1
|
||||
assert aac_info.bitrate == 256000
|
||||
|
||||
aac_info2 = AacMediaCodecInformation(
|
||||
AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC
|
||||
| AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LC
|
||||
| AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LTP
|
||||
| AacMediaCodecInformation.ObjectType.MPEG_4_AAC_SCALABLE,
|
||||
AacMediaCodecInformation.SamplingFrequency.SF_44100
|
||||
| AacMediaCodecInformation.SamplingFrequency.SF_48000,
|
||||
AacMediaCodecInformation.Channels.MONO
|
||||
| AacMediaCodecInformation.Channels.STEREO,
|
||||
aac_info2 = a2dp.AacMediaCodecInformation(
|
||||
a2dp.AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC
|
||||
| a2dp.AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LC
|
||||
| a2dp.AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LTP
|
||||
| a2dp.AacMediaCodecInformation.ObjectType.MPEG_4_AAC_SCALABLE,
|
||||
a2dp.AacMediaCodecInformation.SamplingFrequency.SF_44100
|
||||
| a2dp.AacMediaCodecInformation.SamplingFrequency.SF_48000,
|
||||
a2dp.AacMediaCodecInformation.Channels.MONO
|
||||
| a2dp.AacMediaCodecInformation.Channels.STEREO,
|
||||
1,
|
||||
256000,
|
||||
)
|
||||
@@ -368,25 +386,159 @@ def test_aac_codec_specific_information():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_opus_codec_specific_information():
|
||||
opus_info = OpusMediaCodecInformation.from_bytes(bytes([0x92]))
|
||||
assert opus_info.vendor_id == OpusMediaCodecInformation.VENDOR_ID
|
||||
assert opus_info.codec_id == OpusMediaCodecInformation.CODEC_ID
|
||||
assert opus_info.frame_size == OpusMediaCodecInformation.FrameSize.FS_20MS
|
||||
assert opus_info.channel_mode == OpusMediaCodecInformation.ChannelMode.STEREO
|
||||
opus_info = a2dp.OpusMediaCodecInformation.from_bytes(bytes([0x92]))
|
||||
assert opus_info.vendor_id == a2dp.OpusMediaCodecInformation.VENDOR_ID
|
||||
assert opus_info.codec_id == a2dp.OpusMediaCodecInformation.CODEC_ID
|
||||
assert opus_info.frame_size == a2dp.OpusMediaCodecInformation.FrameSize.FS_20MS
|
||||
assert opus_info.channel_mode == a2dp.OpusMediaCodecInformation.ChannelMode.STEREO
|
||||
assert (
|
||||
opus_info.sampling_frequency
|
||||
== OpusMediaCodecInformation.SamplingFrequency.SF_48000
|
||||
== a2dp.OpusMediaCodecInformation.SamplingFrequency.SF_48000
|
||||
)
|
||||
|
||||
opus_info2 = OpusMediaCodecInformation(
|
||||
OpusMediaCodecInformation.ChannelMode.STEREO,
|
||||
OpusMediaCodecInformation.FrameSize.FS_20MS,
|
||||
OpusMediaCodecInformation.SamplingFrequency.SF_48000,
|
||||
opus_info2 = a2dp.OpusMediaCodecInformation(
|
||||
a2dp.OpusMediaCodecInformation.ChannelMode.STEREO,
|
||||
a2dp.OpusMediaCodecInformation.FrameSize.FS_20MS,
|
||||
a2dp.OpusMediaCodecInformation.SamplingFrequency.SF_48000,
|
||||
)
|
||||
assert opus_info2 == opus_info
|
||||
assert opus_info2.value == bytes([0x92])
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def test_sbc_parser():
|
||||
header = b'\x9c\x80\x08\x00'
|
||||
payload = b'\x00\x00\x00\x00\x00\x00'
|
||||
data = Data(header + payload)
|
||||
|
||||
parser = a2dp.SbcParser(data.read)
|
||||
async for frame in parser.frames:
|
||||
assert frame.sampling_frequency == 44100
|
||||
assert frame.block_count == 4
|
||||
assert frame.channel_mode == 0
|
||||
assert frame.allocation_method == 0
|
||||
assert frame.subband_count == 4
|
||||
assert frame.bitpool == 8
|
||||
assert frame.payload == header + payload
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def test_sbc_packet_source():
|
||||
header = b'\x9c\x80\x08\x00'
|
||||
payload = b'\x00\x00\x00\x00\x00\x00'
|
||||
data = Data((header + payload) * 2)
|
||||
|
||||
packet_source = a2dp.SbcPacketSource(data.read, 23)
|
||||
async for packet in packet_source.packets:
|
||||
assert packet.sequence_number == 0
|
||||
assert packet.timestamp == 0
|
||||
assert packet.payload == b'\x01' + header + payload
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def test_aac_parser():
|
||||
header = b'\xff\xf0\x10\x00\x01\xa0\x00'
|
||||
payload = b'\x00\x00\x00\x00\x00\x00'
|
||||
data = Data(header + payload)
|
||||
|
||||
parser = a2dp.AacParser(data.read)
|
||||
async for frame in parser.frames:
|
||||
assert frame.profile == a2dp.AacFrame.Profile.MAIN
|
||||
assert frame.sampling_frequency == 44100
|
||||
assert frame.channel_configuration == 0
|
||||
assert frame.payload == payload
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def test_aac_packet_source():
|
||||
header = b'\xff\xf0\x10\x00\x01\xa0\x00'
|
||||
payload = b'\x00\x00\x00\x00\x00\x00'
|
||||
data = Data(header + payload)
|
||||
|
||||
packet_source = a2dp.AacPacketSource(data.read, 0)
|
||||
async for packet in packet_source.packets:
|
||||
assert packet.sequence_number == 0
|
||||
assert packet.timestamp == 0
|
||||
assert packet.payload == b' \x00\x12\x00\x00\x000\x00\x00\x00\x00\x00\x00'
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def test_opus_parser():
|
||||
packed_header_data_revised = struct.pack(
|
||||
"<QIIIB",
|
||||
0, # granule_position
|
||||
2, # bitstream_serial_number
|
||||
2, # page_sequence_number
|
||||
0, # crc_checksum
|
||||
3, # page_segments
|
||||
)
|
||||
|
||||
first_page_header_revised = (
|
||||
b'OggS' # Capture pattern
|
||||
+ b'\x00' # Version
|
||||
+ b'\x02' # Header type
|
||||
+ packed_header_data_revised
|
||||
)
|
||||
|
||||
segment_table_revised = b'\x0a\x08\x0a'
|
||||
|
||||
opus_head_packet_data = b'OpusHead' + b'\x00' + b'\x00'
|
||||
opus_tags_packet_data = b'OpusTags'
|
||||
audio_data_packet = b'0123456789'
|
||||
|
||||
data = Data(
|
||||
first_page_header_revised
|
||||
+ segment_table_revised
|
||||
+ opus_head_packet_data
|
||||
+ opus_tags_packet_data
|
||||
+ audio_data_packet
|
||||
)
|
||||
|
||||
parser = a2dp.OpusParser(data.read)
|
||||
async for packet in parser.packets:
|
||||
assert packet.channel_mode == a2dp.OpusPacket.ChannelMode.STEREO
|
||||
assert packet.payload == audio_data_packet
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def test_opus_packet_source():
|
||||
packed_header_data_revised = struct.pack(
|
||||
"<QIIIB",
|
||||
0, # granule_position
|
||||
2, # bitstream_serial_number
|
||||
2, # page_sequence_number
|
||||
0, # crc_checksum
|
||||
3, # page_segments
|
||||
)
|
||||
|
||||
first_page_header_revised = (
|
||||
b'OggS' # Capture pattern
|
||||
+ b'\x00' # Version
|
||||
+ b'\x02' # Header type
|
||||
+ packed_header_data_revised
|
||||
)
|
||||
|
||||
segment_table_revised = b'\x0a\x08\x0a'
|
||||
|
||||
opus_head_packet_data = b'OpusHead' + b'\x00' + b'\x00'
|
||||
opus_tags_packet_data = b'OpusTags'
|
||||
audio_data_packet = b'0123456789'
|
||||
|
||||
data = Data(
|
||||
first_page_header_revised
|
||||
+ segment_table_revised
|
||||
+ opus_head_packet_data
|
||||
+ opus_tags_packet_data
|
||||
+ audio_data_packet
|
||||
)
|
||||
|
||||
parser = a2dp.OpusPacketSource(data.read, 0)
|
||||
async for packet in parser.packets:
|
||||
assert packet.sequence_number == 0
|
||||
assert packet.timestamp == 0
|
||||
assert packet.payload == b'\x01' + audio_data_packet
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def async_main():
|
||||
test_sbc_codec_specific_information()
|
||||
@@ -394,6 +546,12 @@ async def async_main():
|
||||
test_opus_codec_specific_information()
|
||||
await test_self_connection()
|
||||
await test_source_sink_1()
|
||||
test_sbc_parser()
|
||||
test_sbc_packet_source()
|
||||
test_aac_parser()
|
||||
test_aac_packet_source()
|
||||
test_opus_parser()
|
||||
test_opus_packet_source()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -15,67 +15,261 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
from collections.abc import Sequence
|
||||
|
||||
import pytest
|
||||
|
||||
from bumble import avc, avctp, avrcp, controller, core, device, host, link
|
||||
from bumble.transport import common
|
||||
from bumble import avc, avctp, avrcp
|
||||
|
||||
from . import test_utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class TwoDevices:
|
||||
def __init__(self):
|
||||
self.connections = [None, None]
|
||||
|
||||
addresses = ['F0:F1:F2:F3:F4:F5', 'F5:F4:F3:F2:F1:F0']
|
||||
self.link = link.LocalLink()
|
||||
self.controllers = [
|
||||
controller.Controller('C1', link=self.link, public_address=addresses[0]),
|
||||
controller.Controller('C2', link=self.link, public_address=addresses[1]),
|
||||
]
|
||||
self.devices = [
|
||||
device.Device(
|
||||
address=addresses[0],
|
||||
host=host.Host(
|
||||
self.controllers[0], common.AsyncPipeSink(self.controllers[0])
|
||||
),
|
||||
),
|
||||
device.Device(
|
||||
address=addresses[1],
|
||||
host=host.Host(
|
||||
self.controllers[1], common.AsyncPipeSink(self.controllers[1])
|
||||
),
|
||||
),
|
||||
]
|
||||
self.devices[0].classic_enabled = True
|
||||
self.devices[1].classic_enabled = True
|
||||
self.connections = [None, None]
|
||||
self.protocols = [None, None]
|
||||
|
||||
def on_connection(self, which, connection):
|
||||
self.connections[which] = connection
|
||||
|
||||
async def setup_connections(self):
|
||||
await self.devices[0].power_on()
|
||||
await self.devices[1].power_on()
|
||||
|
||||
self.connections = await asyncio.gather(
|
||||
self.devices[0].connect(
|
||||
self.devices[1].public_address, core.PhysicalTransport.BR_EDR
|
||||
),
|
||||
self.devices[1].accept(self.devices[0].public_address),
|
||||
)
|
||||
class TwoDevices(test_utils.TwoDevices):
|
||||
protocols: Sequence[avrcp.Protocol] = ()
|
||||
|
||||
async def setup_avdtp_connections(self):
|
||||
self.protocols = [avrcp.Protocol(), avrcp.Protocol()]
|
||||
self.protocols[0].listen(self.devices[1])
|
||||
await self.protocols[1].connect(self.connections[0])
|
||||
|
||||
@classmethod
|
||||
async def create_with_avdtp(cls) -> TwoDevices:
|
||||
devices = await cls.create_with_connection()
|
||||
await devices.setup_avdtp_connections()
|
||||
return devices
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"command,",
|
||||
[
|
||||
avrcp.GetPlayStatusCommand(),
|
||||
avrcp.GetCapabilitiesCommand(
|
||||
capability_id=avrcp.GetCapabilitiesCommand.CapabilityId.COMPANY_ID
|
||||
),
|
||||
avrcp.SetAbsoluteVolumeCommand(volume=5),
|
||||
avrcp.GetElementAttributesCommand(
|
||||
identifier=999,
|
||||
attribute_ids=[
|
||||
avrcp.MediaAttributeId.ALBUM_NAME,
|
||||
avrcp.MediaAttributeId.ARTIST_NAME,
|
||||
],
|
||||
),
|
||||
avrcp.RegisterNotificationCommand(
|
||||
event_id=avrcp.EventId.ADDRESSED_PLAYER_CHANGED, playback_interval=123
|
||||
),
|
||||
avrcp.SearchCommand(
|
||||
character_set_id=avrcp.CharacterSetId.UTF_8, search_string="Bumble!"
|
||||
),
|
||||
avrcp.PlayItemCommand(
|
||||
scope=avrcp.Scope.MEDIA_PLAYER_LIST, uid=0, uid_counter=1
|
||||
),
|
||||
avrcp.ListPlayerApplicationSettingAttributesCommand(),
|
||||
avrcp.ListPlayerApplicationSettingValuesCommand(
|
||||
attribute=avrcp.ApplicationSetting.AttributeId.REPEAT_MODE
|
||||
),
|
||||
avrcp.GetCurrentPlayerApplicationSettingValueCommand(
|
||||
attribute=[
|
||||
avrcp.ApplicationSetting.AttributeId.REPEAT_MODE,
|
||||
avrcp.ApplicationSetting.AttributeId.SHUFFLE_ON_OFF,
|
||||
]
|
||||
),
|
||||
avrcp.SetPlayerApplicationSettingValueCommand(
|
||||
attribute=[avrcp.ApplicationSetting.AttributeId.REPEAT_MODE],
|
||||
value=[avrcp.ApplicationSetting.RepeatModeStatus.ALL_TRACK_REPEAT],
|
||||
),
|
||||
avrcp.GetPlayerApplicationSettingAttributeTextCommand(
|
||||
attribute=[
|
||||
avrcp.ApplicationSetting.AttributeId.REPEAT_MODE,
|
||||
avrcp.ApplicationSetting.AttributeId.SHUFFLE_ON_OFF,
|
||||
]
|
||||
),
|
||||
avrcp.GetPlayerApplicationSettingValueTextCommand(
|
||||
attribute=avrcp.ApplicationSetting.AttributeId.REPEAT_MODE,
|
||||
value=[
|
||||
avrcp.ApplicationSetting.RepeatModeStatus.ALL_TRACK_REPEAT,
|
||||
avrcp.ApplicationSetting.RepeatModeStatus.GROUP_REPEAT,
|
||||
],
|
||||
),
|
||||
avrcp.InformDisplayableCharacterSetCommand(
|
||||
character_set_id=[avrcp.CharacterSetId.UTF_8]
|
||||
),
|
||||
avrcp.InformBatteryStatusOfCtCommand(
|
||||
battery_status=avrcp.InformBatteryStatusOfCtCommand.BatteryStatus.NORMAL
|
||||
),
|
||||
avrcp.SetAddressedPlayerCommand(player_id=1),
|
||||
avrcp.SetBrowsedPlayerCommand(player_id=1),
|
||||
avrcp.GetFolderItemsCommand(
|
||||
scope=avrcp.Scope.NOW_PLAYING,
|
||||
start_item=0,
|
||||
end_item=1,
|
||||
attributes=[avrcp.MediaAttributeId.ARTIST_NAME],
|
||||
),
|
||||
avrcp.ChangePathCommand(
|
||||
uid_counter=1,
|
||||
direction=avrcp.ChangePathCommand.Direction.DOWN,
|
||||
folder_uid=2,
|
||||
),
|
||||
avrcp.GetItemAttributesCommand(
|
||||
scope=avrcp.Scope.NOW_PLAYING,
|
||||
uid=0,
|
||||
uid_counter=1,
|
||||
start_item=0,
|
||||
end_item=0,
|
||||
attributes=[avrcp.MediaAttributeId.DEFAULT_COVER_ART],
|
||||
),
|
||||
avrcp.GetTotalNumberOfItemsCommand(scope=avrcp.Scope.NOW_PLAYING),
|
||||
avrcp.AddToNowPlayingCommand(
|
||||
scope=avrcp.Scope.NOW_PLAYING, uid=0, uid_counter=1
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_command(command: avrcp.Command):
|
||||
assert avrcp.Command.from_bytes(command.pdu_id, bytes(command)) == command
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"event,",
|
||||
[
|
||||
avrcp.UidsChangedEvent(uid_counter=7),
|
||||
avrcp.TrackChangedEvent(identifier=b'12356'),
|
||||
avrcp.VolumeChangedEvent(volume=9),
|
||||
avrcp.PlaybackStatusChangedEvent(play_status=avrcp.PlayStatus.PLAYING),
|
||||
avrcp.AddressedPlayerChangedEvent(
|
||||
player=avrcp.AddressedPlayerChangedEvent.Player(player_id=9, uid_counter=10)
|
||||
),
|
||||
avrcp.AvailablePlayersChangedEvent(),
|
||||
avrcp.PlaybackPositionChangedEvent(playback_position=1314),
|
||||
avrcp.NowPlayingContentChangedEvent(),
|
||||
avrcp.PlayerApplicationSettingChangedEvent(
|
||||
player_application_settings=[
|
||||
avrcp.PlayerApplicationSettingChangedEvent.Setting(
|
||||
avrcp.ApplicationSetting.AttributeId.REPEAT_MODE,
|
||||
avrcp.ApplicationSetting.RepeatModeStatus.ALL_TRACK_REPEAT,
|
||||
)
|
||||
]
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_event(event: avrcp.Event):
|
||||
assert avrcp.Event.from_bytes(bytes(event)) == event
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"response,",
|
||||
[
|
||||
avrcp.GetPlayStatusResponse(
|
||||
song_length=1010, song_position=13, play_status=avrcp.PlayStatus.PAUSED
|
||||
),
|
||||
avrcp.GetCapabilitiesResponse(
|
||||
capability_id=avrcp.GetCapabilitiesCommand.CapabilityId.EVENTS_SUPPORTED,
|
||||
capabilities=[
|
||||
avrcp.EventId.ADDRESSED_PLAYER_CHANGED,
|
||||
avrcp.EventId.BATT_STATUS_CHANGED,
|
||||
],
|
||||
),
|
||||
avrcp.RegisterNotificationResponse(
|
||||
event=avrcp.PlaybackPositionChangedEvent(playback_position=38)
|
||||
),
|
||||
avrcp.SetAbsoluteVolumeResponse(volume=99),
|
||||
avrcp.GetElementAttributesResponse(
|
||||
attributes=[
|
||||
avrcp.MediaAttribute(
|
||||
attribute_id=avrcp.MediaAttributeId.ALBUM_NAME,
|
||||
attribute_value="White Album",
|
||||
character_set_id=avrcp.CharacterSetId.UTF_8,
|
||||
)
|
||||
]
|
||||
),
|
||||
avrcp.ListPlayerApplicationSettingAttributesResponse(
|
||||
attribute=[
|
||||
avrcp.ApplicationSetting.AttributeId.REPEAT_MODE,
|
||||
avrcp.ApplicationSetting.AttributeId.SHUFFLE_ON_OFF,
|
||||
]
|
||||
),
|
||||
avrcp.ListPlayerApplicationSettingValuesResponse(
|
||||
value=[
|
||||
avrcp.ApplicationSetting.RepeatModeStatus.ALL_TRACK_REPEAT,
|
||||
avrcp.ApplicationSetting.RepeatModeStatus.GROUP_REPEAT,
|
||||
]
|
||||
),
|
||||
avrcp.GetCurrentPlayerApplicationSettingValueResponse(
|
||||
attribute=[avrcp.ApplicationSetting.AttributeId.REPEAT_MODE],
|
||||
value=[avrcp.ApplicationSetting.RepeatModeStatus.ALL_TRACK_REPEAT],
|
||||
),
|
||||
avrcp.SetPlayerApplicationSettingValueResponse(),
|
||||
avrcp.GetPlayerApplicationSettingAttributeTextResponse(
|
||||
attribute=[avrcp.ApplicationSetting.AttributeId.REPEAT_MODE],
|
||||
character_set_id=[avrcp.CharacterSetId.UTF_8],
|
||||
attribute_string=["Repeat"],
|
||||
),
|
||||
avrcp.GetPlayerApplicationSettingValueTextResponse(
|
||||
value=[avrcp.ApplicationSetting.RepeatModeStatus.ALL_TRACK_REPEAT],
|
||||
character_set_id=[avrcp.CharacterSetId.UTF_8],
|
||||
attribute_string=["All track repeat"],
|
||||
),
|
||||
avrcp.InformDisplayableCharacterSetResponse(),
|
||||
avrcp.InformBatteryStatusOfCtResponse(),
|
||||
avrcp.SetAddressedPlayerResponse(status=avrcp.StatusCode.OPERATION_COMPLETED),
|
||||
avrcp.SetBrowsedPlayerResponse(
|
||||
status=avrcp.StatusCode.OPERATION_COMPLETED,
|
||||
uid_counter=1,
|
||||
numbers_of_items=2,
|
||||
character_set_id=avrcp.CharacterSetId.UTF_8,
|
||||
folder_names=["folder1", "folder2"],
|
||||
),
|
||||
avrcp.GetFolderItemsResponse(
|
||||
status=avrcp.StatusCode.OPERATION_COMPLETED,
|
||||
uid_counter=1,
|
||||
items=[
|
||||
avrcp.MediaPlayerItem(
|
||||
player_id=1,
|
||||
major_player_type=avrcp.MediaPlayerItem.MajorPlayerType.AUDIO,
|
||||
player_sub_type=avrcp.MediaPlayerItem.PlayerSubType.AUDIO_BOOK,
|
||||
play_status=avrcp.PlayStatus.FWD_SEEK,
|
||||
feature_bitmask=avrcp.MediaPlayerItem.Features.ADD_TO_NOW_PLAYING,
|
||||
character_set_id=avrcp.CharacterSetId.UTF_8,
|
||||
displayable_name="Woo",
|
||||
)
|
||||
],
|
||||
),
|
||||
avrcp.ChangePathResponse(
|
||||
status=avrcp.StatusCode.OPERATION_COMPLETED, number_of_items=2
|
||||
),
|
||||
avrcp.GetItemAttributesResponse(
|
||||
status=avrcp.StatusCode.OPERATION_COMPLETED,
|
||||
attribute_value_entry_list=[
|
||||
avrcp.AttributeValueEntry(
|
||||
attribute_id=avrcp.MediaAttributeId.GENRE,
|
||||
character_set_id=avrcp.CharacterSetId.UTF_8,
|
||||
attribute_value="uuddlrlrabab",
|
||||
)
|
||||
],
|
||||
),
|
||||
avrcp.GetTotalNumberOfItemsResponse(
|
||||
status=avrcp.StatusCode.OPERATION_COMPLETED,
|
||||
uid_counter=1,
|
||||
number_of_items=2,
|
||||
),
|
||||
avrcp.SearchResponse(
|
||||
status=avrcp.StatusCode.OPERATION_COMPLETED,
|
||||
uid_counter=1,
|
||||
number_of_items=2,
|
||||
),
|
||||
avrcp.PlayItemResponse(status=avrcp.StatusCode.OPERATION_COMPLETED),
|
||||
avrcp.AddToNowPlayingResponse(status=avrcp.StatusCode.OPERATION_COMPLETED),
|
||||
],
|
||||
)
|
||||
def test_response(response: avrcp.Response):
|
||||
assert avrcp.Response.from_bytes(bytes(response), response.pdu_id) == response
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_frame_parser():
|
||||
with pytest.raises(ValueError) as error:
|
||||
with pytest.raises(ValueError):
|
||||
avc.Frame.from_bytes(bytes.fromhex("11480000"))
|
||||
|
||||
x = bytes.fromhex("014D0208")
|
||||
@@ -217,8 +411,7 @@ def test_passthrough_commands():
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_supported_events():
|
||||
two_devices = TwoDevices()
|
||||
await two_devices.setup_connections()
|
||||
two_devices = await TwoDevices.create_with_avdtp()
|
||||
|
||||
supported_events = await two_devices.protocols[0].get_supported_events()
|
||||
assert supported_events == []
|
||||
|
||||
@@ -310,12 +310,12 @@ async def test_pacs():
|
||||
@pytest.mark.asyncio
|
||||
async def test_ascs():
|
||||
devices = TwoDevices()
|
||||
devices[0].add_service(
|
||||
AudioStreamControlService(device=devices[0], sink_ase_id=[1, 2])
|
||||
devices[1].add_service(
|
||||
AudioStreamControlService(device=devices[1], sink_ase_id=[1, 2])
|
||||
)
|
||||
|
||||
await devices.setup_connection()
|
||||
peer = device.Peer(devices.connections[1])
|
||||
peer = device.Peer(devices.connections[0])
|
||||
ascs_client = await peer.discover_service_and_create_proxy(
|
||||
AudioStreamControlServiceProxy
|
||||
)
|
||||
@@ -369,7 +369,7 @@ async def test_ascs():
|
||||
await ascs_client.ase_control_point.write_value(
|
||||
ASE_Config_QOS(
|
||||
ase_id=[1, 2],
|
||||
cig_id=[1, 2],
|
||||
cig_id=[1, 1],
|
||||
cis_id=[3, 4],
|
||||
sdu_interval=[5, 6],
|
||||
framing=[0, 1],
|
||||
@@ -402,25 +402,19 @@ async def test_ascs():
|
||||
)
|
||||
|
||||
# CIS establishment
|
||||
devices[0].emit(
|
||||
'cis_establishment',
|
||||
device.CisLink(
|
||||
device=devices[0],
|
||||
acl_connection=devices.connections[0],
|
||||
handle=5,
|
||||
cis_id=3,
|
||||
cis_handles = await devices[0].setup_cig(
|
||||
device.CigParameters(
|
||||
cig_id=1,
|
||||
),
|
||||
cis_parameters=[
|
||||
device.CigParameters.CisParameters(cis_id=3),
|
||||
device.CigParameters.CisParameters(cis_id=4),
|
||||
],
|
||||
sdu_interval_c_to_p=0,
|
||||
sdu_interval_p_to_c=0,
|
||||
)
|
||||
)
|
||||
devices[0].emit(
|
||||
'cis_establishment',
|
||||
device.CisLink(
|
||||
device=devices[0],
|
||||
acl_connection=devices.connections[0],
|
||||
handle=6,
|
||||
cis_id=4,
|
||||
cig_id=2,
|
||||
),
|
||||
await devices[0].create_cis(
|
||||
[(cis_handle, devices.connections[0]) for cis_handle in cis_handles]
|
||||
)
|
||||
assert (await notifications[1].get())[:2] == bytes(
|
||||
[1, AseStateMachine.State.STREAMING]
|
||||
|
||||
@@ -24,7 +24,7 @@ from unittest import mock
|
||||
import pytest
|
||||
|
||||
from bumble import device, gatt, hci, utils
|
||||
from bumble.core import ConnectionParameters, PhysicalTransport
|
||||
from bumble.core import PhysicalTransport
|
||||
from bumble.device import (
|
||||
AdvertisingEventProperties,
|
||||
AdvertisingParameters,
|
||||
@@ -289,14 +289,15 @@ async def test_legacy_advertising_disconnection(auto_restart):
|
||||
await device.power_on()
|
||||
peer_address = Address('F0:F1:F2:F3:F4:F5')
|
||||
await device.start_advertising(auto_restart=auto_restart)
|
||||
device.on_connection(
|
||||
device.on_le_connection(
|
||||
0x0001,
|
||||
PhysicalTransport.LE,
|
||||
peer_address,
|
||||
None,
|
||||
None,
|
||||
Role.PERIPHERAL,
|
||||
ConnectionParameters(0, 0, 0),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
|
||||
device.on_advertising_set_termination(
|
||||
@@ -347,14 +348,15 @@ async def test_extended_advertising_connection(own_address_type):
|
||||
advertising_set = await device.create_advertising_set(
|
||||
advertising_parameters=AdvertisingParameters(own_address_type=own_address_type)
|
||||
)
|
||||
device.on_connection(
|
||||
device.on_le_connection(
|
||||
0x0001,
|
||||
PhysicalTransport.LE,
|
||||
peer_address,
|
||||
None,
|
||||
None,
|
||||
Role.PERIPHERAL,
|
||||
ConnectionParameters(0, 0, 0),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
device.on_advertising_set_termination(
|
||||
HCI_SUCCESS,
|
||||
@@ -391,14 +393,15 @@ async def test_extended_advertising_connection_out_of_order(own_address_type):
|
||||
0x0001,
|
||||
0,
|
||||
)
|
||||
device.on_connection(
|
||||
device.on_le_connection(
|
||||
0x0001,
|
||||
PhysicalTransport.LE,
|
||||
Address('F0:F1:F2:F3:F4:F5'),
|
||||
None,
|
||||
None,
|
||||
Role.PERIPHERAL,
|
||||
ConnectionParameters(0, 0, 0),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
|
||||
if own_address_type == OwnAddressType.PUBLIC:
|
||||
@@ -758,6 +761,34 @@ async def test_inquiry_result_with_rssi():
|
||||
m.assert_called_with(hci.Address("00:11:22:33:44:55/P"), 3, mock.ANY, 5)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.parametrize(
|
||||
"roles",
|
||||
(
|
||||
(hci.Role.PERIPHERAL, hci.Role.CENTRAL),
|
||||
(hci.Role.CENTRAL, hci.Role.PERIPHERAL),
|
||||
),
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_accept_classic_connection(roles: tuple[hci.Role, hci.Role]):
|
||||
devices = TwoDevices()
|
||||
devices[0].classic_enabled = True
|
||||
devices[1].classic_enabled = True
|
||||
await devices[0].power_on()
|
||||
await devices[1].power_on()
|
||||
|
||||
accept_task = asyncio.create_task(devices[1].accept(role=roles[1]))
|
||||
await devices[0].connect(
|
||||
devices[1].public_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
await accept_task
|
||||
|
||||
assert devices.connections[0]
|
||||
assert devices.connections[0].role == roles[0]
|
||||
assert devices.connections[1]
|
||||
assert devices.connections[1].role == roles[1]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def run_test_device():
|
||||
await test_device_connect_parallel()
|
||||
|
||||
@@ -24,7 +24,7 @@ import sys
|
||||
import pytest
|
||||
|
||||
from bumble import controller, device, hci, link, transport
|
||||
from bumble.transport.common import PacketParser
|
||||
from bumble.transport import common
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -61,9 +61,9 @@ class Sink:
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_parser():
|
||||
sink1 = Sink()
|
||||
parser1 = PacketParser(sink1)
|
||||
parser1 = common.PacketParser(sink1)
|
||||
sink2 = Sink()
|
||||
parser2 = PacketParser(sink2)
|
||||
parser2 = common.PacketParser(sink2)
|
||||
|
||||
for parser in [parser1, parser2]:
|
||||
with open(
|
||||
@@ -82,7 +82,7 @@ def test_parser():
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_parser_extensions():
|
||||
sink = Sink()
|
||||
parser = PacketParser(sink)
|
||||
parser = common.PacketParser(sink)
|
||||
|
||||
# Check that an exception is thrown for an unknown type
|
||||
try:
|
||||
@@ -206,7 +206,7 @@ async def test_unix_connection_abstract():
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.parametrize(
|
||||
"address,",
|
||||
("127.0.0.1",),
|
||||
("127.0.0.1", "[::1]"),
|
||||
)
|
||||
async def test_android_netsim_connection(address):
|
||||
controller_transport = await transport.open_transport(
|
||||
@@ -222,6 +222,33 @@ async def test_android_netsim_connection(address):
|
||||
await client_device.power_on()
|
||||
|
||||
await client_transport.close()
|
||||
await controller_transport.source.grpc_server.stop(None)
|
||||
await controller_transport.close()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.parametrize(
|
||||
"spec,",
|
||||
(
|
||||
"android-netsim:[::1]:{port},mode=host[a=b,c=d]",
|
||||
"android-netsim:localhost:{port},mode=host[a=b,c=d]",
|
||||
"android-netsim:[a=b,c=d][::1]:{port},mode=host",
|
||||
"android-netsim:[a=b,c=d]localhost:{port},mode=host",
|
||||
),
|
||||
)
|
||||
async def test_open_transport_with_metadata(spec):
|
||||
controller_transport = await transport.open_transport(
|
||||
"android-netsim:_:0,mode=controller"
|
||||
)
|
||||
port = controller_transport.source.port
|
||||
_make_controller_from_transport(controller_transport)
|
||||
|
||||
client_transport = await transport.open_transport(spec.format(port=port))
|
||||
assert client_transport.source.metadata['a'] == 'b'
|
||||
assert client_transport.source.metadata['c'] == 'd'
|
||||
|
||||
await client_transport.close()
|
||||
await controller_transport.source.grpc_server.stop(None)
|
||||
await controller_transport.close()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user