Compare commits

..

21 Commits

Author SHA1 Message Date
Gilles Boccon-Gibod
aec50ac616 Merge pull request #789 from google/gbg/nrf-uart-flow-control 2025-09-26 09:34:33 +02:00
Gilles Boccon-Gibod
6a3eaa457f python 3.9 compat 2025-09-26 08:42:10 +02:00
zxzxwu
6e6b4cd4b2 Merge pull request #773 from wescande/main
HAP: wait for MTU to process reconnection event
2025-09-26 01:36:45 +08:00
Gilles Boccon-Gibod
aa1d7933da enhance serial port transport 2025-09-25 18:31:14 +02:00
zxzxwu
34e0f293c2 Merge pull request #788 from zxzxwu/device
Fix wrong with_connection_from_address parameter
2025-09-23 19:44:50 +08:00
Josh Wu
85215df2c3 Fix wrong with_connection_from_address parameter 2025-09-23 17:55:47 +08:00
zxzxwu
f8223ca81f Merge pull request #780 from google/dependabot/cargo/rust/cargo-ad4b9ff1ea
Bump the cargo group across 1 directory with 5 updates
2025-09-19 14:50:45 +08:00
zxzxwu
2b0b1ad726 Merge pull request #781 from zxzxwu/connections
Revert pending_connections
2025-09-19 14:45:48 +08:00
Josh Wu
58debcd8bb Revert pending_connections 2025-09-19 12:32:28 +08:00
dependabot[bot]
6eba81e3dd Bump the cargo group across 1 directory with 5 updates
Bumps the cargo group with 4 updates in the /rust directory: [tokio](https://github.com/tokio-rs/tokio), [h2](https://github.com/hyperium/h2), [openssl](https://github.com/sfackler/rust-openssl) and [rustix](https://github.com/bytecodealliance/rustix).


Updates `tokio` from 1.32.0 to 1.38.2
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.32.0...tokio-1.38.2)

Updates `h2` from 0.3.21 to 0.3.27
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.27/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.21...v0.3.27)

Updates `mio` from 0.8.8 to 0.8.11
- [Release notes](https://github.com/tokio-rs/mio/releases)
- [Changelog](https://github.com/tokio-rs/mio/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/mio/compare/v0.8.8...v0.8.11)

Updates `openssl` from 0.10.60 to 0.10.73
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.60...openssl-v0.10.73)

Updates `rustix` from 0.38.10 to 0.38.44
- [Release notes](https://github.com/bytecodealliance/rustix/releases)
- [Changelog](https://github.com/bytecodealliance/rustix/blob/main/CHANGES.md)
- [Commits](https://github.com/bytecodealliance/rustix/compare/v0.38.10...v0.38.44)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.38.2
  dependency-type: direct:production
  dependency-group: cargo
- dependency-name: h2
  dependency-version: 0.3.27
  dependency-type: indirect
  dependency-group: cargo
- dependency-name: mio
  dependency-version: 0.8.11
  dependency-type: indirect
  dependency-group: cargo
- dependency-name: openssl
  dependency-version: 0.10.73
  dependency-type: indirect
  dependency-group: cargo
- dependency-name: rustix
  dependency-version: 0.38.44
  dependency-type: indirect
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-17 08:10:17 +00:00
zxzxwu
768bbd95cc Merge pull request #778 from zxzxwu/rust
Upgrade Rust to 1.80.0
2025-09-17 16:08:15 +08:00
Josh Wu
502b80af0d Upgrade Rust to 1.80.0 2025-09-17 13:34:08 +08:00
zxzxwu
a25427305c Merge pull request #775 from khsiao-google/update
Remove the word 'complete' from function name
2025-09-17 13:18:37 +08:00
zxzxwu
3c47739029 Merge pull request #776 from khsiao-google/test_coverage
Add a2dp_test.py tests for a2dp.py
2025-09-17 13:18:14 +08:00
zxzxwu
8fc1330948 Merge pull request #777 from zxzxwu/iso
Handle ISO data path race condition
2025-09-17 13:17:53 +08:00
William Escande
8a5f6a61d5 HAP: wait for MTU to process reconnection event
When HAP reconnect, it sends indication of all events that happen during
the disconnection.
But it should wait for the profile to be ready and for the MTU to have
been negotiated or else the remote may not be ready yet.

As a side effect of this, the current GattServer doesn't re-populate the
handle of subscriber during a reconnection, we have to bypass this check
to send the notification
2025-09-16 16:18:16 -07:00
Josh Wu
83c5061700 Handle ISO data path race condition 2025-09-16 13:39:09 +08:00
khsiao-google
b80b790dc1 Remove the word 'complete' from function name 2025-09-16 03:45:32 +00:00
khsiao-google
21bf69592c Add a2dp_test.py tests for a2dp.py 2025-09-16 03:23:53 +00:00
zxzxwu
7d8addb849 Merge pull request #762 from zxzxwu/ipv6
Distinguish IPv6 address and metadata
2025-09-10 15:58:41 +08:00
Josh Wu
bb08a1c70b Distinguish IPv6 address and metadata 2025-09-09 11:59:51 +08:00
17 changed files with 813 additions and 368 deletions

View File

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

View File

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

View File

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

View File

@@ -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
@@ -2176,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: hci.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
@@ -2191,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
@@ -2367,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
@@ -3836,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
@@ -3985,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:
@@ -5449,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:
@@ -5477,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:
@@ -5539,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
@@ -5610,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.
@@ -5639,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(
@@ -5952,7 +5992,7 @@ class Device(utils.CompositeEventEmitter):
@host_event_handler
@try_with_connection_from_address
def on_remote_name(
self, connection: Connection, address: hci.Address, remote_name: bytes
self, connection: Optional[Connection], address: hci.Address, remote_name: bytes
):
# Try to decode the name
try:
@@ -5971,7 +6011,7 @@ class Device(utils.CompositeEventEmitter):
@host_event_handler
@try_with_connection_from_address
def on_remote_name_failure(
self, connection: Connection, address: hci.Address, error: int
self, connection: Optional[Connection], address: hci.Address, error: int
):
if connection:
connection.emit(connection.EVENT_REMOTE_NAME_FAILURE, error)
@@ -6183,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,
)
@@ -6404,7 +6444,11 @@ class Device(utils.CompositeEventEmitter):
# [Classic only]
@host_event_handler
@with_connection_from_address
def on_role_change(self, connection: Connection, new_role: hci.Role):
def on_role_change(
self,
connection: Connection,
new_role: hci.Role,
):
connection.role = new_role
connection.emit(connection.EVENT_ROLE_CHANGE, new_role)
@@ -6412,7 +6456,7 @@ class Device(utils.CompositeEventEmitter):
@host_event_handler
@try_with_connection_from_address
def on_role_change_failure(
self, connection: Connection, address: hci.Address, error: int
self, connection: Optional[Connection], address: hci.Address, error: int
):
if connection:
connection.emit(connection.EVENT_ROLE_CHANGE_FAILURE, error)

View File

@@ -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
@@ -555,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}'
)
@@ -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

View File

@@ -273,12 +273,19 @@ class HearingAccessService(gatt.TemplateService):
def on_disconnection(_reason) -> None:
self.currently_connected_clients.discard(connection)
@connection.on(connection.EVENT_CONNECTION_ATT_MTU_UPDATE)
def on_mtu_update(*_: Any) -> None:
self.on_incoming_connection(connection)
@connection.on(connection.EVENT_CONNECTION_ENCRYPTION_CHANGE)
def on_encryption_change(*_: Any) -> None:
self.on_incoming_connection(connection)
@connection.on(connection.EVENT_PAIRING)
def on_pairing(*_: Any) -> None:
self.on_incoming_paired_connection(connection)
self.on_incoming_connection(connection)
if connection.peer_resolvable_address:
self.on_incoming_paired_connection(connection)
self.on_incoming_connection(connection)
self.hearing_aid_features_characteristic = gatt.Characteristic(
uuid=gatt.GATT_HEARING_AID_FEATURES_CHARACTERISTIC,
@@ -315,9 +322,30 @@ class HearingAccessService(gatt.TemplateService):
]
)
def on_incoming_paired_connection(self, connection: Connection):
def on_incoming_connection(self, connection: Connection):
'''Setup initial operations to handle a remote bonded HAP device'''
# TODO Should we filter on HAP device only ?
if not connection.is_encrypted:
logging.debug(f'HAS: {connection.peer_address} is not encrypted')
return
if not connection.peer_resolvable_address:
logging.debug(f'HAS: {connection.peer_address} is not paired')
return
if connection.att_mtu < 49:
logging.debug(
f'HAS: {connection.peer_address} invalid MTU={connection.att_mtu}'
)
return
if connection.peer_address in self.currently_connected_clients:
logging.debug(
f'HAS: Already connected to {connection.peer_address} nothing to do'
)
return
self.currently_connected_clients.add(connection)
if (
connection.peer_address
@@ -457,6 +485,7 @@ class HearingAccessService(gatt.TemplateService):
connection,
self.hearing_aid_preset_control_point,
value=op_list[0].to_bytes(len(op_list) == 1),
force=True, # TODO GATT notification subscription should be persistent
)
# Remove item once sent, and keep the non sent item in the list
op_list.pop(0)

View File

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

View File

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

View File

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

View File

@@ -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
View File

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

View File

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

View File

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

View File

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

View File

@@ -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()

View File

@@ -82,7 +82,6 @@ async def hap_client():
)
await devices.setup_connection()
# TODO negotiate MTU > 49 to not truncate preset names
# Mock encryption.
devices.connections[0].encryption = 1 # type: ignore
@@ -93,6 +92,9 @@ async def hap_client():
)
peer = device.Peer(devices.connections[1]) # type: ignore
await peer.request_mtu(49)
peer2 = device.Peer(devices.connections[0]) # type: ignore
await peer2.request_mtu(49)
hap_client = await peer.discover_service_and_create_proxy(
hap.HearingAccessServiceProxy
)

View File

@@ -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()