Compare commits

..

1 Commits

Author SHA1 Message Date
khsiao-google ca23d6b89a Revert "Improve connection related functions and names" 2025-09-10 15:00:41 +08:00
18 changed files with 372 additions and 816 deletions
+2 -2
View File
@@ -49,7 +49,7 @@ jobs:
strategy: strategy:
matrix: matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
rust-version: [ "1.80.0", "stable" ] rust-version: [ "1.76.0", "stable" ]
fail-fast: false fail-fast: false
steps: steps:
- name: Check out from Git - name: Check out from Git
@@ -72,7 +72,7 @@ jobs:
- name: Check License Headers - name: Check License Headers
run: cd rust && cargo run --features dev-tools --bin file-header check-all run: cd rust && cargo run --features dev-tools --bin file-header check-all
- name: Rust Build - name: Rust Build
run: cd rust && cargo build --all-targets && cargo build-all-features run: cd rust && cargo build --all-targets && cargo build-all-features --all-targets
# Lints after build so what clippy needs is already built # Lints after build so what clippy needs is already built
- name: Rust Lints - name: Rust Lints
run: cd rust && cargo fmt --check && cargo clippy --all-targets -- --deny warnings && cargo clippy --all-features --all-targets -- --deny warnings run: cd rust && cargo fmt --check && cargo clippy --all-targets -- --deny warnings && cargo clippy --all-features --all-targets -- --deny warnings
+1 -1
View File
@@ -50,7 +50,7 @@ Bumble is easiest to use with a dedicated USB dongle.
This is because internal Bluetooth interfaces tend to be locked down by the operating system. This is because internal Bluetooth interfaces tend to be locked down by the operating system.
You can use the [usb_probe](/docs/mkdocs/src/apps_and_tools/usb_probe.md) tool (all platforms) or `lsusb` (Linux or macOS) to list the available USB devices on your system. You can use the [usb_probe](/docs/mkdocs/src/apps_and_tools/usb_probe.md) tool (all platforms) or `lsusb` (Linux or macOS) to list the available USB devices on your system.
See the [USB Transport](/docs/mkdocs/src/transports/usb.md) page for details on how to refer to USB devices. Also, if you are on a mac, see [these instructions](docs/mkdocs/src/platforms/macos.md). See the [USB Transport](/docs/mkdocs/src/transports/usb.md) page for details on how to refer to USB devices. Also, if your are on a mac, see [these instructions](docs/mkdocs/src/platforms/macos.md).
## License ## License
+3 -43
View File
@@ -33,6 +33,7 @@ from bumble.hci import (
HCI_COMMAND_DISALLOWED_ERROR, HCI_COMMAND_DISALLOWED_ERROR,
HCI_COMMAND_PACKET, HCI_COMMAND_PACKET,
HCI_COMMAND_STATUS_PENDING, HCI_COMMAND_STATUS_PENDING,
HCI_CONNECTION_TIMEOUT_ERROR,
HCI_CONTROLLER_BUSY_ERROR, HCI_CONTROLLER_BUSY_ERROR,
HCI_EVENT_PACKET, HCI_EVENT_PACKET,
HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR, HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR,
@@ -87,7 +88,6 @@ class CisLink:
cis_id: int cis_id: int
cig_id: int cig_id: int
acl_connection: Optional[Connection] = None acl_connection: Optional[Connection] = None
data_paths: set[int] = dataclasses.field(default_factory=set)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -381,11 +381,6 @@ class Controller:
return connection return connection
return None 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): def on_link_central_connected(self, central_address):
''' '''
Called when an incoming connection occurs from a central on the link Called when an incoming connection occurs from a central on the link
@@ -1858,51 +1853,16 @@ class Controller:
) )
) )
def on_hci_le_setup_iso_data_path_command( def on_hci_le_setup_iso_data_path_command(self, 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 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) return struct.pack('<BH', HCI_SUCCESS, command.connection_handle)
def on_hci_le_remove_iso_data_path_command( def on_hci_le_remove_iso_data_path_command(self, 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 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) return struct.pack('<BH', HCI_SUCCESS, command.connection_handle)
def on_hci_le_set_host_feature_command( def on_hci_le_set_host_feature_command(
+17
View File
@@ -2110,6 +2110,23 @@ class AdvertisingData:
return self.to_string() 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 # Connection PHY
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
+123 -166
View File
@@ -1453,8 +1453,6 @@ class _IsoLink:
handle: int handle: int
device: Device device: Device
sink: Callable[[hci.HCI_IsoDataPacket], Any] | None = None sink: Callable[[hci.HCI_IsoDataPacket], Any] | None = None
data_paths: set[_IsoLink.Direction]
_data_path_lock: asyncio.Lock
class Direction(IntEnum): class Direction(IntEnum):
HOST_TO_CONTROLLER = ( HOST_TO_CONTROLLER = (
@@ -1464,10 +1462,6 @@ class _IsoLink:
hci.HCI_LE_Setup_ISO_Data_Path_Command.Direction.CONTROLLER_TO_HOST 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( async def setup_data_path(
self, self,
direction: _IsoLink.Direction, direction: _IsoLink.Direction,
@@ -1488,45 +1482,37 @@ class _IsoLink:
Raises: Raises:
HCI_Error: When command complete status is not HCI_SUCCESS. HCI_Error: When command complete status is not HCI_SUCCESS.
""" """
async with self._data_path_lock: await self.device.send_command(
if direction in self.data_paths: hci.HCI_LE_Setup_ISO_Data_Path_Command(
return connection_handle=self.handle,
await self.device.send_command( data_path_direction=direction,
hci.HCI_LE_Setup_ISO_Data_Path_Command( data_path_id=data_path_id,
connection_handle=self.handle, codec_id=codec_id or hci.CodingFormat(hci.CodecID.TRANSPARENT),
data_path_direction=direction, controller_delay=controller_delay,
data_path_id=data_path_id, codec_configuration=codec_configuration,
codec_id=codec_id or hci.CodingFormat(hci.CodecID.TRANSPARENT), ),
controller_delay=controller_delay, check_result=True,
codec_configuration=codec_configuration, )
),
check_result=True,
)
self.data_paths.add(direction)
async def remove_data_path(self, directions: Iterable[_IsoLink.Direction]) -> None: async def remove_data_path(self, directions: Iterable[_IsoLink.Direction]) -> int:
"""Remove a data path with controller on given direction. """Remove a data path with controller on given direction.
Args: Args:
direction: Direction of data path. direction: Direction of data path.
Raises: Returns:
HCI_Error: When command complete status is not HCI_SUCCESS. Command status.
""" """
async with self._data_path_lock: response = await self.device.send_command(
directions_to_remove = set(directions).intersection(self.data_paths) hci.HCI_LE_Remove_ISO_Data_Path_Command(
if not directions_to_remove: connection_handle=self.handle,
return data_path_direction=sum(
await self.device.send_command( 1 << direction for direction in set(directions)
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=True, ),
) check_result=False,
self.data_paths.difference_update(directions_to_remove) )
return response.return_parameters.status
def write(self, sdu: bytes) -> None: def write(self, sdu: bytes) -> None:
"""Write an ISO SDU.""" """Write an ISO SDU."""
@@ -1636,8 +1622,7 @@ class CisLink(utils.EventEmitter, _IsoLink):
EVENT_ESTABLISHMENT_FAILURE: ClassVar[str] = "establishment_failure" EVENT_ESTABLISHMENT_FAILURE: ClassVar[str] = "establishment_failure"
def __post_init__(self) -> None: def __post_init__(self) -> None:
utils.EventEmitter.__init__(self) super().__init__()
_IsoLink.__init__(self)
async def disconnect( async def disconnect(
self, reason: int = hci.HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR self, reason: int = hci.HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR
@@ -1653,7 +1638,6 @@ class BisLink(_IsoLink):
sink: Callable[[hci.HCI_IsoDataPacket], Any] | None = None sink: Callable[[hci.HCI_IsoDataPacket], Any] | None = None
def __post_init__(self) -> None: def __post_init__(self) -> None:
super().__init__()
self.device = self.big.device self.device = self.big.device
@@ -1798,22 +1782,15 @@ class Connection(utils.CompositeEventEmitter):
@dataclass @dataclass
class Parameters: class Parameters:
""" connection_interval: float # Connection interval, in milliseconds. [LE only]
LE connection parameters. peripheral_latency: int # Peripheral latency, in number of intervals. [LE only]
supervision_timeout: float # Supervision timeout, in milliseconds.
Attributes: subrate_factor: int = (
connection_interval: Connection interval, in milliseconds. 1 # See Bluetooth spec Vol 6, Part B - 4.5.1 Connection events
peripheral_latency: Peripheral latency, in number of intervals. )
supervision_timeout: Supervision timeout, in milliseconds. continuation_number: int = (
subrate_factor: See Bluetooth spec Vol 6, Part B - 4.5.1 Connection events 0 # 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__( def __init__(
self, self,
@@ -1854,6 +1831,36 @@ class Connection(utils.CompositeEventEmitter):
self.cs_configs = {} self.cs_configs = {}
self.cs_procedures = {} 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 @property
def role_name(self): def role_name(self):
if self.role is None: if self.role is None:
@@ -1865,7 +1872,7 @@ class Connection(utils.CompositeEventEmitter):
return f'UNKNOWN[{self.role}]' return f'UNKNOWN[{self.role}]'
@property @property
def is_encrypted(self) -> bool: def is_encrypted(self):
return self.encryption != 0 return self.encryption != 0
@property @property
@@ -2169,12 +2176,12 @@ def with_connection_from_handle(function):
# Decorator that converts the first argument from a bluetooth address to a connection # Decorator that converts the first argument from a bluetooth address to a connection
def with_connection_from_address(function): def with_connection_from_address(function):
@functools.wraps(function) @functools.wraps(function)
def wrapper(device: Device, address: hci.Address, *args, **kwargs): def wrapper(self, address: hci.Address, *args, **kwargs):
if connection := device.pending_connections.get(address): if connection := self.pending_connections.get(address, False):
return function(device, connection, *args, **kwargs) return function(self, connection, *args, **kwargs)
for connection in device.connections.values(): for connection in self.connections.values():
if connection.peer_address == address: if connection.peer_address == address:
return function(device, connection, *args, **kwargs) return function(self, connection, *args, **kwargs)
raise ObjectLookupError('no connection for address') raise ObjectLookupError('no connection for address')
return wrapper return wrapper
@@ -2184,13 +2191,13 @@ def with_connection_from_address(function):
# connection # connection
def try_with_connection_from_address(function): def try_with_connection_from_address(function):
@functools.wraps(function) @functools.wraps(function)
def wrapper(device: Device, address: hci.Address, *args, **kwargs): def wrapper(self, address, *args, **kwargs):
if connection := device.pending_connections.get(address): if connection := self.pending_connections.get(address, False):
return function(device, connection, address, *args, **kwargs) return function(self, connection, address, *args, **kwargs)
for connection in device.connections.values(): for connection in self.connections.values():
if connection.peer_address == address: if connection.peer_address == address:
return function(device, connection, address, *args, **kwargs) return function(self, connection, address, *args, **kwargs)
return function(device, None, address, *args, **kwargs) return function(self, None, address, *args, **kwargs)
return wrapper return wrapper
@@ -2263,6 +2270,8 @@ class Device(utils.CompositeEventEmitter):
EVENT_CONNECTION_FAILURE = "connection_failure" EVENT_CONNECTION_FAILURE = "connection_failure"
EVENT_SCO_REQUEST = "sco_request" EVENT_SCO_REQUEST = "sco_request"
EVENT_INQUIRY_COMPLETE = "inquiry_complete" EVENT_INQUIRY_COMPLETE = "inquiry_complete"
EVENT_REMOTE_NAME = "remote_name"
EVENT_REMOTE_NAME_FAILURE = "remote_name_failure"
EVENT_SCO_CONNECTION = "sco_connection" EVENT_SCO_CONNECTION = "sco_connection"
EVENT_SCO_CONNECTION_FAILURE = "sco_connection_failure" EVENT_SCO_CONNECTION_FAILURE = "sco_connection_failure"
EVENT_CIS_REQUEST = "cis_request" EVENT_CIS_REQUEST = "cis_request"
@@ -2358,9 +2367,7 @@ class Device(utils.CompositeEventEmitter):
self.le_connecting = False self.le_connecting = False
self.disconnecting = False self.disconnecting = False
self.connections = {} # Connections, by connection handle self.connections = {} # Connections, by connection handle
self.pending_connections = ( self.pending_connections = {} # Connections, by BD address (BR/EDR only)
{}
) # Pending connections, by BD address (BR/EDR only)
self.sco_links = {} # ScoLinks, by connection handle (BR/EDR only) self.sco_links = {} # ScoLinks, by connection handle (BR/EDR only)
self.cis_links = {} # CisLinks, by connection handle (LE only) self.cis_links = {} # CisLinks, by connection handle (LE only)
self._pending_cis = {} # (CIS_ID, CIG_ID), by CIS_handle self._pending_cis = {} # (CIS_ID, CIG_ID), by CIS_handle
@@ -3829,16 +3836,8 @@ class Device(utils.CompositeEventEmitter):
) )
else: else:
# Save pending connection # Save pending connection
self.pending_connections[peer_address] = Connection( self.pending_connections[peer_address] = Connection.incomplete(
device=self, self, peer_address, hci.Role.CENTRAL
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 # TODO: allow passing other settings
@@ -3986,20 +3985,12 @@ class Device(utils.CompositeEventEmitter):
self.on(self.EVENT_CONNECTION, on_connection) self.on(self.EVENT_CONNECTION, on_connection)
self.on(self.EVENT_CONNECTION_FAILURE, on_connection_failure) self.on(self.EVENT_CONNECTION_FAILURE, on_connection_failure)
# Save Peripheral hci.role. # Save pending connection, with the Peripheral hci.role.
# Even if we requested a role switch in the hci.HCI_Accept_Connection_Request # Even if we requested a role switch in the hci.HCI_Accept_Connection_Request
# command, this connection is still considered Peripheral until an eventual # command, this connection is still considered Peripheral until an eventual
# role change event. # role change event.
self.pending_connections[peer_address] = Connection( self.pending_connections[peer_address] = Connection.incomplete(
device=self, self, peer_address, hci.Role.PERIPHERAL
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: try:
@@ -4725,7 +4716,7 @@ class Device(utils.CompositeEventEmitter):
self, cis_acl_pairs: Sequence[tuple[int, Connection]] self, cis_acl_pairs: Sequence[tuple[int, Connection]]
) -> list[CisLink]: ) -> list[CisLink]:
for cis_handle, acl_connection in cis_acl_pairs: for cis_handle, acl_connection in cis_acl_pairs:
cis_id, cig_id = self._pending_cis[cis_handle] cis_id, cig_id = self._pending_cis.pop(cis_handle)
self.cis_links[cis_handle] = CisLink( self.cis_links[cis_handle] = CisLink(
device=self, device=self,
acl_connection=acl_connection, acl_connection=acl_connection,
@@ -4741,7 +4732,6 @@ class Device(utils.CompositeEventEmitter):
} }
def on_cis_establishment(cis_link: CisLink) -> None: def on_cis_establishment(cis_link: CisLink) -> None:
self._pending_cis.pop(cis_link.handle)
if pending_future := pending_cis_establishments.get(cis_link.handle): if pending_future := pending_cis_establishments.get(cis_link.handle):
pending_future.set_result(cis_link) pending_future.set_result(cis_link)
@@ -5459,47 +5449,15 @@ class Device(utils.CompositeEventEmitter):
self.emit(self.EVENT_CONNECTION, connection) self.emit(self.EVENT_CONNECTION, connection)
@host_event_handler @host_event_handler
def on_classic_connection( def on_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, self,
connection_handle: int, connection_handle: int,
transport: core.PhysicalTransport,
peer_address: hci.Address, peer_address: hci.Address,
self_resolvable_address: Optional[hci.Address], self_resolvable_address: Optional[hci.Address],
peer_resolvable_address: Optional[hci.Address], peer_resolvable_address: Optional[hci.Address],
role: hci.Role, role: hci.Role,
connection_interval: int, connection_parameters: Optional[core.ConnectionParameters],
peripheral_latency: int,
supervision_timeout: int,
) -> None: ) -> None:
# Convert all-zeros addresses into None. # Convert all-zeros addresses into None.
if self_resolvable_address == hci.Address.ANY_RANDOM: if self_resolvable_address == hci.Address.ANY_RANDOM:
@@ -5519,6 +5477,19 @@ class Device(utils.CompositeEventEmitter):
'new connection reuses the same handle as a previous connection' '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: if peer_resolvable_address is None:
# Resolve the peer address if we can # Resolve the peer address if we can
if self.address_resolver: if self.address_resolver:
@@ -5568,16 +5539,16 @@ class Device(utils.CompositeEventEmitter):
connection = Connection( connection = Connection(
self, self,
connection_handle, connection_handle,
PhysicalTransport.LE, transport,
self_address, self_address,
self_resolvable_address, self_resolvable_address,
peer_address, peer_address,
peer_resolvable_address, peer_resolvable_address,
role, role,
Connection.Parameters( Connection.Parameters(
connection_interval * 1.25, connection_parameters.connection_interval * 1.25,
peripheral_latency, connection_parameters.peripheral_latency,
supervision_timeout * 10.0, connection_parameters.supervision_timeout * 10.0,
), ),
) )
self.connections[connection_handle] = connection self.connections[connection_handle] = connection
@@ -5639,9 +5610,7 @@ class Device(utils.CompositeEventEmitter):
# FIXME: Explore a delegate-model for BR/EDR wait connection #56. # FIXME: Explore a delegate-model for BR/EDR wait connection #56.
@host_event_handler @host_event_handler
def on_connection_request( def on_connection_request(self, bd_addr, class_of_device, link_type):
self, bd_addr: hci.Address, class_of_device: int, link_type: int
):
logger.debug(f'*** Connection request: {bd_addr}') logger.debug(f'*** Connection request: {bd_addr}')
# Handle SCO request. # Handle SCO request.
@@ -5670,16 +5639,8 @@ class Device(utils.CompositeEventEmitter):
# device configuration is set to accept any incoming connection # device configuration is set to accept any incoming connection
elif self.classic_accept_any: elif self.classic_accept_any:
# Save pending connection # Save pending connection
self.pending_connections[bd_addr] = Connection( self.pending_connections[bd_addr] = Connection.incomplete(
device=self, self, bd_addr, hci.Role.PERIPHERAL
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( self.host.send_command_sync(
@@ -5991,7 +5952,7 @@ class Device(utils.CompositeEventEmitter):
@host_event_handler @host_event_handler
@try_with_connection_from_address @try_with_connection_from_address
def on_remote_name( def on_remote_name(
self, connection: Optional[Connection], address: hci.Address, remote_name: bytes self, connection: Connection, address: hci.Address, remote_name: bytes
): ):
# Try to decode the name # Try to decode the name
try: try:
@@ -6010,7 +5971,7 @@ class Device(utils.CompositeEventEmitter):
@host_event_handler @host_event_handler
@try_with_connection_from_address @try_with_connection_from_address
def on_remote_name_failure( def on_remote_name_failure(
self, connection: Optional[Connection], address: hci.Address, error: int self, connection: Connection, address: hci.Address, error: int
): ):
if connection: if connection:
connection.emit(connection.EVENT_REMOTE_NAME_FAILURE, error) connection.emit(connection.EVENT_REMOTE_NAME_FAILURE, error)
@@ -6222,27 +6183,27 @@ class Device(utils.CompositeEventEmitter):
@host_event_handler @host_event_handler
@with_connection_from_handle @with_connection_from_handle
def on_connection_parameters_update( def on_connection_parameters_update(
self, self, connection: Connection, connection_parameters: core.ConnectionParameters
connection: Connection,
connection_interval: int,
peripheral_latency: int,
supervision_timeout: int,
): ):
logger.debug( logger.debug(
f'*** Connection Parameters Update: [0x{connection.handle:04X}] ' f'*** Connection Parameters Update: [0x{connection.handle:04X}] '
f'{connection.peer_address} as {connection.role_name}, ' f'{connection.peer_address} as {connection.role_name}, '
f'{connection_parameters}'
) )
if connection.parameters.connection_interval != connection_interval * 1.25: if (
connection.parameters.connection_interval
!= connection_parameters.connection_interval * 1.25
):
connection.parameters = Connection.Parameters( connection.parameters = Connection.Parameters(
connection_interval * 1.25, connection_parameters.connection_interval * 1.25,
peripheral_latency, connection_parameters.peripheral_latency,
supervision_timeout * 10.0, connection_parameters.supervision_timeout * 10.0,
) )
else: else:
connection.parameters = Connection.Parameters( connection.parameters = Connection.Parameters(
connection_interval * 1.25, connection_parameters.connection_interval * 1.25,
peripheral_latency, connection_parameters.peripheral_latency,
supervision_timeout * 10.0, connection_parameters.supervision_timeout * 10.0,
connection.parameters.subrate_factor, connection.parameters.subrate_factor,
connection.parameters.continuation_number, connection.parameters.continuation_number,
) )
@@ -6443,11 +6404,7 @@ class Device(utils.CompositeEventEmitter):
# [Classic only] # [Classic only]
@host_event_handler @host_event_handler
@with_connection_from_address @with_connection_from_address
def on_role_change( def on_role_change(self, connection: Connection, new_role: hci.Role):
self,
connection: Connection,
new_role: hci.Role,
):
connection.role = new_role connection.role = new_role
connection.emit(connection.EVENT_ROLE_CHANGE, new_role) connection.emit(connection.EVENT_ROLE_CHANGE, new_role)
@@ -6455,7 +6412,7 @@ class Device(utils.CompositeEventEmitter):
@host_event_handler @host_event_handler
@try_with_connection_from_address @try_with_connection_from_address
def on_role_change_failure( def on_role_change_failure(
self, connection: Optional[Connection], address: hci.Address, error: int self, connection: Connection, address: hci.Address, error: int
): ):
if connection: if connection:
connection.emit(connection.EVENT_ROLE_CHANGE_FAILURE, error) connection.emit(connection.EVENT_ROLE_CHANGE_FAILURE, error)
+25 -10
View File
@@ -26,7 +26,12 @@ from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, Union, cas
from bumble import drivers, hci, utils from bumble import drivers, hci, utils
from bumble.colors import color from bumble.colors import color
from bumble.core import ConnectionPHY, InvalidStateError, PhysicalTransport from bumble.core import (
ConnectionParameters,
ConnectionPHY,
InvalidStateError,
PhysicalTransport,
)
from bumble.l2cap import L2CAP_PDU from bumble.l2cap import L2CAP_PDU
from bumble.snoop import Snooper from bumble.snoop import Snooper
from bumble.transport.common import TransportLostError from bumble.transport.common import TransportLostError
@@ -550,7 +555,7 @@ class Host(utils.EventEmitter):
logger.debug( logger.debug(
'HCI LE flow control: ' 'HCI LE flow control: '
f'le_acl_data_packet_length={le_acl_data_packet_length},' 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'iso_data_packet_length={iso_data_packet_length},'
f'total_num_iso_data_packets={total_num_iso_data_packets}' f'total_num_iso_data_packets={total_num_iso_data_packets}'
) )
@@ -991,16 +996,20 @@ class Host(utils.EventEmitter):
self.connections[event.connection_handle] = connection self.connections[event.connection_handle] = connection
# Notify the client # Notify the client
connection_parameters = ConnectionParameters(
event.connection_interval,
event.peripheral_latency,
event.supervision_timeout,
)
self.emit( self.emit(
'le_connection', 'connection',
event.connection_handle, event.connection_handle,
PhysicalTransport.LE,
event.peer_address, event.peer_address,
getattr(event, 'local_resolvable_private_address', None), getattr(event, 'local_resolvable_private_address', None),
getattr(event, 'peer_resolvable_private_address', None), getattr(event, 'peer_resolvable_private_address', None),
hci.Role(event.role), hci.Role(event.role),
event.connection_interval, connection_parameters,
event.peripheral_latency,
event.supervision_timeout,
) )
else: else:
logger.debug(f'### CONNECTION FAILED: {event.status}') logger.debug(f'### CONNECTION FAILED: {event.status}')
@@ -1051,9 +1060,14 @@ class Host(utils.EventEmitter):
# Notify the client # Notify the client
self.emit( self.emit(
'classic_connection', 'connection',
event.connection_handle, event.connection_handle,
PhysicalTransport.BR_EDR,
event.bd_addr, event.bd_addr,
None,
None,
None,
None,
) )
else: else:
logger.debug(f'### BR/EDR CONNECTION FAILED: {event.status}') logger.debug(f'### BR/EDR CONNECTION FAILED: {event.status}')
@@ -1116,13 +1130,14 @@ class Host(utils.EventEmitter):
# Notify the client # Notify the client
if event.status == hci.HCI_SUCCESS: if event.status == hci.HCI_SUCCESS:
self.emit( connection_parameters = ConnectionParameters(
'connection_parameters_update',
connection.handle,
event.connection_interval, event.connection_interval,
event.peripheral_latency, event.peripheral_latency,
event.supervision_timeout, event.supervision_timeout,
) )
self.emit(
'connection_parameters_update', connection.handle, connection_parameters
)
else: else:
self.emit( self.emit(
'connection_parameters_update_failure', connection.handle, event.status 'connection_parameters_update_failure', connection.handle, event.status
+4 -33
View File
@@ -273,19 +273,12 @@ class HearingAccessService(gatt.TemplateService):
def on_disconnection(_reason) -> None: def on_disconnection(_reason) -> None:
self.currently_connected_clients.discard(connection) 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) @connection.on(connection.EVENT_PAIRING)
def on_pairing(*_: Any) -> None: def on_pairing(*_: Any) -> None:
self.on_incoming_connection(connection) self.on_incoming_paired_connection(connection)
self.on_incoming_connection(connection) if connection.peer_resolvable_address:
self.on_incoming_paired_connection(connection)
self.hearing_aid_features_characteristic = gatt.Characteristic( self.hearing_aid_features_characteristic = gatt.Characteristic(
uuid=gatt.GATT_HEARING_AID_FEATURES_CHARACTERISTIC, uuid=gatt.GATT_HEARING_AID_FEATURES_CHARACTERISTIC,
@@ -322,30 +315,9 @@ class HearingAccessService(gatt.TemplateService):
] ]
) )
def on_incoming_connection(self, connection: Connection): def on_incoming_paired_connection(self, connection: Connection):
'''Setup initial operations to handle a remote bonded HAP device''' '''Setup initial operations to handle a remote bonded HAP device'''
# TODO Should we filter on HAP device only ? # 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) self.currently_connected_clients.add(connection)
if ( if (
connection.peer_address connection.peer_address
@@ -485,7 +457,6 @@ class HearingAccessService(gatt.TemplateService):
connection, connection,
self.hearing_aid_preset_control_point, self.hearing_aid_preset_control_point,
value=op_list[0].to_bytes(len(op_list) == 1), 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 # Remove item once sent, and keep the non sent item in the list
op_list.pop(0) op_list.pop(0)
+10 -11
View File
@@ -12,12 +12,13 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import logging
import os
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import logging from contextlib import asynccontextmanager
import os
import re
from typing import Optional from typing import Optional
from bumble import utils from bumble import utils
@@ -84,14 +85,12 @@ async def open_transport(name: str) -> Transport:
scheme, *tail = name.split(':', 1) scheme, *tail = name.split(':', 1)
spec = tail[0] if tail else None spec = tail[0] if tail else None
metadata = None metadata = None
if spec and (m := re.search(r'\[(\w+=\w+(?:,\w+=\w+)*,?)\]', spec)): if spec:
metadata_str = m.group(1) # Metadata may precede the spec
if m.start() == 0: if spec.startswith('['):
# <metadata><spec> metadata_str, *tail = spec[1:].split(']')
spec = spec[m.end() :] spec = tail[0] if tail else None
else: metadata = dict([entry.split('=') for entry in metadata_str.split(',')])
spec = spec[: m.start()]
metadata = dict([entry.split('=') for entry in metadata_str.split(',')])
transport = await _open_transport(scheme, spec) transport = await _open_transport(scheme, spec)
if metadata: if metadata:
+1 -5
View File
@@ -131,11 +131,7 @@ def publish_grpc_port(grpc_port: int, instance_number: int) -> bool:
def cleanup(): def cleanup():
logger.debug("removing .ini file") logger.debug("removing .ini file")
try: ini_file.unlink()
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) atexit.register(cleanup)
return True return True
+2 -56
View File
@@ -17,7 +17,6 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import asyncio import asyncio
import logging import logging
from typing import Optional
import serial_asyncio import serial_asyncio
@@ -29,56 +28,25 @@ from bumble.transport.common import StreamPacketSink, StreamPacketSource, Transp
logger = logging.getLogger(__name__) 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: async def open_serial_transport(spec: str) -> Transport:
''' '''
Open a serial port transport. Open a serial port transport.
The parameter string has this syntax: The parameter string has this syntax:
<device-path>[,<speed>][,rtscts][,dsrdtr][,delay] <device-path>[,<speed>][,rtscts][,dsrdtr]
When <speed> is omitted, the default value of 1000000 is used When <speed> is omitted, the default value of 1000000 is used
When "rtscts" is specified, RTS/CTS hardware flow control is enabled When "rtscts" is specified, RTS/CTS hardware flow control is enabled
When "dsrdtr" is specified, DSR/DTR 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: Examples:
/dev/tty.usbmodem0006839912172 /dev/tty.usbmodem0006839912172
/dev/tty.usbmodem0006839912172,1000000 /dev/tty.usbmodem0006839912172,1000000
/dev/tty.usbmodem0006839912172,rtscts /dev/tty.usbmodem0006839912172,rtscts
/dev/tty.usbmodem0006839912172,rtscts,delay
''' '''
speed = 1000000 speed = 1000000
rtscts = False rtscts = False
dsrdtr = False dsrdtr = False
delay = 0.0
if ',' in spec: if ',' in spec:
parts = spec.split(',') parts = spec.split(',')
device = parts[0] device = parts[0]
@@ -87,16 +55,13 @@ async def open_serial_transport(spec: str) -> Transport:
rtscts = True rtscts = True
elif part == 'dsrdtr': elif part == 'dsrdtr':
dsrdtr = True dsrdtr = True
elif part == 'delay':
delay = DEFAULT_POST_OPEN_DELAY
elif part.isnumeric(): elif part.isnumeric():
speed = int(part) speed = int(part)
else: else:
device = spec device = spec
serial_transport, packet_source = await serial_asyncio.create_serial_connection( serial_transport, packet_source = await serial_asyncio.create_serial_connection(
asyncio.get_running_loop(), asyncio.get_running_loop(),
SerialPacketSource, StreamPacketSource,
device, device,
baudrate=speed, baudrate=speed,
rtscts=rtscts, rtscts=rtscts,
@@ -104,23 +69,4 @@ async def open_serial_transport(spec: str) -> Transport:
) )
packet_sink = StreamPacketSink(serial_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) return Transport(packet_source, packet_sink)
+4 -13
View File
@@ -4,18 +4,9 @@ SERIAL TRANSPORT
The serial transport implements sending/receiving HCI packets over a UART (a.k.a serial port). The serial transport implements sending/receiving HCI packets over a UART (a.k.a serial port).
## Moniker ## Moniker
The moniker syntax for a serial transport is: The moniker syntax for a serial transport is: `serial:<device-path>[,<speed>]`
`<device-path>[,<speed>][,rtscts][,dsrdtr][,delay]` When `<speed>` is omitted, the default value of 1000000 is used
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 !!! example
``` `serial:/dev/tty.usbmodem0006839912172,1000000`
/dev/tty.usbmodem0006839912172 Opens the serial port `/dev/tty.usbmodem0006839912172` at `1000000`bps
/dev/tty.usbmodem0006839912172,1000000
/dev/tty.usbmodem0006839912172,rtscts
/dev/tty.usbmodem0006839912172,rtscts,delay
```
+56 -140
View File
@@ -61,7 +61,7 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
dependencies = [ dependencies = [
"windows-sys 0.48.0", "windows-sys",
] ]
[[package]] [[package]]
@@ -71,7 +71,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"windows-sys 0.48.0", "windows-sys",
] ]
[[package]] [[package]]
@@ -249,7 +249,7 @@ dependencies = [
"atty", "atty",
"bitflags 1.3.2", "bitflags 1.3.2",
"clap_lex 0.2.4", "clap_lex 0.2.4",
"indexmap 1.9.3", "indexmap",
"strsim", "strsim",
"termcolor", "termcolor",
"textwrap", "textwrap",
@@ -451,7 +451,7 @@ dependencies = [
"libc", "libc",
"option-ext", "option-ext",
"redox_users", "redox_users",
"windows-sys 0.48.0", "windows-sys",
] ]
[[package]] [[package]]
@@ -483,19 +483,24 @@ dependencies = [
] ]
[[package]] [[package]]
name = "equivalent" name = "errno"
version = "1.0.2" version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
dependencies = [
"errno-dragonfly",
"libc",
"windows-sys",
]
[[package]] [[package]]
name = "errno" name = "errno-dragonfly"
version = "0.3.14" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [ dependencies = [
"cc",
"libc", "libc",
"windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -678,9 +683,9 @@ dependencies = [
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.27" version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
@@ -688,7 +693,7 @@ dependencies = [
"futures-sink", "futures-sink",
"futures-util", "futures-util",
"http", "http",
"indexmap 2.11.3", "indexmap",
"slab", "slab",
"tokio", "tokio",
"tokio-util", "tokio-util",
@@ -701,12 +706,6 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.4.1" version = "0.4.1"
@@ -828,17 +827,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown 0.12.3", "hashbrown",
]
[[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]] [[package]]
@@ -867,7 +856,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [ dependencies = [
"hermit-abi 0.3.2", "hermit-abi 0.3.2",
"rustix", "rustix",
"windows-sys 0.48.0", "windows-sys",
] ]
[[package]] [[package]]
@@ -902,9 +891,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.175" version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]] [[package]]
name = "libusb1-sys" name = "libusb1-sys"
@@ -931,9 +920,9 @@ dependencies = [
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.15" version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
@@ -1007,13 +996,13 @@ dependencies = [
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.8.11" version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [ dependencies = [
"libc", "libc",
"wasi", "wasi",
"windows-sys 0.48.0", "windows-sys",
] ]
[[package]] [[package]]
@@ -1084,9 +1073,9 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.73" version = "0.10.60"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800"
dependencies = [ dependencies = [
"bitflags 2.4.0", "bitflags 2.4.0",
"cfg-if", "cfg-if",
@@ -1116,9 +1105,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.109" version = "0.9.96"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@@ -1164,7 +1153,7 @@ dependencies = [
"libc", "libc",
"redox_syscall 0.3.5", "redox_syscall 0.3.5",
"smallvec", "smallvec",
"windows-targets 0.48.5", "windows-targets",
] ]
[[package]] [[package]]
@@ -1553,15 +1542,15 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.44" version = "0.38.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" checksum = "ed6248e1caa625eb708e266e06159f135e8c26f2bb7ceb72dc4b2766d0340964"
dependencies = [ dependencies = [
"bitflags 2.4.0", "bitflags 2.4.0",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.52.0", "windows-sys",
] ]
[[package]] [[package]]
@@ -1591,7 +1580,7 @@ version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
dependencies = [ dependencies = [
"windows-sys 0.48.0", "windows-sys",
] ]
[[package]] [[package]]
@@ -1713,12 +1702,12 @@ dependencies = [
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.5.10" version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.52.0", "windows-sys",
] ]
[[package]] [[package]]
@@ -1784,7 +1773,7 @@ dependencies = [
"fastrand", "fastrand",
"redox_syscall 0.3.5", "redox_syscall 0.3.5",
"rustix", "rustix",
"windows-sys 0.48.0", "windows-sys",
] ]
[[package]] [[package]]
@@ -1839,9 +1828,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.38.2" version = "1.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68722da18b0fc4a05fdc1120b302b82051265792a1e1b399086e9b204b10ad3d" checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@@ -1851,16 +1840,16 @@ dependencies = [
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"socket2 0.5.10", "socket2 0.5.3",
"tokio-macros", "tokio-macros",
"windows-sys 0.48.0", "windows-sys",
] ]
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "2.3.0" version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2141,16 +2130,7 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [ dependencies = [
"windows-targets 0.48.5", "windows-targets",
]
[[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]] [[package]]
@@ -2159,29 +2139,13 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [ dependencies = [
"windows_aarch64_gnullvm 0.48.5", "windows_aarch64_gnullvm",
"windows_aarch64_msvc 0.48.5", "windows_aarch64_msvc",
"windows_i686_gnu 0.48.5", "windows_i686_gnu",
"windows_i686_msvc 0.48.5", "windows_i686_msvc",
"windows_x86_64_gnu 0.48.5", "windows_x86_64_gnu",
"windows_x86_64_gnullvm 0.48.5", "windows_x86_64_gnullvm",
"windows_x86_64_msvc 0.48.5", "windows_x86_64_msvc",
]
[[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]] [[package]]
@@ -2190,90 +2154,42 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 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]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 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]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 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]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 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]] [[package]]
name = "winreg" name = "winreg"
version = "0.50.0" version = "0.50.0"
@@ -2281,5 +2197,5 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-sys 0.48.0", "windows-sys",
] ]
+3 -3
View File
@@ -10,7 +10,7 @@ documentation = "https://docs.rs/crate/bumble"
authors = ["Marshall Pierce <marshallpierce@google.com>"] authors = ["Marshall Pierce <marshallpierce@google.com>"]
keywords = ["bluetooth", "ble"] keywords = ["bluetooth", "ble"]
categories = ["api-bindings", "network-programming"] categories = ["api-bindings", "network-programming"]
rust-version = "1.80.0" rust-version = "1.76.0"
# https://github.com/frewsxcv/cargo-all-features#options # https://github.com/frewsxcv/cargo-all-features#options
[package.metadata.cargo-all-features] [package.metadata.cargo-all-features]
@@ -22,7 +22,7 @@ always_include_features = ["anyhow", "pyo3-asyncio-attributes", "dev-tools", "bu
[dependencies] [dependencies]
pyo3 = { version = "0.18.3", features = ["macros"] } pyo3 = { version = "0.18.3", features = ["macros"] }
pyo3-asyncio = { version = "0.18.0", features = ["tokio-runtime"] } pyo3-asyncio = { version = "0.18.0", features = ["tokio-runtime"] }
tokio = { version = "1.38.2", features = ["macros", "signal"] } tokio = { version = "1.28.2", features = ["macros", "signal"] }
nom = "7.1.3" nom = "7.1.3"
strum = "0.25.0" strum = "0.25.0"
strum_macros = "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 } rusb = { version = "0.9.2", optional = true }
[dev-dependencies] [dev-dependencies]
tokio = { version = "1.38.2", features = ["full"] } tokio = { version = "1.28.2", features = ["full"] }
tempfile = "3.6.0" tempfile = "3.6.0"
nix = "0.26.2" nix = "0.26.2"
anyhow = "1.0.71" anyhow = "1.0.71"
+84 -242
View File
@@ -18,12 +18,14 @@
import asyncio import asyncio
import logging import logging
import os import os
import struct
from typing import Awaitable
import pytest import pytest
from bumble import a2dp from bumble.a2dp import (
AacMediaCodecInformation,
OpusMediaCodecInformation,
SbcMediaCodecInformation,
)
from bumble.avdtp import ( from bumble.avdtp import (
A2DP_SBC_CODEC_TYPE, A2DP_SBC_CODEC_TYPE,
AVDTP_AUDIO_MEDIA_TYPE, AVDTP_AUDIO_MEDIA_TYPE,
@@ -80,24 +82,6 @@ class TwoDevices:
self.paired[which] = keys 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 @pytest.mark.asyncio
async def test_self_connection(): async def test_self_connection():
@@ -138,12 +122,12 @@ def source_codec_capabilities():
return MediaCodecCapabilities( return MediaCodecCapabilities(
media_type=AVDTP_AUDIO_MEDIA_TYPE, media_type=AVDTP_AUDIO_MEDIA_TYPE,
media_codec_type=A2DP_SBC_CODEC_TYPE, media_codec_type=A2DP_SBC_CODEC_TYPE,
media_codec_information=a2dp.SbcMediaCodecInformation( media_codec_information=SbcMediaCodecInformation(
sampling_frequency=a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_44100, sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_44100,
channel_mode=a2dp.SbcMediaCodecInformation.ChannelMode.JOINT_STEREO, channel_mode=SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
block_length=a2dp.SbcMediaCodecInformation.BlockLength.BL_16, block_length=SbcMediaCodecInformation.BlockLength.BL_16,
subbands=a2dp.SbcMediaCodecInformation.Subbands.S_8, subbands=SbcMediaCodecInformation.Subbands.S_8,
allocation_method=a2dp.SbcMediaCodecInformation.AllocationMethod.LOUDNESS, allocation_method=SbcMediaCodecInformation.AllocationMethod.LOUDNESS,
minimum_bitpool_value=2, minimum_bitpool_value=2,
maximum_bitpool_value=53, maximum_bitpool_value=53,
), ),
@@ -155,23 +139,23 @@ def sink_codec_capabilities():
return MediaCodecCapabilities( return MediaCodecCapabilities(
media_type=AVDTP_AUDIO_MEDIA_TYPE, media_type=AVDTP_AUDIO_MEDIA_TYPE,
media_codec_type=A2DP_SBC_CODEC_TYPE, media_codec_type=A2DP_SBC_CODEC_TYPE,
media_codec_information=a2dp.SbcMediaCodecInformation( media_codec_information=SbcMediaCodecInformation(
sampling_frequency=a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_48000 sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_48000
| a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_44100 | SbcMediaCodecInformation.SamplingFrequency.SF_44100
| a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_32000 | SbcMediaCodecInformation.SamplingFrequency.SF_32000
| a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_16000, | SbcMediaCodecInformation.SamplingFrequency.SF_16000,
channel_mode=a2dp.SbcMediaCodecInformation.ChannelMode.MONO channel_mode=SbcMediaCodecInformation.ChannelMode.MONO
| a2dp.SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL | SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
| a2dp.SbcMediaCodecInformation.ChannelMode.STEREO | SbcMediaCodecInformation.ChannelMode.STEREO
| a2dp.SbcMediaCodecInformation.ChannelMode.JOINT_STEREO, | SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
block_length=a2dp.SbcMediaCodecInformation.BlockLength.BL_4 block_length=SbcMediaCodecInformation.BlockLength.BL_4
| a2dp.SbcMediaCodecInformation.BlockLength.BL_8 | SbcMediaCodecInformation.BlockLength.BL_8
| a2dp.SbcMediaCodecInformation.BlockLength.BL_12 | SbcMediaCodecInformation.BlockLength.BL_12
| a2dp.SbcMediaCodecInformation.BlockLength.BL_16, | SbcMediaCodecInformation.BlockLength.BL_16,
subbands=a2dp.SbcMediaCodecInformation.Subbands.S_4 subbands=SbcMediaCodecInformation.Subbands.S_4
| a2dp.SbcMediaCodecInformation.Subbands.S_8, | SbcMediaCodecInformation.Subbands.S_8,
allocation_method=a2dp.SbcMediaCodecInformation.AllocationMethod.LOUDNESS allocation_method=SbcMediaCodecInformation.AllocationMethod.LOUDNESS
| a2dp.SbcMediaCodecInformation.AllocationMethod.SNR, | SbcMediaCodecInformation.AllocationMethod.SNR,
minimum_bitpool_value=2, minimum_bitpool_value=2,
maximum_bitpool_value=53, maximum_bitpool_value=53,
), ),
@@ -290,54 +274,52 @@ async def test_source_sink_1():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_sbc_codec_specific_information(): def test_sbc_codec_specific_information():
sbc_info = a2dp.SbcMediaCodecInformation.from_bytes(bytes.fromhex("3fff0235")) sbc_info = SbcMediaCodecInformation.from_bytes(bytes.fromhex("3fff0235"))
assert ( assert (
sbc_info.sampling_frequency sbc_info.sampling_frequency
== a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_44100 == SbcMediaCodecInformation.SamplingFrequency.SF_44100
| a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_48000 | SbcMediaCodecInformation.SamplingFrequency.SF_48000
) )
assert ( assert (
sbc_info.channel_mode sbc_info.channel_mode
== a2dp.SbcMediaCodecInformation.ChannelMode.MONO == SbcMediaCodecInformation.ChannelMode.MONO
| a2dp.SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL | SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
| a2dp.SbcMediaCodecInformation.ChannelMode.STEREO | SbcMediaCodecInformation.ChannelMode.STEREO
| a2dp.SbcMediaCodecInformation.ChannelMode.JOINT_STEREO | SbcMediaCodecInformation.ChannelMode.JOINT_STEREO
) )
assert ( assert (
sbc_info.block_length sbc_info.block_length
== a2dp.SbcMediaCodecInformation.BlockLength.BL_4 == SbcMediaCodecInformation.BlockLength.BL_4
| a2dp.SbcMediaCodecInformation.BlockLength.BL_8 | SbcMediaCodecInformation.BlockLength.BL_8
| a2dp.SbcMediaCodecInformation.BlockLength.BL_12 | SbcMediaCodecInformation.BlockLength.BL_12
| a2dp.SbcMediaCodecInformation.BlockLength.BL_16 | SbcMediaCodecInformation.BlockLength.BL_16
) )
assert ( assert (
sbc_info.subbands sbc_info.subbands
== a2dp.SbcMediaCodecInformation.Subbands.S_4 == SbcMediaCodecInformation.Subbands.S_4 | SbcMediaCodecInformation.Subbands.S_8
| a2dp.SbcMediaCodecInformation.Subbands.S_8
) )
assert ( assert (
sbc_info.allocation_method sbc_info.allocation_method
== a2dp.SbcMediaCodecInformation.AllocationMethod.SNR == SbcMediaCodecInformation.AllocationMethod.SNR
| a2dp.SbcMediaCodecInformation.AllocationMethod.LOUDNESS | SbcMediaCodecInformation.AllocationMethod.LOUDNESS
) )
assert sbc_info.minimum_bitpool_value == 2 assert sbc_info.minimum_bitpool_value == 2
assert sbc_info.maximum_bitpool_value == 53 assert sbc_info.maximum_bitpool_value == 53
sbc_info2 = a2dp.SbcMediaCodecInformation( sbc_info2 = SbcMediaCodecInformation(
a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_44100 SbcMediaCodecInformation.SamplingFrequency.SF_44100
| a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_48000, | SbcMediaCodecInformation.SamplingFrequency.SF_48000,
a2dp.SbcMediaCodecInformation.ChannelMode.MONO SbcMediaCodecInformation.ChannelMode.MONO
| a2dp.SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL | SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
| a2dp.SbcMediaCodecInformation.ChannelMode.STEREO | SbcMediaCodecInformation.ChannelMode.STEREO
| a2dp.SbcMediaCodecInformation.ChannelMode.JOINT_STEREO, | SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
a2dp.SbcMediaCodecInformation.BlockLength.BL_4 SbcMediaCodecInformation.BlockLength.BL_4
| a2dp.SbcMediaCodecInformation.BlockLength.BL_8 | SbcMediaCodecInformation.BlockLength.BL_8
| a2dp.SbcMediaCodecInformation.BlockLength.BL_12 | SbcMediaCodecInformation.BlockLength.BL_12
| a2dp.SbcMediaCodecInformation.BlockLength.BL_16, | SbcMediaCodecInformation.BlockLength.BL_16,
a2dp.SbcMediaCodecInformation.Subbands.S_4 SbcMediaCodecInformation.Subbands.S_4 | SbcMediaCodecInformation.Subbands.S_8,
| a2dp.SbcMediaCodecInformation.Subbands.S_8, SbcMediaCodecInformation.AllocationMethod.SNR
a2dp.SbcMediaCodecInformation.AllocationMethod.SNR | SbcMediaCodecInformation.AllocationMethod.LOUDNESS,
| a2dp.SbcMediaCodecInformation.AllocationMethod.LOUDNESS,
2, 2,
53, 53,
) )
@@ -347,36 +329,36 @@ def test_sbc_codec_specific_information():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_aac_codec_specific_information(): def test_aac_codec_specific_information():
aac_info = a2dp.AacMediaCodecInformation.from_bytes(bytes.fromhex("f0018c83e800")) aac_info = AacMediaCodecInformation.from_bytes(bytes.fromhex("f0018c83e800"))
assert ( assert (
aac_info.object_type aac_info.object_type
== a2dp.AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC == AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC
| a2dp.AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LC | AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LC
| a2dp.AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LTP | AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LTP
| a2dp.AacMediaCodecInformation.ObjectType.MPEG_4_AAC_SCALABLE | AacMediaCodecInformation.ObjectType.MPEG_4_AAC_SCALABLE
) )
assert ( assert (
aac_info.sampling_frequency aac_info.sampling_frequency
== a2dp.AacMediaCodecInformation.SamplingFrequency.SF_44100 == AacMediaCodecInformation.SamplingFrequency.SF_44100
| a2dp.AacMediaCodecInformation.SamplingFrequency.SF_48000 | AacMediaCodecInformation.SamplingFrequency.SF_48000
) )
assert ( assert (
aac_info.channels aac_info.channels
== a2dp.AacMediaCodecInformation.Channels.MONO == AacMediaCodecInformation.Channels.MONO
| a2dp.AacMediaCodecInformation.Channels.STEREO | AacMediaCodecInformation.Channels.STEREO
) )
assert aac_info.vbr == 1 assert aac_info.vbr == 1
assert aac_info.bitrate == 256000 assert aac_info.bitrate == 256000
aac_info2 = a2dp.AacMediaCodecInformation( aac_info2 = AacMediaCodecInformation(
a2dp.AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC
| a2dp.AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LC | AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LC
| a2dp.AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LTP | AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LTP
| a2dp.AacMediaCodecInformation.ObjectType.MPEG_4_AAC_SCALABLE, | AacMediaCodecInformation.ObjectType.MPEG_4_AAC_SCALABLE,
a2dp.AacMediaCodecInformation.SamplingFrequency.SF_44100 AacMediaCodecInformation.SamplingFrequency.SF_44100
| a2dp.AacMediaCodecInformation.SamplingFrequency.SF_48000, | AacMediaCodecInformation.SamplingFrequency.SF_48000,
a2dp.AacMediaCodecInformation.Channels.MONO AacMediaCodecInformation.Channels.MONO
| a2dp.AacMediaCodecInformation.Channels.STEREO, | AacMediaCodecInformation.Channels.STEREO,
1, 1,
256000, 256000,
) )
@@ -386,159 +368,25 @@ def test_aac_codec_specific_information():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_opus_codec_specific_information(): def test_opus_codec_specific_information():
opus_info = a2dp.OpusMediaCodecInformation.from_bytes(bytes([0x92])) opus_info = OpusMediaCodecInformation.from_bytes(bytes([0x92]))
assert opus_info.vendor_id == a2dp.OpusMediaCodecInformation.VENDOR_ID assert opus_info.vendor_id == OpusMediaCodecInformation.VENDOR_ID
assert opus_info.codec_id == a2dp.OpusMediaCodecInformation.CODEC_ID assert opus_info.codec_id == OpusMediaCodecInformation.CODEC_ID
assert opus_info.frame_size == a2dp.OpusMediaCodecInformation.FrameSize.FS_20MS assert opus_info.frame_size == OpusMediaCodecInformation.FrameSize.FS_20MS
assert opus_info.channel_mode == a2dp.OpusMediaCodecInformation.ChannelMode.STEREO assert opus_info.channel_mode == OpusMediaCodecInformation.ChannelMode.STEREO
assert ( assert (
opus_info.sampling_frequency opus_info.sampling_frequency
== a2dp.OpusMediaCodecInformation.SamplingFrequency.SF_48000 == OpusMediaCodecInformation.SamplingFrequency.SF_48000
) )
opus_info2 = a2dp.OpusMediaCodecInformation( opus_info2 = OpusMediaCodecInformation(
a2dp.OpusMediaCodecInformation.ChannelMode.STEREO, OpusMediaCodecInformation.ChannelMode.STEREO,
a2dp.OpusMediaCodecInformation.FrameSize.FS_20MS, OpusMediaCodecInformation.FrameSize.FS_20MS,
a2dp.OpusMediaCodecInformation.SamplingFrequency.SF_48000, OpusMediaCodecInformation.SamplingFrequency.SF_48000,
) )
assert opus_info2 == opus_info assert opus_info2 == opus_info
assert opus_info2.value == bytes([0x92]) 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(): async def async_main():
test_sbc_codec_specific_information() test_sbc_codec_specific_information()
@@ -546,12 +394,6 @@ async def async_main():
test_opus_codec_specific_information() test_opus_codec_specific_information()
await test_self_connection() await test_self_connection()
await test_source_sink_1() 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()
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
+21 -15
View File
@@ -310,12 +310,12 @@ async def test_pacs():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_ascs(): async def test_ascs():
devices = TwoDevices() devices = TwoDevices()
devices[1].add_service( devices[0].add_service(
AudioStreamControlService(device=devices[1], sink_ase_id=[1, 2]) AudioStreamControlService(device=devices[0], sink_ase_id=[1, 2])
) )
await devices.setup_connection() await devices.setup_connection()
peer = device.Peer(devices.connections[0]) peer = device.Peer(devices.connections[1])
ascs_client = await peer.discover_service_and_create_proxy( ascs_client = await peer.discover_service_and_create_proxy(
AudioStreamControlServiceProxy AudioStreamControlServiceProxy
) )
@@ -369,7 +369,7 @@ async def test_ascs():
await ascs_client.ase_control_point.write_value( await ascs_client.ase_control_point.write_value(
ASE_Config_QOS( ASE_Config_QOS(
ase_id=[1, 2], ase_id=[1, 2],
cig_id=[1, 1], cig_id=[1, 2],
cis_id=[3, 4], cis_id=[3, 4],
sdu_interval=[5, 6], sdu_interval=[5, 6],
framing=[0, 1], framing=[0, 1],
@@ -402,19 +402,25 @@ async def test_ascs():
) )
# CIS establishment # CIS establishment
cis_handles = await devices[0].setup_cig( devices[0].emit(
device.CigParameters( 'cis_establishment',
device.CisLink(
device=devices[0],
acl_connection=devices.connections[0],
handle=5,
cis_id=3,
cig_id=1, 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,
)
) )
await devices[0].create_cis( devices[0].emit(
[(cis_handle, devices.connections[0]) for cis_handle in cis_handles] 'cis_establishment',
device.CisLink(
device=devices[0],
acl_connection=devices.connections[0],
handle=6,
cis_id=4,
cig_id=2,
),
) )
assert (await notifications[1].get())[:2] == bytes( assert (await notifications[1].get())[:2] == bytes(
[1, AseStateMachine.State.STREAMING] [1, AseStateMachine.State.STREAMING]
+10 -41
View File
@@ -24,7 +24,7 @@ from unittest import mock
import pytest import pytest
from bumble import device, gatt, hci, utils from bumble import device, gatt, hci, utils
from bumble.core import PhysicalTransport from bumble.core import ConnectionParameters, PhysicalTransport
from bumble.device import ( from bumble.device import (
AdvertisingEventProperties, AdvertisingEventProperties,
AdvertisingParameters, AdvertisingParameters,
@@ -289,15 +289,14 @@ async def test_legacy_advertising_disconnection(auto_restart):
await device.power_on() await device.power_on()
peer_address = Address('F0:F1:F2:F3:F4:F5') peer_address = Address('F0:F1:F2:F3:F4:F5')
await device.start_advertising(auto_restart=auto_restart) await device.start_advertising(auto_restart=auto_restart)
device.on_le_connection( device.on_connection(
0x0001, 0x0001,
PhysicalTransport.LE,
peer_address, peer_address,
None, None,
None, None,
Role.PERIPHERAL, Role.PERIPHERAL,
0, ConnectionParameters(0, 0, 0),
0,
0,
) )
device.on_advertising_set_termination( device.on_advertising_set_termination(
@@ -348,15 +347,14 @@ async def test_extended_advertising_connection(own_address_type):
advertising_set = await device.create_advertising_set( advertising_set = await device.create_advertising_set(
advertising_parameters=AdvertisingParameters(own_address_type=own_address_type) advertising_parameters=AdvertisingParameters(own_address_type=own_address_type)
) )
device.on_le_connection( device.on_connection(
0x0001, 0x0001,
PhysicalTransport.LE,
peer_address, peer_address,
None, None,
None, None,
Role.PERIPHERAL, Role.PERIPHERAL,
0, ConnectionParameters(0, 0, 0),
0,
0,
) )
device.on_advertising_set_termination( device.on_advertising_set_termination(
HCI_SUCCESS, HCI_SUCCESS,
@@ -393,15 +391,14 @@ async def test_extended_advertising_connection_out_of_order(own_address_type):
0x0001, 0x0001,
0, 0,
) )
device.on_le_connection( device.on_connection(
0x0001, 0x0001,
PhysicalTransport.LE,
Address('F0:F1:F2:F3:F4:F5'), Address('F0:F1:F2:F3:F4:F5'),
None, None,
None, None,
Role.PERIPHERAL, Role.PERIPHERAL,
0, ConnectionParameters(0, 0, 0),
0,
0,
) )
if own_address_type == OwnAddressType.PUBLIC: if own_address_type == OwnAddressType.PUBLIC:
@@ -761,34 +758,6 @@ async def test_inquiry_result_with_rssi():
m.assert_called_with(hci.Address("00:11:22:33:44:55/P"), 3, mock.ANY, 5) 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(): async def run_test_device():
await test_device_connect_parallel() await test_device_connect_parallel()
+1 -3
View File
@@ -82,6 +82,7 @@ async def hap_client():
) )
await devices.setup_connection() await devices.setup_connection()
# TODO negotiate MTU > 49 to not truncate preset names
# Mock encryption. # Mock encryption.
devices.connections[0].encryption = 1 # type: ignore devices.connections[0].encryption = 1 # type: ignore
@@ -92,9 +93,6 @@ async def hap_client():
) )
peer = device.Peer(devices.connections[1]) # type: ignore 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_client = await peer.discover_service_and_create_proxy(
hap.HearingAccessServiceProxy hap.HearingAccessServiceProxy
) )
+5 -32
View File
@@ -24,7 +24,7 @@ import sys
import pytest import pytest
from bumble import controller, device, hci, link, transport from bumble import controller, device, hci, link, transport
from bumble.transport import common from bumble.transport.common import PacketParser
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -61,9 +61,9 @@ class Sink:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_parser(): def test_parser():
sink1 = Sink() sink1 = Sink()
parser1 = common.PacketParser(sink1) parser1 = PacketParser(sink1)
sink2 = Sink() sink2 = Sink()
parser2 = common.PacketParser(sink2) parser2 = PacketParser(sink2)
for parser in [parser1, parser2]: for parser in [parser1, parser2]:
with open( with open(
@@ -82,7 +82,7 @@ def test_parser():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_parser_extensions(): def test_parser_extensions():
sink = Sink() sink = Sink()
parser = common.PacketParser(sink) parser = PacketParser(sink)
# Check that an exception is thrown for an unknown type # Check that an exception is thrown for an unknown type
try: try:
@@ -206,7 +206,7 @@ async def test_unix_connection_abstract():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@pytest.mark.parametrize( @pytest.mark.parametrize(
"address,", "address,",
("127.0.0.1", "[::1]"), ("127.0.0.1",),
) )
async def test_android_netsim_connection(address): async def test_android_netsim_connection(address):
controller_transport = await transport.open_transport( controller_transport = await transport.open_transport(
@@ -222,33 +222,6 @@ async def test_android_netsim_connection(address):
await client_device.power_on() await client_device.power_on()
await client_transport.close() 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() await controller_transport.close()