mirror of
https://github.com/google/bumble.git
synced 2026-05-07 03:48:01 +00:00
Compare commits
10 Commits
revert-771
...
v0.0.215
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
768bbd95cc | ||
|
|
502b80af0d | ||
|
|
a25427305c | ||
|
|
3c47739029 | ||
|
|
8fc1330948 | ||
|
|
83c5061700 | ||
|
|
b80b790dc1 | ||
|
|
21bf69592c | ||
|
|
7d8addb849 | ||
|
|
bb08a1c70b |
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
|
||||
|
||||
@@ -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
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
247
bumble/device.py
247
bumble/device.py
@@ -1453,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 = (
|
||||
@@ -1462,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,
|
||||
@@ -1482,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."""
|
||||
@@ -1622,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
|
||||
@@ -1638,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
|
||||
|
||||
|
||||
@@ -1782,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,
|
||||
@@ -1831,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:
|
||||
@@ -1872,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
|
||||
@@ -2177,8 +2170,6 @@ def with_connection_from_handle(function):
|
||||
def with_connection_from_address(function):
|
||||
@functools.wraps(function)
|
||||
def wrapper(self, address: hci.Address, *args, **kwargs):
|
||||
if connection := self.pending_connections.get(address, False):
|
||||
return function(self, connection, *args, **kwargs)
|
||||
for connection in self.connections.values():
|
||||
if connection.peer_address == address:
|
||||
return function(self, connection, *args, **kwargs)
|
||||
@@ -2192,8 +2183,6 @@ def with_connection_from_address(function):
|
||||
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():
|
||||
if connection.peer_address == address:
|
||||
return function(self, connection, address, *args, **kwargs)
|
||||
@@ -2245,7 +2234,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
scan_response_data: bytes
|
||||
cs_capabilities: ChannelSoundingCapabilities | None = None
|
||||
connections: dict[int, Connection]
|
||||
pending_connections: dict[hci.Address, Connection]
|
||||
connection_roles: dict[hci.Address, hci.Role]
|
||||
classic_pending_accepts: dict[
|
||||
hci.Address,
|
||||
list[asyncio.Future[Union[Connection, tuple[hci.Address, int, int]]]],
|
||||
@@ -2367,7 +2356,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.connection_roles = (
|
||||
{}
|
||||
) # Local connection roles, 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
|
||||
@@ -3836,9 +3827,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
)
|
||||
else:
|
||||
# Save pending connection
|
||||
self.pending_connections[peer_address] = Connection.incomplete(
|
||||
self, peer_address, hci.Role.CENTRAL
|
||||
)
|
||||
self.connection_roles[peer_address] = hci.Role.CENTRAL
|
||||
|
||||
# TODO: allow passing other settings
|
||||
result = await self.send_command(
|
||||
@@ -3891,7 +3880,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
self.le_connecting = False
|
||||
self.connect_own_address_type = None
|
||||
else:
|
||||
self.pending_connections.pop(peer_address, None)
|
||||
self.connection_roles.pop(peer_address, None)
|
||||
|
||||
async def accept(
|
||||
self,
|
||||
@@ -3985,13 +3974,11 @@ 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.connection_roles[peer_address] = hci.Role.PERIPHERAL
|
||||
|
||||
try:
|
||||
# Accept connection request
|
||||
@@ -4009,7 +3996,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
finally:
|
||||
self.remove_listener(self.EVENT_CONNECTION, on_connection)
|
||||
self.remove_listener(self.EVENT_CONNECTION_FAILURE, on_connection_failure)
|
||||
self.pending_connections.pop(peer_address, None)
|
||||
self.connection_roles.pop(peer_address, None)
|
||||
|
||||
@asynccontextmanager
|
||||
async def connect_as_gatt(self, peer_address: Union[hci.Address, str]):
|
||||
@@ -5449,15 +5436,49 @@ 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:
|
||||
connection_role = self.connection_roles.pop(peer_address, hci.Role.PERIPHERAL)
|
||||
|
||||
logger.debug(
|
||||
f'*** Connection: [0x{connection_handle:04X}] '
|
||||
f'{peer_address} {hci.HCI_Constant.role_name(connection_role)}'
|
||||
)
|
||||
if connection_handle in self.connections:
|
||||
logger.warning(
|
||||
'new connection reuses the same handle as a previous connection'
|
||||
)
|
||||
|
||||
# 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=connection_role,
|
||||
parameters=Connection.Parameters(0.0, 0, 0.0),
|
||||
)
|
||||
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:
|
||||
@@ -5477,19 +5498,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:
|
||||
@@ -5539,16 +5547,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
|
||||
@@ -5639,9 +5647,7 @@ 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.connection_roles[bd_addr] = hci.Role.PERIPHERAL
|
||||
|
||||
self.host.send_command_sync(
|
||||
hci.HCI_Accept_Connection_Request_Command(
|
||||
@@ -6183,27 +6189,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,
|
||||
)
|
||||
@@ -6403,10 +6409,15 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_role_change(self, connection: Connection, new_role: hci.Role):
|
||||
connection.role = new_role
|
||||
connection.emit(connection.EVENT_ROLE_CHANGE, new_role)
|
||||
@try_with_connection_from_address
|
||||
def on_role_change(
|
||||
self, connection: Connection, peer_address: hci.Address, new_role: hci.Role
|
||||
):
|
||||
if connection:
|
||||
connection.role = new_role
|
||||
connection.emit(connection.EVENT_ROLE_CHANGE, new_role)
|
||||
else:
|
||||
self.connection_roles[peer_address] = new_role
|
||||
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
|
||||
@@ -26,12 +26,7 @@ from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, Union, cas
|
||||
|
||||
from bumble import drivers, hci, utils
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
ConnectionParameters,
|
||||
ConnectionPHY,
|
||||
InvalidStateError,
|
||||
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
|
||||
@@ -996,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}')
|
||||
@@ -1060,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}')
|
||||
@@ -1130,14 +1116,13 @@ class Host(utils.EventEmitter):
|
||||
|
||||
# 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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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