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
43 changed files with 1299 additions and 2169 deletions
+2 -2
View File
@@ -49,7 +49,7 @@ jobs:
strategy:
matrix:
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
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
run: cd rust && cargo build --all-targets && cargo build-all-features --all-targets
# 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
+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.
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
+17 -56
View File
@@ -21,12 +21,11 @@ import dataclasses
import enum
import logging
import struct
from collections.abc import AsyncGenerator, Awaitable, Callable
from typing import Union
from collections.abc import AsyncGenerator
from typing import Awaitable, Callable
from typing_extensions import ClassVar, Self
from bumble import utils
from bumble.codecs import AacAudioRtpPacket
from bumble.company_ids import COMPANY_IDENTIFIERS
from bumble.core import (
@@ -60,18 +59,19 @@ logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
# fmt: off
class CodecType(utils.OpenIntEnum):
SBC = 0x00
MPEG_1_2_AUDIO = 0x01
MPEG_2_4_AAC = 0x02
ATRAC_FAMILY = 0x03
NON_A2DP = 0xFF
A2DP_SBC_CODEC_TYPE = 0x00
A2DP_MPEG_1_2_AUDIO_CODEC_TYPE = 0x01
A2DP_MPEG_2_4_AAC_CODEC_TYPE = 0x02
A2DP_ATRAC_FAMILY_CODEC_TYPE = 0x03
A2DP_NON_A2DP_CODEC_TYPE = 0xFF
A2DP_SBC_CODEC_TYPE = CodecType.SBC
A2DP_MPEG_1_2_AUDIO_CODEC_TYPE = CodecType.MPEG_1_2_AUDIO
A2DP_MPEG_2_4_AAC_CODEC_TYPE = CodecType.MPEG_2_4_AAC
A2DP_ATRAC_FAMILY_CODEC_TYPE = CodecType.ATRAC_FAMILY
A2DP_NON_A2DP_CODEC_TYPE = CodecType.NON_A2DP
A2DP_CODEC_TYPE_NAMES = {
A2DP_SBC_CODEC_TYPE: 'A2DP_SBC_CODEC_TYPE',
A2DP_MPEG_1_2_AUDIO_CODEC_TYPE: 'A2DP_MPEG_1_2_AUDIO_CODEC_TYPE',
A2DP_MPEG_2_4_AAC_CODEC_TYPE: 'A2DP_MPEG_2_4_AAC_CODEC_TYPE',
A2DP_ATRAC_FAMILY_CODEC_TYPE: 'A2DP_ATRAC_FAMILY_CODEC_TYPE',
A2DP_NON_A2DP_CODEC_TYPE: 'A2DP_NON_A2DP_CODEC_TYPE'
}
SBC_SYNC_WORD = 0x9C
@@ -259,48 +259,9 @@ def make_audio_sink_service_sdp_records(service_record_handle, version=(1, 3)):
]
# -----------------------------------------------------------------------------
class MediaCodecInformation:
'''Base Media Codec Information.'''
@classmethod
def create(
cls, media_codec_type: int, data: bytes
) -> Union[MediaCodecInformation, bytes]:
if media_codec_type == CodecType.SBC:
return SbcMediaCodecInformation.from_bytes(data)
elif media_codec_type == CodecType.MPEG_2_4_AAC:
return AacMediaCodecInformation.from_bytes(data)
elif media_codec_type == CodecType.NON_A2DP:
vendor_media_codec_information = (
VendorSpecificMediaCodecInformation.from_bytes(data)
)
if (
vendor_class_map := A2DP_VENDOR_MEDIA_CODEC_INFORMATION_CLASSES.get(
vendor_media_codec_information.vendor_id
)
) and (
media_codec_information_class := vendor_class_map.get(
vendor_media_codec_information.codec_id
)
):
return media_codec_information_class.from_bytes(
vendor_media_codec_information.value
)
return vendor_media_codec_information
@classmethod
def from_bytes(cls, data: bytes) -> Self:
del data # Unused.
raise NotImplementedError
def __bytes__(self) -> bytes:
raise NotImplementedError
# -----------------------------------------------------------------------------
@dataclasses.dataclass
class SbcMediaCodecInformation(MediaCodecInformation):
class SbcMediaCodecInformation:
'''
A2DP spec - 4.3.2 Codec Specific Information Elements
'''
@@ -384,7 +345,7 @@ class SbcMediaCodecInformation(MediaCodecInformation):
# -----------------------------------------------------------------------------
@dataclasses.dataclass
class AacMediaCodecInformation(MediaCodecInformation):
class AacMediaCodecInformation:
'''
A2DP spec - 4.5.2 Codec Specific Information Elements
'''
@@ -466,7 +427,7 @@ class AacMediaCodecInformation(MediaCodecInformation):
@dataclasses.dataclass
# -----------------------------------------------------------------------------
class VendorSpecificMediaCodecInformation(MediaCodecInformation):
class VendorSpecificMediaCodecInformation:
'''
A2DP spec - 4.7.2 Codec Specific Information Elements
'''
+12 -8
View File
@@ -19,11 +19,10 @@ from __future__ import annotations
import logging
import struct
from collections.abc import Callable
from enum import IntEnum
from typing import Optional
from typing import Callable, Optional, cast
from bumble import core, l2cap
from bumble import avc, core, l2cap
from bumble.colors import color
# -----------------------------------------------------------------------------
@@ -145,9 +144,9 @@ class MessageAssembler:
# -----------------------------------------------------------------------------
class Protocol:
CommandHandler = Callable[[int, bytes], None]
CommandHandler = Callable[[int, avc.CommandFrame], None]
command_handlers: dict[int, CommandHandler] # Command handlers, by PID
ResponseHandler = Callable[[int, Optional[bytes]], None]
ResponseHandler = Callable[[int, Optional[avc.ResponseFrame]], None]
response_handlers: dict[int, ResponseHandler] # Response handlers, by PID
next_transaction_label: int
message_assembler: MessageAssembler
@@ -205,15 +204,20 @@ class Protocol:
self.send_ipid(transaction_label, pid)
return
self.command_handlers[pid](transaction_label, payload)
command_frame = cast(avc.CommandFrame, avc.Frame.from_bytes(payload))
self.command_handlers[pid](transaction_label, command_frame)
else:
if pid not in self.response_handlers:
logger.warning(f"no response handler for PID {pid}")
return
# By convention, for an ipid, send a None payload to the response handler.
response_payload = None if ipid else payload
self.response_handlers[pid](transaction_label, response_payload)
if ipid:
response_frame = None
else:
response_frame = cast(avc.ResponseFrame, avc.Frame.from_bytes(payload))
self.response_handlers[pid](transaction_label, response_frame)
def send_message(
self,
+523 -574
View File
File diff suppressed because it is too large Load Diff
+20 -20
View File
@@ -22,9 +22,21 @@ import enum
import functools
import logging
import struct
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Sequence
from dataclasses import dataclass, field
from typing import ClassVar, Optional, SupportsBytes, TypeVar, Union
from typing import (
AsyncIterator,
Awaitable,
Callable,
ClassVar,
Iterable,
List,
Optional,
Sequence,
SupportsBytes,
TypeVar,
Union,
cast,
)
from bumble import avc, avctp, core, hci, l2cap, utils
from bumble.colors import color
@@ -1750,11 +1762,7 @@ class Protocol(utils.EventEmitter):
),
)
response = self._check_response(response_context, GetCapabilitiesResponse)
return list(
capability
for capability in response.capabilities
if isinstance(capability, EventId)
)
return cast(List[EventId], response.capabilities)
async def get_play_status(self) -> SongAndPlayStatus:
"""Get the play status of the connected peer."""
@@ -2004,12 +2012,9 @@ class Protocol(utils.EventEmitter):
self.emit(self.EVENT_STOP)
def _on_avctp_command(self, transaction_label: int, payload: bytes) -> None:
command = avc.CommandFrame.from_bytes(payload)
if not isinstance(command, avc.CommandFrame):
raise core.InvalidPacketError(
f"{command} is not a valid AV/C Command Frame"
)
def _on_avctp_command(
self, transaction_label: int, command: avc.CommandFrame
) -> None:
logger.debug(
f"<<< AVCTP Command, transaction_label={transaction_label}: " f"{command}"
)
@@ -2068,13 +2073,8 @@ class Protocol(utils.EventEmitter):
self.send_not_implemented_response(transaction_label, command)
def _on_avctp_response(
self, transaction_label: int, payload: Optional[bytes]
self, transaction_label: int, response: Optional[avc.ResponseFrame]
) -> None:
response = avc.ResponseFrame.from_bytes(payload) if payload else None
if not isinstance(response, avc.ResponseFrame):
raise core.InvalidPacketError(
f"{response} is not a valid AV/C Response Frame"
)
logger.debug(
f"<<< AVCTP Response, transaction_label={transaction_label}: {response}"
)
@@ -2391,7 +2391,7 @@ class Protocol(utils.EventEmitter):
effective_volume = await self.delegate.get_absolute_volume()
self.send_avrcp_response(
transaction_label,
avc.ResponseFrame.ResponseCode.ACCEPTED,
avc.ResponseFrame.ResponseCode.IMPLEMENTED_OR_STABLE,
SetAbsoluteVolumeResponse(effective_volume),
)
+230 -470
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -2110,6 +2110,23 @@ 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
# -----------------------------------------------------------------------------
+123 -166
View File
@@ -1453,8 +1453,6 @@ 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 = (
@@ -1464,10 +1462,6 @@ 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,
@@ -1488,45 +1482,37 @@ class _IsoLink:
Raises:
HCI_Error: When command complete status is not HCI_SUCCESS.
"""
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)
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 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.
Args:
direction: Direction of data path.
Raises:
HCI_Error: When command complete status is not HCI_SUCCESS.
Returns:
Command status.
"""
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
),
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)
),
check_result=True,
)
self.data_paths.difference_update(directions_to_remove)
),
check_result=False,
)
return response.return_parameters.status
def write(self, sdu: bytes) -> None:
"""Write an ISO SDU."""
@@ -1636,8 +1622,7 @@ class CisLink(utils.EventEmitter, _IsoLink):
EVENT_ESTABLISHMENT_FAILURE: ClassVar[str] = "establishment_failure"
def __post_init__(self) -> None:
utils.EventEmitter.__init__(self)
_IsoLink.__init__(self)
super().__init__()
async def disconnect(
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
def __post_init__(self) -> None:
super().__init__()
self.device = self.big.device
@@ -1798,22 +1782,15 @@ class Connection(utils.CompositeEventEmitter):
@dataclass
class Parameters:
"""
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
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
)
def __init__(
self,
@@ -1854,6 +1831,36 @@ 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:
@@ -1865,7 +1872,7 @@ class Connection(utils.CompositeEventEmitter):
return f'UNKNOWN[{self.role}]'
@property
def is_encrypted(self) -> bool:
def is_encrypted(self):
return self.encryption != 0
@property
@@ -2169,12 +2176,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(device: Device, address: hci.Address, *args, **kwargs):
if connection := device.pending_connections.get(address):
return function(device, connection, *args, **kwargs)
for connection in device.connections.values():
def wrapper(self, address: hci.Address, *args, **kwargs):
if connection := self.pending_connections.get(address, False):
return function(self, connection, *args, **kwargs)
for connection in self.connections.values():
if connection.peer_address == address:
return function(device, connection, *args, **kwargs)
return function(self, connection, *args, **kwargs)
raise ObjectLookupError('no connection for address')
return wrapper
@@ -2184,13 +2191,13 @@ def with_connection_from_address(function):
# connection
def try_with_connection_from_address(function):
@functools.wraps(function)
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():
def wrapper(self, address, *args, **kwargs):
if connection := self.pending_connections.get(address, False):
return function(self, connection, address, *args, **kwargs)
for connection in self.connections.values():
if connection.peer_address == address:
return function(device, connection, address, *args, **kwargs)
return function(device, None, address, *args, **kwargs)
return function(self, connection, address, *args, **kwargs)
return function(self, None, address, *args, **kwargs)
return wrapper
@@ -2263,6 +2270,8 @@ class Device(utils.CompositeEventEmitter):
EVENT_CONNECTION_FAILURE = "connection_failure"
EVENT_SCO_REQUEST = "sco_request"
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_FAILURE = "sco_connection_failure"
EVENT_CIS_REQUEST = "cis_request"
@@ -2358,9 +2367,7 @@ class Device(utils.CompositeEventEmitter):
self.le_connecting = False
self.disconnecting = False
self.connections = {} # Connections, by connection handle
self.pending_connections = (
{}
) # Pending connections, by BD address (BR/EDR only)
self.pending_connections = {} # 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
@@ -3829,16 +3836,8 @@ class Device(utils.CompositeEventEmitter):
)
else:
# Save pending connection
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),
self.pending_connections[peer_address] = Connection.incomplete(
self, peer_address, hci.Role.CENTRAL
)
# 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_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
# command, this connection is still considered Peripheral until an eventual
# role change event.
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),
self.pending_connections[peer_address] = Connection.incomplete(
self, peer_address, hci.Role.PERIPHERAL
)
try:
@@ -4725,7 +4716,7 @@ class Device(utils.CompositeEventEmitter):
self, cis_acl_pairs: Sequence[tuple[int, Connection]]
) -> list[CisLink]:
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(
device=self,
acl_connection=acl_connection,
@@ -4741,7 +4732,6 @@ class Device(utils.CompositeEventEmitter):
}
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):
pending_future.set_result(cis_link)
@@ -5459,47 +5449,15 @@ class Device(utils.CompositeEventEmitter):
self.emit(self.EVENT_CONNECTION, connection)
@host_event_handler
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(
def on_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_interval: int,
peripheral_latency: int,
supervision_timeout: int,
connection_parameters: Optional[core.ConnectionParameters],
) -> None:
# Convert all-zeros addresses into None.
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'
)
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:
@@ -5568,16 +5539,16 @@ class Device(utils.CompositeEventEmitter):
connection = Connection(
self,
connection_handle,
PhysicalTransport.LE,
transport,
self_address,
self_resolvable_address,
peer_address,
peer_resolvable_address,
role,
Connection.Parameters(
connection_interval * 1.25,
peripheral_latency,
supervision_timeout * 10.0,
connection_parameters.connection_interval * 1.25,
connection_parameters.peripheral_latency,
connection_parameters.supervision_timeout * 10.0,
),
)
self.connections[connection_handle] = connection
@@ -5639,9 +5610,7 @@ 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: hci.Address, class_of_device: int, link_type: int
):
def on_connection_request(self, bd_addr, class_of_device, link_type):
logger.debug(f'*** Connection request: {bd_addr}')
# Handle SCO request.
@@ -5670,16 +5639,8 @@ 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(
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.pending_connections[bd_addr] = Connection.incomplete(
self, bd_addr, hci.Role.PERIPHERAL
)
self.host.send_command_sync(
@@ -5991,7 +5952,7 @@ class Device(utils.CompositeEventEmitter):
@host_event_handler
@try_with_connection_from_address
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:
@@ -6010,7 +5971,7 @@ class Device(utils.CompositeEventEmitter):
@host_event_handler
@try_with_connection_from_address
def on_remote_name_failure(
self, connection: Optional[Connection], address: hci.Address, error: int
self, connection: Connection, address: hci.Address, error: int
):
if connection:
connection.emit(connection.EVENT_REMOTE_NAME_FAILURE, error)
@@ -6222,27 +6183,27 @@ class Device(utils.CompositeEventEmitter):
@host_event_handler
@with_connection_from_handle
def on_connection_parameters_update(
self,
connection: Connection,
connection_interval: int,
peripheral_latency: int,
supervision_timeout: int,
self, connection: Connection, connection_parameters: core.ConnectionParameters
):
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_interval * 1.25:
if (
connection.parameters.connection_interval
!= connection_parameters.connection_interval * 1.25
):
connection.parameters = Connection.Parameters(
connection_interval * 1.25,
peripheral_latency,
supervision_timeout * 10.0,
connection_parameters.connection_interval * 1.25,
connection_parameters.peripheral_latency,
connection_parameters.supervision_timeout * 10.0,
)
else:
connection.parameters = Connection.Parameters(
connection_interval * 1.25,
peripheral_latency,
supervision_timeout * 10.0,
connection_parameters.connection_interval * 1.25,
connection_parameters.peripheral_latency,
connection_parameters.supervision_timeout * 10.0,
connection.parameters.subrate_factor,
connection.parameters.continuation_number,
)
@@ -6443,11 +6404,7 @@ class Device(utils.CompositeEventEmitter):
# [Classic only]
@host_event_handler
@with_connection_from_address
def on_role_change(
self,
connection: Connection,
new_role: hci.Role,
):
def on_role_change(self, connection: Connection, new_role: hci.Role):
connection.role = new_role
connection.emit(connection.EVENT_ROLE_CHANGE, new_role)
@@ -6455,7 +6412,7 @@ class Device(utils.CompositeEventEmitter):
@host_event_handler
@try_with_connection_from_address
def on_role_change_failure(
self, connection: Optional[Connection], address: hci.Address, error: int
self, connection: Connection, address: hci.Address, error: int
):
if connection:
connection.emit(connection.EVENT_ROLE_CHANGE_FAILURE, error)
-4
View File
@@ -49,10 +49,6 @@ async def get_driver_for_host(host: Host) -> Optional[Driver]:
driver_classes: dict[str, type[Driver]] = {"rtk": rtk.Driver, "intel": intel.Driver}
probe_list: Iterable[str]
if driver_name := host.hci_metadata.get("driver"):
# The "driver" metadata may include runtime options after a '/' (for example
# "intel/ddc=..."). Keep only the base driver name (the portion before the
# first slash) so it matches a key in driver_classes (e.g. "intel").
driver_name = driver_name.split("/")[0]
# Only probe a single driver
probe_list = [driver_name]
else:
+7 -33
View File
@@ -459,10 +459,6 @@ class Driver(common.Driver):
== ModeOfOperation.OPERATIONAL
):
logger.debug("firmware already loaded")
# If the firmeare is already loaded, still attempt to load any
# device configuration (DDC). DDC can be applied independently of a
# firmware reload and may contain runtime overrides or patches.
await self.load_ddc_if_any()
return
# We only support some platforms and variants.
@@ -602,39 +598,17 @@ class Driver(common.Driver):
await self.reset_complete.wait()
logger.debug("reset complete")
await self.load_ddc_if_any(firmware_base_name)
async def load_ddc_if_any(self, firmware_base_name: Optional[str] = None) -> None:
"""
Check for and load any Device Data Configuration (DDC) blobs.
Args:
firmware_base_name: Base name of the selected firmware (e.g. "ibt-XXXX-YYYY").
If None, don't attempt to look up a .ddc file that
corresponds to the firmware image.
Priority:
1. If a ddc_override was provided via driver metadata, use it (highest priority).
2. Otherwise, if firmware_base_name is provided, attempt to find a .ddc file
that corresponds to the selected firmware image.
3. Finally, if a ddc_addon was provided, append/load it after the primary DDC.
"""
# If an explicit DDC override was supplied, use it and skip file lookup.
# Load the device config if there is one.
if self.ddc_override:
logger.debug("loading overridden DDC")
await self.load_device_config(self.ddc_override)
else:
# Only attempt .ddc file lookup if a firmware_base_name was provided.
if firmware_base_name is None:
logger.debug(
"no firmware_base_name provided; skipping .ddc file lookup"
)
else:
ddc_name = f"{firmware_base_name}.ddc"
ddc_path = _find_binary_path(ddc_name)
if ddc_path:
logger.debug(f"loading DDC from {ddc_path}")
ddc_data = ddc_path.read_bytes()
await self.load_device_config(ddc_data)
ddc_name = f"{firmware_base_name}.ddc"
ddc_path = _find_binary_path(ddc_name)
if ddc_path:
logger.debug(f"loading DDC from {ddc_path}")
ddc_data = ddc_path.read_bytes()
await self.load_device_config(ddc_data)
if self.ddc_addon:
logger.debug("loading DDC addon")
await self.load_device_config(self.ddc_addon)
+1 -3
View File
@@ -115,14 +115,12 @@ RTK_USB_PRODUCTS = {
# Realtek 8761BUV
(0x0B05, 0x190E),
(0x0BDA, 0x8771),
(0x0BDA, 0x877B),
(0x0BDA, 0xA728),
(0x0BDA, 0xA729),
(0x2230, 0x0016),
(0x2357, 0x0604),
(0x2550, 0x8761),
(0x2B89, 0x8761),
(0x7392, 0xC611),
(0x0BDA, 0x877B),
# Realtek 8821AE
(0x0B05, 0x17DC),
(0x13D3, 0x3414),
-38
View File
@@ -3441,17 +3441,6 @@ class HCI_Write_Synchronous_Flow_Control_Enable_Command(HCI_Command):
synchronous_flow_control_enable: int = field(metadata=metadata(1))
# -----------------------------------------------------------------------------
@HCI_Command.command
@dataclasses.dataclass
class HCI_Set_Controller_To_Host_Flow_Control_Command(HCI_Command):
'''
See Bluetooth spec @ 7.3.38 Set Controller To Host Flow Control command
'''
flow_control_enable: int = field(metadata=metadata(1))
# -----------------------------------------------------------------------------
@HCI_Command.command
@dataclasses.dataclass
@@ -4349,15 +4338,6 @@ class HCI_LE_Write_Suggested_Default_Data_Length_Command(HCI_Command):
suggested_max_tx_time: int = field(metadata=metadata(2))
# -----------------------------------------------------------------------------
@HCI_Command.command
@dataclasses.dataclass
class HCI_LE_Read_Local_P_256_Public_Key_Command(HCI_Command):
'''
See Bluetooth spec @ 7.8.36 LE LE Read Local P-256 Public Key command
'''
# -----------------------------------------------------------------------------
@HCI_Command.command
@dataclasses.dataclass
@@ -4385,15 +4365,6 @@ class HCI_LE_Clear_Resolving_List_Command(HCI_Command):
'''
# -----------------------------------------------------------------------------
@HCI_Command.command
@dataclasses.dataclass
class HCI_LE_Read_Resolving_List_Size_Command(HCI_Command):
'''
See Bluetooth spec @ 7.8.41 LE Read Resolving List Size command
'''
# -----------------------------------------------------------------------------
@HCI_Command.command
@dataclasses.dataclass
@@ -5057,15 +5028,6 @@ class HCI_LE_Periodic_Advertising_Terminate_Sync_Command(HCI_Command):
sync_handle: int = field(metadata=metadata(2))
# -----------------------------------------------------------------------------
@HCI_Command.command
@dataclasses.dataclass
class HCI_LE_Read_Transmit_Power_Command(HCI_Command):
'''
See Bluetooth spec @ 7.8.74 LE Read Transmit Power command
'''
# -----------------------------------------------------------------------------
@HCI_Command.command
@dataclasses.dataclass
+4 -4
View File
@@ -489,9 +489,9 @@ STATUS_CODES = {
@dataclasses.dataclass
class HfConfiguration:
supported_hf_features: collections.abc.Sequence[HfFeature]
supported_hf_indicators: collections.abc.Sequence[HfIndicator]
supported_audio_codecs: collections.abc.Sequence[AudioCodec]
supported_hf_features: list[HfFeature]
supported_hf_indicators: list[HfIndicator]
supported_audio_codecs: list[AudioCodec]
@dataclasses.dataclass
@@ -753,7 +753,7 @@ class HfProtocol(utils.EventEmitter):
# Build local features.
self.supported_hf_features = sum(configuration.supported_hf_features)
self.supported_audio_codecs = list(configuration.supported_audio_codecs)
self.supported_audio_codecs = configuration.supported_audio_codecs
self.hf_indicators = {
indicator: HfIndicatorState(indicator=indicator)
+1 -1
View File
@@ -246,7 +246,7 @@ class HID(ABC, utils.EventEmitter):
# Create a new L2CAP connection - interrupt channel
try:
channel = await self.connection.create_l2cap_channel(
l2cap.ClassicChannelSpec(HID_INTERRUPT_PSM)
l2cap.ClassicChannelSpec(HID_CONTROL_PSM)
)
channel.sink = self.on_intr_pdu
self.l2cap_intr_channel = channel
+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.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.snoop import Snooper
from bumble.transport.common import TransportLostError
@@ -550,7 +555,7 @@ class Host(utils.EventEmitter):
logger.debug(
'HCI LE flow control: '
f'le_acl_data_packet_length={le_acl_data_packet_length},'
f'total_num_le_acl_data_packets={total_num_le_acl_data_packets},'
f'total_num_le_acl_data_packets={total_num_le_acl_data_packets}'
f'iso_data_packet_length={iso_data_packet_length},'
f'total_num_iso_data_packets={total_num_iso_data_packets}'
)
@@ -991,16 +996,20 @@ 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(
'le_connection',
'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),
event.connection_interval,
event.peripheral_latency,
event.supervision_timeout,
connection_parameters,
)
else:
logger.debug(f'### CONNECTION FAILED: {event.status}')
@@ -1051,9 +1060,14 @@ class Host(utils.EventEmitter):
# Notify the client
self.emit(
'classic_connection',
'connection',
event.connection_handle,
PhysicalTransport.BR_EDR,
event.bd_addr,
None,
None,
None,
None,
)
else:
logger.debug(f'### BR/EDR CONNECTION FAILED: {event.status}')
@@ -1116,13 +1130,14 @@ class Host(utils.EventEmitter):
# Notify the client
if event.status == hci.HCI_SUCCESS:
self.emit(
'connection_parameters_update',
connection.handle,
connection_parameters = ConnectionParameters(
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
+4 -33
View File
@@ -273,19 +273,12 @@ class HearingAccessService(gatt.TemplateService):
def on_disconnection(_reason) -> None:
self.currently_connected_clients.discard(connection)
@connection.on(connection.EVENT_CONNECTION_ATT_MTU_UPDATE)
def on_mtu_update(*_: Any) -> None:
self.on_incoming_connection(connection)
@connection.on(connection.EVENT_CONNECTION_ENCRYPTION_CHANGE)
def on_encryption_change(*_: Any) -> None:
self.on_incoming_connection(connection)
@connection.on(connection.EVENT_PAIRING)
def on_pairing(*_: Any) -> None:
self.on_incoming_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(
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'''
# TODO Should we filter on HAP device only ?
if not connection.is_encrypted:
logging.debug(f'HAS: {connection.peer_address} is not encrypted')
return
if not connection.peer_resolvable_address:
logging.debug(f'HAS: {connection.peer_address} is not paired')
return
if connection.att_mtu < 49:
logging.debug(
f'HAS: {connection.peer_address} invalid MTU={connection.att_mtu}'
)
return
if connection.peer_address in self.currently_connected_clients:
logging.debug(
f'HAS: Already connected to {connection.peer_address} nothing to do'
)
return
self.currently_connected_clients.add(connection)
if (
connection.peer_address
@@ -485,7 +457,6 @@ class HearingAccessService(gatt.TemplateService):
connection,
self.hearing_aid_preset_control_point,
value=op_list[0].to_bytes(len(op_list) == 1),
force=True, # TODO GATT notification subscription should be persistent
)
# Remove item once sent, and keep the non sent item in the list
op_list.pop(0)
+3 -7
View File
@@ -674,14 +674,10 @@ class DLC(utils.EventEmitter):
while (self.tx_buffer and self.tx_credits > 0) or rx_credits_needed > 0:
# Get the next chunk, up to MTU size
if rx_credits_needed > 0:
chunk = bytes([rx_credits_needed])
chunk = bytes([rx_credits_needed]) + self.tx_buffer[: self.mtu - 1]
self.tx_buffer = self.tx_buffer[len(chunk) - 1 :]
self.rx_credits += rx_credits_needed
if self.tx_buffer and self.tx_credits > 0:
chunk += self.tx_buffer[: self.mtu - 1]
self.tx_buffer = self.tx_buffer[len(chunk) - 1 :]
tx_credit_spent = True
else:
tx_credit_spent = False
tx_credit_spent = len(chunk) > 1
else:
chunk = self.tx_buffer[: self.mtu]
self.tx_buffer = self.tx_buffer[len(chunk) :]
+10 -16
View File
@@ -12,12 +12,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import os
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
import logging
import os
import re
from contextlib import asynccontextmanager
from typing import Optional
from bumble import utils
@@ -84,19 +85,12 @@ async def open_transport(name: str) -> Transport:
scheme, *tail = name.split(':', 1)
spec = tail[0] if tail else None
metadata = None
# If a spec is provided, check for a metadata section in square brackets.
# The regex captures a comma-separated list of key=value pairs (allowing an
# optional trailing comma). The key is matched by \w+ and the value by [^,\]]+,
# meaning the value may contain any character except a comma or a closing
# bracket (']').
if spec and (m := re.search(r'\[(\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(',')])
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(',')])
transport = await _open_transport(scheme, spec)
if metadata:
+1 -5
View File
@@ -131,11 +131,7 @@ def publish_grpc_port(grpc_port: int, instance_number: int) -> bool:
def cleanup():
logger.debug("removing .ini file")
try:
ini_file.unlink()
except OSError as error:
# Don't log at exception level, since this may happen normally.
logger.debug(f'failed to remove .ini file ({error})')
ini_file.unlink()
atexit.register(cleanup)
return True
+3 -6
View File
@@ -22,7 +22,6 @@ import contextlib
import io
import logging
import struct
from collections.abc import Awaitable, Callable
from typing import Any, ContextManager, Optional, Protocol
from bumble import core, hci
@@ -390,17 +389,15 @@ class PumpedPacketSource(ParserSource):
# -----------------------------------------------------------------------------
class PumpedPacketSink:
pump_task: Optional[asyncio.Task[None]]
def __init__(self, send: Callable[[bytes], Awaitable[Any]]):
def __init__(self, send):
self.send_function = send
self.packet_queue = asyncio.Queue[bytes]()
self.packet_queue = asyncio.Queue()
self.pump_task = None
def on_packet(self, packet: bytes) -> None:
self.packet_queue.put_nowait(packet)
def start(self) -> None:
def start(self):
async def pump_packets():
while True:
try:
+2 -56
View File
@@ -17,7 +17,6 @@
# -----------------------------------------------------------------------------
import asyncio
import logging
from typing import Optional
import serial_asyncio
@@ -29,56 +28,25 @@ from bumble.transport.common import StreamPacketSink, StreamPacketSource, Transp
logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
# Constants
# -----------------------------------------------------------------------------
DEFAULT_POST_OPEN_DELAY = 0.5 # in seconds
# -----------------------------------------------------------------------------
# Classes and Functions
# -----------------------------------------------------------------------------
# -----------------------------------------------------------------------------
class SerialPacketSource(StreamPacketSource):
def __init__(self) -> None:
super().__init__()
self._ready = asyncio.Event()
async def wait_until_ready(self) -> None:
await self._ready.wait()
def connection_made(self, transport: asyncio.BaseTransport) -> None:
logger.debug('connection made')
self._ready.set()
def connection_lost(self, exc: Optional[Exception]) -> None:
logger.debug('connection lost')
self.on_transport_lost()
# -----------------------------------------------------------------------------
async def open_serial_transport(spec: str) -> Transport:
'''
Open a serial port transport.
The parameter string has this syntax:
<device-path>[,<speed>][,rtscts][,dsrdtr][,delay]
<device-path>[,<speed>][,rtscts][,dsrdtr]
When <speed> is omitted, the default value of 1000000 is used
When "rtscts" is specified, RTS/CTS hardware flow control is enabled
When "dsrdtr" is specified, DSR/DTR hardware flow control is enabled
When "delay" is specified, a short delay is added after opening the port
Examples:
/dev/tty.usbmodem0006839912172
/dev/tty.usbmodem0006839912172,1000000
/dev/tty.usbmodem0006839912172,rtscts
/dev/tty.usbmodem0006839912172,rtscts,delay
'''
speed = 1000000
rtscts = False
dsrdtr = False
delay = 0.0
if ',' in spec:
parts = spec.split(',')
device = parts[0]
@@ -87,16 +55,13 @@ async def open_serial_transport(spec: str) -> Transport:
rtscts = True
elif part == 'dsrdtr':
dsrdtr = True
elif part == 'delay':
delay = DEFAULT_POST_OPEN_DELAY
elif part.isnumeric():
speed = int(part)
else:
device = spec
serial_transport, packet_source = await serial_asyncio.create_serial_connection(
asyncio.get_running_loop(),
SerialPacketSource,
StreamPacketSource,
device,
baudrate=speed,
rtscts=rtscts,
@@ -104,23 +69,4 @@ async def open_serial_transport(spec: str) -> Transport:
)
packet_sink = StreamPacketSink(serial_transport)
logger.debug('waiting for the port to be ready')
await packet_source.wait_until_ready()
logger.debug('port is ready')
# Try to assert DTR
assert serial_transport.serial is not None
try:
serial_transport.serial.dtr = True
logger.debug(
f"DSR={serial_transport.serial.dsr}, DTR={serial_transport.serial.dtr}"
)
except Exception as e:
logger.warning(f'could not assert DTR: {e}')
# Wait a bit after opening the port, if requested
if delay > 0.0:
logger.debug(f'waiting {delay} seconds after opening the port')
await asyncio.sleep(delay)
return Transport(packet_source, packet_sink)
+2 -2
View File
@@ -17,7 +17,7 @@
# -----------------------------------------------------------------------------
import logging
import websockets.asyncio.client
import websockets.client
from bumble.transport.common import (
PumpedPacketSink,
@@ -42,7 +42,7 @@ async def open_ws_client_transport(spec: str) -> Transport:
Example: ws://localhost:7681/v1/websocket/bt
'''
websocket = await websockets.asyncio.client.connect(spec)
websocket = await websockets.client.connect(spec)
class WsTransport(PumpedTransport):
async def close(self):
+8 -16
View File
@@ -16,9 +16,8 @@
# Imports
# -----------------------------------------------------------------------------
import logging
from typing import Optional
import websockets.asyncio.server
import websockets
from bumble.transport.common import ParserSource, PumpedPacketSink, Transport
@@ -41,12 +40,7 @@ async def open_ws_server_transport(spec: str) -> Transport:
'''
class WsServerTransport(Transport):
sink: PumpedPacketSink
source: ParserSource
connection: Optional[websockets.asyncio.server.ServerConnection]
server: Optional[websockets.asyncio.server.Server]
def __init__(self) -> None:
def __init__(self):
source = ParserSource()
sink = PumpedPacketSink(self.send_packet)
self.connection = None
@@ -54,19 +48,17 @@ async def open_ws_server_transport(spec: str) -> Transport:
super().__init__(source, sink)
async def serve(self, local_host: str, local_port: str) -> None:
async def serve(self, local_host, local_port):
self.sink.start()
# pylint: disable-next=no-member
self.server = await websockets.asyncio.server.serve(
handler=self.on_connection,
self.server = await websockets.serve(
ws_handler=self.on_connection,
host=local_host if local_host != '_' else None,
port=int(local_port),
)
logger.debug(f'websocket server ready on port {local_port}')
async def on_connection(
self, connection: websockets.asyncio.server.ServerConnection
) -> None:
async def on_connection(self, connection):
logger.debug(
f'new connection on {connection.local_address} '
f'from {connection.remote_address}'
@@ -85,11 +77,11 @@ async def open_ws_server_transport(spec: str) -> Transport:
# We're now disconnected
self.connection = None
async def send_packet(self, packet: bytes) -> None:
async def send_packet(self, packet):
if self.connection is None:
logger.debug('no connection, dropping packet')
return
await self.connection.send(packet)
return await self.connection.send(packet)
local_host, local_port = spec.rsplit(':', maxsplit=1)
transport = WsServerTransport()
+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).
## Moniker
The moniker syntax for a serial transport is:
`<device-path>[,<speed>][,rtscts][,dsrdtr][,delay]`
When `<speed>` is omitted, the default value of 1000000 is used.
When `rtscts` is specified, RTS/CTS hardware flow control is enabled.
When `dsrdtr` is specified, DSR/DTR hardware flow control is enabled.
When `delay` is specified, a short delay is added after opening the port.
The moniker syntax for a serial transport is: `serial:<device-path>[,<speed>]`
When `<speed>` is omitted, the default value of 1000000 is used
!!! example
```
/dev/tty.usbmodem0006839912172
/dev/tty.usbmodem0006839912172,1000000
/dev/tty.usbmodem0006839912172,rtscts
/dev/tty.usbmodem0006839912172,rtscts,delay
```
`serial:/dev/tty.usbmodem0006839912172,1000000`
Opens the serial port `/dev/tty.usbmodem0006839912172` at `1000000`bps
+3 -3
View File
@@ -20,7 +20,7 @@ import json
import struct
import sys
import websockets.asyncio.server
import websockets
import bumble.logging
from bumble import data_types
@@ -367,7 +367,7 @@ async def keyboard_device(device, command):
if command == 'web':
# Start a Websocket server to receive events from a web page
async def serve(websocket: websockets.asyncio.server.ServerConnection):
async def serve(websocket, _path):
while True:
try:
message = await websocket.recv()
@@ -398,7 +398,7 @@ async def keyboard_device(device, command):
pass
# pylint: disable-next=no-member
await websockets.asyncio.server.serve(serve, 'localhost', 8989)
await websockets.serve(serve, 'localhost', 8989)
await asyncio.get_event_loop().create_future()
else:
message = bytes('hello', 'ascii')
+5 -4
View File
@@ -20,7 +20,7 @@ import logging
import sys
from typing import Optional
import websockets.asyncio.server
import websockets
import bumble.logging
from bumble import data_types, decoder, gatt
@@ -29,11 +29,12 @@ from bumble.device import AdvertisingParameters, Device
from bumble.profiles import asha
from bumble.transport import open_transport
ws_connection: Optional[websockets.asyncio.server.ServerConnection] = None
ws_connection: Optional[websockets.WebSocketServerProtocol] = None
g722_decoder = decoder.G722Decoder()
async def ws_server(ws_client: websockets.asyncio.server.ServerConnection):
async def ws_server(ws_client: websockets.WebSocketServerProtocol, path: str):
del path
global ws_connection
ws_connection = ws_client
@@ -99,7 +100,7 @@ async def main() -> None:
),
)
await websockets.asyncio.server.serve(ws_server, port=8888)
await websockets.serve(ws_server, port=8888)
await hci_transport.source.terminated
+3 -6
View File
@@ -21,9 +21,8 @@ import asyncio
import json
import logging
import sys
from typing import Optional
import websockets.asyncio.server
import websockets
import bumble.logging
from bumble import a2dp, avc, avdtp, avrcp, utils
@@ -218,8 +217,6 @@ def on_avrcp_start(avrcp_protocol: avrcp.Protocol, websocket_server: WebSocketSe
# -----------------------------------------------------------------------------
class WebSocketServer:
socket: Optional[websockets.asyncio.server.ServerConnection]
def __init__(
self, avrcp_protocol: avrcp.Protocol, avrcp_delegate: Delegate
) -> None:
@@ -230,9 +227,9 @@ class WebSocketServer:
async def start(self) -> None:
# pylint: disable-next=no-member
await websockets.asyncio.server.serve(self.serve, 'localhost', 8989) # type: ignore
await websockets.serve(self.serve, 'localhost', 8989) # type: ignore
async def serve(self, socket: websockets.asyncio.server.ServerConnection) -> None:
async def serve(self, socket, _path) -> None:
print('### WebSocket connected')
self.socket = socket
while True:
+1 -2
View File
@@ -19,7 +19,6 @@ import asyncio
import sys
import bumble.logging
from bumble import hci
from bumble.controller import Controller
from bumble.device import Device
from bumble.gatt import (
@@ -62,7 +61,7 @@ async def main() -> None:
host_sink=hci_transport.sink,
link=link,
)
controller1.random_address = hci.Address(sys.argv[1])
controller1.random_address = sys.argv[1]
# Create a second controller using the same link
controller2 = Controller('C2', link=link)
+5 -4
View File
@@ -22,7 +22,7 @@ import logging
import sys
from typing import Iterable, Optional
import websockets.asyncio.server
import websockets
import bumble.core
import bumble.logging
@@ -33,7 +33,7 @@ from bumble.transport import open_transport
logger = logging.getLogger(__name__)
ws: Optional[websockets.asyncio.server.ServerConnection] = None
ws: Optional[websockets.WebSocketServerProtocol] = None
ag_protocol: Optional[hfp.AgProtocol] = None
source_file: Optional[io.BufferedReader] = None
@@ -114,7 +114,8 @@ def on_hfp_state_change(connected: bool):
send_message(type='hfp_state_change', connected=connected)
async def ws_server(ws_client: websockets.asyncio.server.ServerConnection):
async def ws_server(ws_client: websockets.WebSocketServerProtocol, path: str):
del path
global ws
ws = ws_client
@@ -272,7 +273,7 @@ async def main() -> None:
on_dlc(session)
await websockets.asyncio.server.serve(ws_server, port=8888)
await websockets.serve(ws_server, port=8888)
if len(sys.argv) >= 5:
global source_file
+4 -4
View File
@@ -22,7 +22,7 @@ import json
import sys
from typing import Optional
import websockets.asyncio.server
import websockets
import bumble.logging
from bumble import hci, hfp, rfcomm
@@ -30,7 +30,7 @@ from bumble.device import Connection, Device
from bumble.hfp import HfProtocol
from bumble.transport import open_transport
ws: Optional[websockets.asyncio.server.ServerConnection] = None
ws: Optional[websockets.WebSocketServerProtocol] = None
hf_protocol: Optional[HfProtocol] = None
@@ -143,7 +143,7 @@ async def main() -> None:
await device.set_connectable(True)
# Start the UI websocket server to offer a few buttons and input boxes
async def serve(websocket: websockets.asyncio.server.ServerConnection):
async def serve(websocket: websockets.WebSocketServerProtocol, _path):
global ws
ws = websocket
async for message in websocket:
@@ -166,7 +166,7 @@ async def main() -> None:
response = str(await hf_protocol.query_current_calls())
await websocket.send(response)
await websockets.asyncio.server.serve(serve, 'localhost', 8989)
await websockets.serve(serve, 'localhost', 8989)
await hci_transport.source.wait_for_termination()
+3 -3
View File
@@ -20,7 +20,7 @@ import json
import struct
import sys
import websockets.asyncio.server
import websockets
import bumble.logging
from bumble.core import (
@@ -425,7 +425,7 @@ deviceData = DeviceData()
async def keyboard_device(hid_device: HID_Device):
# Start a Websocket server to receive events from a web page
async def serve(websocket: websockets.asyncio.server.ServerConnection):
async def serve(websocket, _path):
global deviceData
while True:
try:
@@ -476,7 +476,7 @@ async def keyboard_device(hid_device: HID_Device):
pass
# pylint: disable-next=no-member
await websockets.asyncio.server.serve(serve, 'localhost', 8989)
await websockets.serve(serve, 'localhost', 8989)
await asyncio.get_event_loop().create_future()
+4 -4
View File
@@ -20,7 +20,7 @@ import json
import sys
from typing import Optional
import websockets.asyncio.server
import websockets
import bumble.logging
from bumble import data_types
@@ -101,7 +101,7 @@ async def main() -> None:
)
device.add_service(AudioStreamControlService(device, sink_ase_id=[1]))
ws: Optional[websockets.asyncio.server.ServerConnection] = None
ws: Optional[websockets.WebSocketServerProtocol] = None
mcp: Optional[MediaControlServiceProxy] = None
advertising_data = bytes(
@@ -162,7 +162,7 @@ async def main() -> None:
device.on('connection', on_connection)
async def serve(websocket: websockets.asyncio.server.ServerConnection):
async def serve(websocket: websockets.WebSocketServerProtocol, _path):
nonlocal ws
ws = websocket
async for message in websocket:
@@ -173,7 +173,7 @@ async def main() -> None:
)
ws = None
await websockets.asyncio.server.serve(serve, 'localhost', 8989)
await websockets.serve(serve, 'localhost', 8989)
await hci_transport.source.terminated
+4 -4
View File
@@ -21,7 +21,7 @@ import secrets
import sys
from typing import Optional
import websockets.asyncio.server
import websockets
import bumble.logging
from bumble import data_types
@@ -110,7 +110,7 @@ async def main() -> None:
vcs = VolumeControlService()
device.add_service(vcs)
ws: Optional[websockets.asyncio.server.ServerConnection] = None
ws: Optional[websockets.WebSocketServerProtocol] = None
def on_volume_state_change():
if ws:
@@ -152,7 +152,7 @@ async def main() -> None:
advertising_data=advertising_data,
)
async def serve(websocket: websockets.asyncio.server.ServerConnection):
async def serve(websocket: websockets.WebSocketServerProtocol, _path):
nonlocal ws
await websocket.send(
dumps_volume_state(vcs.volume_setting, vcs.muted, vcs.change_counter)
@@ -166,7 +166,7 @@ async def main() -> None:
await device.notify_subscribers(vcs.volume_state)
ws = None
await websockets.asyncio.server.serve(serve, 'localhost', 8989)
await websockets.serve(serve, 'localhost', 8989)
await hci_transport.source.terminated
+1 -1
View File
@@ -32,7 +32,7 @@ dependencies = [
"pyserial-asyncio >= 0.5; platform_system!='Emscripten'",
"pyserial >= 3.5; platform_system!='Emscripten'",
"pyusb >= 1.2; platform_system!='Emscripten'",
"websockets >= 15.0.1; platform_system!='Emscripten'",
"websockets == 13.1; platform_system!='Emscripten'",
]
[project.optional-dependencies]
+56 -140
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 0.48.0",
"windows-sys",
]
[[package]]
@@ -71,7 +71,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
dependencies = [
"anstyle",
"windows-sys 0.48.0",
"windows-sys",
]
[[package]]
@@ -249,7 +249,7 @@ dependencies = [
"atty",
"bitflags 1.3.2",
"clap_lex 0.2.4",
"indexmap 1.9.3",
"indexmap",
"strsim",
"termcolor",
"textwrap",
@@ -451,7 +451,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.48.0",
"windows-sys",
]
[[package]]
@@ -483,19 +483,24 @@ dependencies = [
]
[[package]]
name = "equivalent"
version = "1.0.2"
name = "errno"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
dependencies = [
"errno-dragonfly",
"libc",
"windows-sys",
]
[[package]]
name = "errno"
version = "0.3.14"
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
"windows-sys 0.52.0",
]
[[package]]
@@ -678,9 +683,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.27"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833"
dependencies = [
"bytes",
"fnv",
@@ -688,7 +693,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
"indexmap 2.11.3",
"indexmap",
"slab",
"tokio",
"tokio-util",
@@ -701,12 +706,6 @@ 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"
@@ -828,17 +827,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"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",
"hashbrown",
]
[[package]]
@@ -867,7 +856,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [
"hermit-abi 0.3.2",
"rustix",
"windows-sys 0.48.0",
"windows-sys",
]
[[package]]
@@ -902,9 +891,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.175"
version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "libusb1-sys"
@@ -931,9 +920,9 @@ dependencies = [
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
[[package]]
name = "lock_api"
@@ -1007,13 +996,13 @@ dependencies = [
[[package]]
name = "mio"
version = "0.8.11"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [
"libc",
"wasi",
"windows-sys 0.48.0",
"windows-sys",
]
[[package]]
@@ -1084,9 +1073,9 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "openssl"
version = "0.10.73"
version = "0.10.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800"
dependencies = [
"bitflags 2.4.0",
"cfg-if",
@@ -1116,9 +1105,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.109"
version = "0.9.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f"
dependencies = [
"cc",
"libc",
@@ -1164,7 +1153,7 @@ dependencies = [
"libc",
"redox_syscall 0.3.5",
"smallvec",
"windows-targets 0.48.5",
"windows-targets",
]
[[package]]
@@ -1553,15 +1542,15 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustix"
version = "0.38.44"
version = "0.38.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
checksum = "ed6248e1caa625eb708e266e06159f135e8c26f2bb7ceb72dc4b2766d0340964"
dependencies = [
"bitflags 2.4.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys",
]
[[package]]
@@ -1591,7 +1580,7 @@ version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
dependencies = [
"windows-sys 0.48.0",
"windows-sys",
]
[[package]]
@@ -1713,12 +1702,12 @@ dependencies = [
[[package]]
name = "socket2"
version = "0.5.10"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys",
]
[[package]]
@@ -1784,7 +1773,7 @@ dependencies = [
"fastrand",
"redox_syscall 0.3.5",
"rustix",
"windows-sys 0.48.0",
"windows-sys",
]
[[package]]
@@ -1839,9 +1828,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.38.2"
version = "1.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68722da18b0fc4a05fdc1120b302b82051265792a1e1b399086e9b204b10ad3d"
checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
dependencies = [
"backtrace",
"bytes",
@@ -1851,16 +1840,16 @@ dependencies = [
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2 0.5.10",
"socket2 0.5.3",
"tokio-macros",
"windows-sys 0.48.0",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "2.3.0"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
@@ -2141,16 +2130,7 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"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",
"windows-targets",
]
[[package]]
@@ -2159,29 +2139,13 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"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",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
@@ -2190,90 +2154,42 @@ 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"
@@ -2281,5 +2197,5 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
dependencies = [
"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>"]
keywords = ["bluetooth", "ble"]
categories = ["api-bindings", "network-programming"]
rust-version = "1.80.0"
rust-version = "1.76.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.38.2", features = ["macros", "signal"] }
tokio = { version = "1.28.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.38.2", features = ["full"] }
tokio = { version = "1.28.2", features = ["full"] }
tempfile = "3.6.0"
nix = "0.26.2"
anyhow = "1.0.71"
+114 -261
View File
@@ -18,12 +18,25 @@
import asyncio
import logging
import os
import struct
from typing import Awaitable
import pytest
from bumble import a2dp, avdtp
from bumble.a2dp import (
AacMediaCodecInformation,
OpusMediaCodecInformation,
SbcMediaCodecInformation,
)
from bumble.avdtp import (
A2DP_SBC_CODEC_TYPE,
AVDTP_AUDIO_MEDIA_TYPE,
AVDTP_IDLE_STATE,
AVDTP_STREAMING_STATE,
AVDTP_TSEP_SNK,
Listener,
MediaCodecCapabilities,
MediaPacketPump,
Protocol,
)
from bumble.controller import Controller
from bumble.core import PhysicalTransport
from bumble.device import Device
@@ -69,24 +82,6 @@ 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():
@@ -124,15 +119,15 @@ async def test_self_connection():
# -----------------------------------------------------------------------------
def source_codec_capabilities():
return avdtp.MediaCodecCapabilities(
media_type=avdtp.MediaType.AUDIO,
media_codec_type=a2dp.CodecType.SBC,
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,
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,
minimum_bitpool_value=2,
maximum_bitpool_value=53,
),
@@ -141,26 +136,26 @@ def source_codec_capabilities():
# -----------------------------------------------------------------------------
def sink_codec_capabilities():
return avdtp.MediaCodecCapabilities(
media_type=avdtp.MediaType.AUDIO,
media_codec_type=a2dp.CodecType.SBC,
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,
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,
minimum_bitpool_value=2,
maximum_bitpool_value=53,
),
@@ -190,7 +185,7 @@ async def test_source_sink_1():
sink.on('rtp_packet', on_rtp_packet)
# Create a listener to wait for AVDTP connections
listener = avdtp.Listener.for_device(two_devices.devices[1])
listener = Listener.for_device(two_devices.devices[1])
listener.on('connection', on_avdtp_connection)
async def make_connection():
@@ -203,13 +198,13 @@ async def test_source_sink_1():
return connections[0]
connection = await make_connection()
client = await avdtp.Protocol.connect(connection)
client = await Protocol.connect(connection)
endpoints = await client.discover_remote_endpoints()
assert len(endpoints) == 1
remote_sink = list(endpoints)[0]
assert remote_sink.in_use == 0
assert remote_sink.media_type == avdtp.MediaType.AUDIO
assert remote_sink.tsep == avdtp.StreamEndPointType.SNK
assert remote_sink.media_type == AVDTP_AUDIO_MEDIA_TYPE
assert remote_sink.tsep == AVDTP_TSEP_SNK
async def generate_packets(packet_count):
sequence_number = 0
@@ -228,24 +223,24 @@ async def test_source_sink_1():
rtp_packets_fully_received = asyncio.get_running_loop().create_future()
rtp_packets_expected = 3
rtp_packets = []
pump = avdtp.MediaPacketPump(generate_packets(3))
pump = MediaPacketPump(generate_packets(3))
source = client.add_source(source_codec_capabilities(), pump)
stream = await client.create_stream(source, remote_sink)
await stream.start()
assert stream.state == avdtp.State.STREAMING
assert stream.state == AVDTP_STREAMING_STATE
assert stream.local_endpoint.in_use == 1
assert stream.rtp_channel is not None
assert sink.in_use == 1
assert sink.stream is not None
assert sink.stream.state == avdtp.State.STREAMING
assert sink.stream.state == AVDTP_STREAMING_STATE
await rtp_packets_fully_received
await stream.close()
assert stream.rtp_channel is None
assert source.in_use == 0
assert source.stream.state == avdtp.State.IDLE
assert source.stream.state == AVDTP_IDLE_STATE
assert sink.in_use == 0
assert sink.stream.state == avdtp.State.IDLE
assert sink.stream.state == AVDTP_IDLE_STATE
# Send packets manually
rtp_packets_fully_received = asyncio.get_running_loop().create_future()
@@ -257,12 +252,12 @@ async def test_source_sink_1():
source = client.add_source(source_codec_capabilities(), None)
stream = await client.create_stream(source, remote_sink)
await stream.start()
assert stream.state == avdtp.State.STREAMING
assert stream.state == AVDTP_STREAMING_STATE
assert stream.local_endpoint.in_use == 1
assert stream.rtp_channel is not None
assert sink.in_use == 1
assert sink.stream is not None
assert sink.stream.state == avdtp.State.STREAMING
assert sink.stream.state == AVDTP_STREAMING_STATE
stream.send_media_packet(source_packets[0])
stream.send_media_packet(source_packets[1])
@@ -272,61 +267,59 @@ async def test_source_sink_1():
assert stream.rtp_channel is None
assert len(rtp_packets) == 3
assert source.in_use == 0
assert source.stream.state == avdtp.State.IDLE
assert source.stream.state == AVDTP_IDLE_STATE
assert sink.in_use == 0
assert sink.stream.state == avdtp.State.IDLE
assert sink.stream.state == AVDTP_IDLE_STATE
# -----------------------------------------------------------------------------
def test_sbc_codec_specific_information():
sbc_info = a2dp.SbcMediaCodecInformation.from_bytes(bytes.fromhex("3fff0235"))
sbc_info = SbcMediaCodecInformation.from_bytes(bytes.fromhex("3fff0235"))
assert (
sbc_info.sampling_frequency
== a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_44100
| a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_48000
== SbcMediaCodecInformation.SamplingFrequency.SF_44100
| SbcMediaCodecInformation.SamplingFrequency.SF_48000
)
assert (
sbc_info.channel_mode
== a2dp.SbcMediaCodecInformation.ChannelMode.MONO
| a2dp.SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
| a2dp.SbcMediaCodecInformation.ChannelMode.STEREO
| a2dp.SbcMediaCodecInformation.ChannelMode.JOINT_STEREO
== SbcMediaCodecInformation.ChannelMode.MONO
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
| SbcMediaCodecInformation.ChannelMode.STEREO
| SbcMediaCodecInformation.ChannelMode.JOINT_STEREO
)
assert (
sbc_info.block_length
== a2dp.SbcMediaCodecInformation.BlockLength.BL_4
| a2dp.SbcMediaCodecInformation.BlockLength.BL_8
| a2dp.SbcMediaCodecInformation.BlockLength.BL_12
| a2dp.SbcMediaCodecInformation.BlockLength.BL_16
== SbcMediaCodecInformation.BlockLength.BL_4
| SbcMediaCodecInformation.BlockLength.BL_8
| SbcMediaCodecInformation.BlockLength.BL_12
| SbcMediaCodecInformation.BlockLength.BL_16
)
assert (
sbc_info.subbands
== a2dp.SbcMediaCodecInformation.Subbands.S_4
| a2dp.SbcMediaCodecInformation.Subbands.S_8
== SbcMediaCodecInformation.Subbands.S_4 | SbcMediaCodecInformation.Subbands.S_8
)
assert (
sbc_info.allocation_method
== a2dp.SbcMediaCodecInformation.AllocationMethod.SNR
| a2dp.SbcMediaCodecInformation.AllocationMethod.LOUDNESS
== SbcMediaCodecInformation.AllocationMethod.SNR
| SbcMediaCodecInformation.AllocationMethod.LOUDNESS
)
assert sbc_info.minimum_bitpool_value == 2
assert sbc_info.maximum_bitpool_value == 53
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,
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,
2,
53,
)
@@ -336,36 +329,36 @@ def test_sbc_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 (
aac_info.object_type
== 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
== AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC
| AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LC
| AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LTP
| AacMediaCodecInformation.ObjectType.MPEG_4_AAC_SCALABLE
)
assert (
aac_info.sampling_frequency
== a2dp.AacMediaCodecInformation.SamplingFrequency.SF_44100
| a2dp.AacMediaCodecInformation.SamplingFrequency.SF_48000
== AacMediaCodecInformation.SamplingFrequency.SF_44100
| AacMediaCodecInformation.SamplingFrequency.SF_48000
)
assert (
aac_info.channels
== a2dp.AacMediaCodecInformation.Channels.MONO
| a2dp.AacMediaCodecInformation.Channels.STEREO
== AacMediaCodecInformation.Channels.MONO
| AacMediaCodecInformation.Channels.STEREO
)
assert aac_info.vbr == 1
assert aac_info.bitrate == 256000
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,
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,
1,
256000,
)
@@ -375,159 +368,25 @@ def test_aac_codec_specific_information():
# -----------------------------------------------------------------------------
def test_opus_codec_specific_information():
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
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
assert (
opus_info.sampling_frequency
== a2dp.OpusMediaCodecInformation.SamplingFrequency.SF_48000
== OpusMediaCodecInformation.SamplingFrequency.SF_48000
)
opus_info2 = a2dp.OpusMediaCodecInformation(
a2dp.OpusMediaCodecInformation.ChannelMode.STEREO,
a2dp.OpusMediaCodecInformation.FrameSize.FS_20MS,
a2dp.OpusMediaCodecInformation.SamplingFrequency.SF_48000,
opus_info2 = OpusMediaCodecInformation(
OpusMediaCodecInformation.ChannelMode.STEREO,
OpusMediaCodecInformation.FrameSize.FS_20MS,
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()
@@ -535,12 +394,6 @@ 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()
# -----------------------------------------------------------------------------
+36 -95
View File
@@ -15,108 +15,43 @@
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
import pytest
from bumble import avdtp
from bumble.a2dp import A2DP_SBC_CODEC_TYPE
from bumble.avdtp import (
AVDTP_AUDIO_MEDIA_TYPE,
AVDTP_DELAY_REPORTING_SERVICE_CATEGORY,
AVDTP_GET_CAPABILITIES,
AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY,
AVDTP_SET_CONFIGURATION,
Get_Capabilities_Response,
MediaCodecCapabilities,
Message,
ServiceCapabilities,
Set_Configuration_Command,
)
from bumble.rtp import MediaPacket
# -----------------------------------------------------------------------------
@pytest.mark.parametrize(
'message',
(
avdtp.Discover_Command(),
avdtp.Discover_Response(
endpoints=[
avdtp.EndPointInfo(
seid=1,
in_use=1,
media_type=avdtp.MediaType.AUDIO,
tsep=avdtp.StreamEndPointType.SNK,
)
]
def test_messages():
capabilities = [
ServiceCapabilities(AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY),
MediaCodecCapabilities(
media_type=AVDTP_AUDIO_MEDIA_TYPE,
media_codec_type=A2DP_SBC_CODEC_TYPE,
media_codec_information=bytes.fromhex('211502fa'),
),
avdtp.Get_Capabilities_Command(acp_seid=1),
avdtp.Get_Capabilities_Response(
capabilities=[
avdtp.ServiceCapabilities(avdtp.AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY),
avdtp.MediaCodecCapabilities(
media_type=avdtp.AVDTP_AUDIO_MEDIA_TYPE,
media_codec_type=A2DP_SBC_CODEC_TYPE,
media_codec_information=bytes.fromhex('211502fa'),
),
avdtp.ServiceCapabilities(avdtp.AVDTP_DELAY_REPORTING_SERVICE_CATEGORY),
]
),
avdtp.Get_Capabilities_Reject(error_code=avdtp.AVDTP_BAD_ACP_SEID_ERROR),
avdtp.Get_All_Capabilities_Command(acp_seid=1),
avdtp.Get_All_Capabilities_Response(
capabilities=[
avdtp.ServiceCapabilities(avdtp.AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY)
]
),
avdtp.Get_All_Capabilities_Reject(error_code=avdtp.AVDTP_BAD_ACP_SEID_ERROR),
avdtp.Set_Configuration_Command(
acp_seid=1,
int_seid=2,
capabilities=[
avdtp.ServiceCapabilities(avdtp.AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY)
],
),
avdtp.Set_Configuration_Response(),
avdtp.Set_Configuration_Reject(
service_category=avdtp.AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY,
error_code=avdtp.AVDTP_UNSUPPORTED_CONFIGURATION_ERROR,
),
avdtp.Get_Configuration_Command(acp_seid=1),
avdtp.Get_Configuration_Response(
capabilities=[
avdtp.ServiceCapabilities(avdtp.AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY)
]
),
avdtp.Get_Configuration_Reject(error_code=avdtp.AVDTP_BAD_ACP_SEID_ERROR),
avdtp.Reconfigure_Command(
acp_seid=1,
capabilities=[
avdtp.ServiceCapabilities(avdtp.AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY)
],
),
avdtp.Reconfigure_Response(),
avdtp.Reconfigure_Reject(
service_category=avdtp.AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY,
error_code=avdtp.AVDTP_UNSUPPORTED_CONFIGURATION_ERROR,
),
avdtp.Open_Command(acp_seid=1),
avdtp.Open_Response(),
avdtp.Open_Reject(error_code=avdtp.AVDTP_BAD_ACP_SEID_ERROR),
avdtp.Start_Command(acp_seids=[1, 2]),
avdtp.Start_Response(),
avdtp.Start_Reject(acp_seid=1, error_code=avdtp.AVDTP_BAD_STATE_ERROR),
avdtp.Close_Command(acp_seid=1),
avdtp.Close_Response(),
avdtp.Close_Reject(error_code=avdtp.AVDTP_BAD_ACP_SEID_ERROR),
avdtp.Suspend_Command(acp_seids=[1, 2]),
avdtp.Suspend_Response(),
avdtp.Suspend_Reject(acp_seid=1, error_code=avdtp.AVDTP_BAD_STATE_ERROR),
avdtp.Abort_Command(acp_seid=1),
avdtp.Abort_Response(),
avdtp.Security_Control_Command(acp_seid=1, data=b'foo'),
avdtp.Security_Control_Response(),
avdtp.Security_Control_Reject(error_code=avdtp.AVDTP_BAD_ACP_SEID_ERROR),
avdtp.General_Reject(),
avdtp.DelayReport_Command(acp_seid=1, delay=100),
avdtp.DelayReport_Response(),
avdtp.DelayReport_Reject(error_code=avdtp.AVDTP_BAD_ACP_SEID_ERROR),
),
)
def test_messages(message: avdtp.Message):
parsed = avdtp.Message.create(
signal_identifier=message.signal_identifier,
message_type=message.message_type,
payload=message.payload,
ServiceCapabilities(AVDTP_DELAY_REPORTING_SERVICE_CATEGORY),
]
message = Get_Capabilities_Response(capabilities)
parsed = Message.create(
AVDTP_GET_CAPABILITIES, Message.MessageType.RESPONSE_ACCEPT, message.payload
)
assert message.payload == parsed.payload
message = Set_Configuration_Command(3, 4, capabilities)
parsed = Message.create(
AVDTP_SET_CONFIGURATION, Message.MessageType.COMMAND, message.payload
)
assert message == parsed
assert message.payload == parsed.payload
@@ -127,3 +62,9 @@ def test_rtp():
)
media_packet = MediaPacket.from_bytes(packet)
print(media_packet)
# -----------------------------------------------------------------------------
if __name__ == '__main__':
test_messages()
test_rtp()
+21 -15
View File
@@ -310,12 +310,12 @@ async def test_pacs():
@pytest.mark.asyncio
async def test_ascs():
devices = TwoDevices()
devices[1].add_service(
AudioStreamControlService(device=devices[1], sink_ase_id=[1, 2])
devices[0].add_service(
AudioStreamControlService(device=devices[0], sink_ase_id=[1, 2])
)
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(
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, 1],
cig_id=[1, 2],
cis_id=[3, 4],
sdu_interval=[5, 6],
framing=[0, 1],
@@ -402,19 +402,25 @@ async def test_ascs():
)
# CIS establishment
cis_handles = await devices[0].setup_cig(
device.CigParameters(
devices[0].emit(
'cis_establishment',
device.CisLink(
device=devices[0],
acl_connection=devices.connections[0],
handle=5,
cis_id=3,
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(
[(cis_handle, devices.connections[0]) for cis_handle in cis_handles]
devices[0].emit(
'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(
[1, AseStateMachine.State.STREAMING]
+10 -41
View File
@@ -24,7 +24,7 @@ from unittest import mock
import pytest
from bumble import device, gatt, hci, utils
from bumble.core import PhysicalTransport
from bumble.core import ConnectionParameters, PhysicalTransport
from bumble.device import (
AdvertisingEventProperties,
AdvertisingParameters,
@@ -289,15 +289,14 @@ 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_le_connection(
device.on_connection(
0x0001,
PhysicalTransport.LE,
peer_address,
None,
None,
Role.PERIPHERAL,
0,
0,
0,
ConnectionParameters(0, 0, 0),
)
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_parameters=AdvertisingParameters(own_address_type=own_address_type)
)
device.on_le_connection(
device.on_connection(
0x0001,
PhysicalTransport.LE,
peer_address,
None,
None,
Role.PERIPHERAL,
0,
0,
0,
ConnectionParameters(0, 0, 0),
)
device.on_advertising_set_termination(
HCI_SUCCESS,
@@ -393,15 +391,14 @@ async def test_extended_advertising_connection_out_of_order(own_address_type):
0x0001,
0,
)
device.on_le_connection(
device.on_connection(
0x0001,
PhysicalTransport.LE,
Address('F0:F1:F2:F3:F4:F5'),
None,
None,
Role.PERIPHERAL,
0,
0,
0,
ConnectionParameters(0, 0, 0),
)
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)
# -----------------------------------------------------------------------------
@pytest.mark.parametrize(
"roles",
(
(hci.Role.PERIPHERAL, hci.Role.CENTRAL),
(hci.Role.CENTRAL, hci.Role.PERIPHERAL),
),
)
@pytest.mark.asyncio
async def test_accept_classic_connection(roles: tuple[hci.Role, hci.Role]):
devices = TwoDevices()
devices[0].classic_enabled = True
devices[1].classic_enabled = True
await devices[0].power_on()
await devices[1].power_on()
accept_task = asyncio.create_task(devices[1].accept(role=roles[1]))
await devices[0].connect(
devices[1].public_address, transport=PhysicalTransport.BR_EDR
)
await accept_task
assert devices.connections[0]
assert devices.connections[0].role == roles[0]
assert devices.connections[1]
assert devices.connections[1].role == roles[1]
# -----------------------------------------------------------------------------
async def run_test_device():
await test_device_connect_parallel()
+1 -3
View File
@@ -82,6 +82,7 @@ async def hap_client():
)
await devices.setup_connection()
# TODO negotiate MTU > 49 to not truncate preset names
# Mock encryption.
devices.connections[0].encryption = 1 # type: ignore
@@ -92,9 +93,6 @@ async def hap_client():
)
peer = device.Peer(devices.connections[1]) # type: ignore
await peer.request_mtu(49)
peer2 = device.Peer(devices.connections[0]) # type: ignore
await peer2.request_mtu(49)
hap_client = await peer.discover_service_and_create_proxy(
hap.HearingAccessServiceProxy
)
+5 -32
View File
@@ -24,7 +24,7 @@ import sys
import pytest
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():
sink1 = Sink()
parser1 = common.PacketParser(sink1)
parser1 = PacketParser(sink1)
sink2 = Sink()
parser2 = common.PacketParser(sink2)
parser2 = PacketParser(sink2)
for parser in [parser1, parser2]:
with open(
@@ -82,7 +82,7 @@ def test_parser():
# -----------------------------------------------------------------------------
def test_parser_extensions():
sink = Sink()
parser = common.PacketParser(sink)
parser = 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", "[::1]"),
("127.0.0.1",),
)
async def test_android_netsim_connection(address):
controller_transport = await transport.open_transport(
@@ -222,33 +222,6 @@ 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()