Compare commits

..

14 Commits

Author SHA1 Message Date
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
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
12 changed files with 681 additions and 358 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, address, *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,
)
@@ -6403,16 +6443,24 @@ 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: Optional[Connection],
peer_address: hci.Address,
new_role: hci.Role,
):
if connection:
connection.role = new_role
connection.emit(connection.EVENT_ROLE_CHANGE, new_role)
else:
logger.warning("Role change to unknown connection %s", peer_address)
# [Classic only]
@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
@@ -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

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

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:

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