mirror of
https://github.com/google/bumble.git
synced 2026-05-06 03:38:01 +00:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d112901a17 | ||
|
|
2d74aef0e9 | ||
|
|
f06e19e1ca | ||
|
|
36aefb280d | ||
|
|
227f5cf62e | ||
|
|
1336cfa42c | ||
|
|
0ca7b8b322 | ||
|
|
eef5304a36 | ||
|
|
1a2141126c | ||
|
|
19b7660f88 | ||
|
|
1932f14fb6 | ||
|
|
b70b92097f | ||
|
|
d43f5573a6 | ||
|
|
1982168a9f | ||
|
|
5e1794a15b | ||
|
|
578f7f054d | ||
|
|
4b25b3581d | ||
|
|
9601c7f287 | ||
|
|
dae3ec5cba | ||
|
|
95225a1774 | ||
|
|
e54a26393e | ||
|
|
5dc76cf7b4 | ||
|
|
6c68115660 | ||
|
|
88ef65a4e2 | ||
|
|
324b26d8f2 | ||
|
|
a43b403511 | ||
|
|
c657494362 | ||
|
|
11505f08b7 | ||
|
|
9bf9ed5f59 | ||
|
|
0fa517a4f6 | ||
|
|
a11962a487 | ||
|
|
32d448edf3 | ||
|
|
3d615b13ce | ||
|
|
1ad92dc759 | ||
|
|
aacfd4328c | ||
|
|
6aa1f5211c | ||
|
|
df8e454ee5 | ||
|
|
aec50ac616 | ||
|
|
6a3eaa457f | ||
|
|
6e6b4cd4b2 | ||
|
|
aa1d7933da | ||
|
|
34e0f293c2 | ||
|
|
85215df2c3 | ||
|
|
f8223ca81f | ||
|
|
2b0b1ad726 | ||
|
|
58debcd8bb | ||
|
|
6eba81e3dd | ||
|
|
8a5f6a61d5 |
@@ -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 your 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 you are on a mac, see [these instructions](docs/mkdocs/src/platforms/macos.md).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -21,11 +21,12 @@ import dataclasses
|
||||
import enum
|
||||
import logging
|
||||
import struct
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Awaitable, Callable
|
||||
from collections.abc import AsyncGenerator, Awaitable, Callable
|
||||
from typing import Union
|
||||
|
||||
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 (
|
||||
@@ -59,19 +60,18 @@ logger = logging.getLogger(__name__)
|
||||
# -----------------------------------------------------------------------------
|
||||
# fmt: off
|
||||
|
||||
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
|
||||
class CodecType(utils.OpenIntEnum):
|
||||
SBC = 0x00
|
||||
MPEG_1_2_AUDIO = 0x01
|
||||
MPEG_2_4_AAC = 0x02
|
||||
ATRAC_FAMILY = 0x03
|
||||
NON_A2DP = 0xFF
|
||||
|
||||
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'
|
||||
}
|
||||
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
|
||||
|
||||
|
||||
SBC_SYNC_WORD = 0x9C
|
||||
@@ -259,9 +259,48 @@ 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:
|
||||
class SbcMediaCodecInformation(MediaCodecInformation):
|
||||
'''
|
||||
A2DP spec - 4.3.2 Codec Specific Information Elements
|
||||
'''
|
||||
@@ -345,7 +384,7 @@ class SbcMediaCodecInformation:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclasses.dataclass
|
||||
class AacMediaCodecInformation:
|
||||
class AacMediaCodecInformation(MediaCodecInformation):
|
||||
'''
|
||||
A2DP spec - 4.5.2 Codec Specific Information Elements
|
||||
'''
|
||||
@@ -427,7 +466,7 @@ class AacMediaCodecInformation:
|
||||
|
||||
@dataclasses.dataclass
|
||||
# -----------------------------------------------------------------------------
|
||||
class VendorSpecificMediaCodecInformation:
|
||||
class VendorSpecificMediaCodecInformation(MediaCodecInformation):
|
||||
'''
|
||||
A2DP spec - 4.7.2 Codec Specific Information Elements
|
||||
'''
|
||||
|
||||
@@ -19,10 +19,11 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import struct
|
||||
from collections.abc import Callable
|
||||
from enum import IntEnum
|
||||
from typing import Callable, Optional, cast
|
||||
from typing import Optional
|
||||
|
||||
from bumble import avc, core, l2cap
|
||||
from bumble import core, l2cap
|
||||
from bumble.colors import color
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -144,9 +145,9 @@ class MessageAssembler:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Protocol:
|
||||
CommandHandler = Callable[[int, avc.CommandFrame], None]
|
||||
CommandHandler = Callable[[int, bytes], None]
|
||||
command_handlers: dict[int, CommandHandler] # Command handlers, by PID
|
||||
ResponseHandler = Callable[[int, Optional[avc.ResponseFrame]], None]
|
||||
ResponseHandler = Callable[[int, Optional[bytes]], None]
|
||||
response_handlers: dict[int, ResponseHandler] # Response handlers, by PID
|
||||
next_transaction_label: int
|
||||
message_assembler: MessageAssembler
|
||||
@@ -204,20 +205,15 @@ class Protocol:
|
||||
self.send_ipid(transaction_label, pid)
|
||||
return
|
||||
|
||||
command_frame = cast(avc.CommandFrame, avc.Frame.from_bytes(payload))
|
||||
self.command_handlers[pid](transaction_label, command_frame)
|
||||
self.command_handlers[pid](transaction_label, payload)
|
||||
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.
|
||||
if ipid:
|
||||
response_frame = None
|
||||
else:
|
||||
response_frame = cast(avc.ResponseFrame, avc.Frame.from_bytes(payload))
|
||||
|
||||
self.response_handlers[pid](transaction_label, response_frame)
|
||||
response_payload = None if ipid else payload
|
||||
self.response_handlers[pid](transaction_label, response_payload)
|
||||
|
||||
def send_message(
|
||||
self,
|
||||
|
||||
1097
bumble/avdtp.py
1097
bumble/avdtp.py
File diff suppressed because it is too large
Load Diff
@@ -22,21 +22,9 @@ 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 (
|
||||
AsyncIterator,
|
||||
Awaitable,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
SupportsBytes,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
from typing import ClassVar, Optional, SupportsBytes, TypeVar, Union
|
||||
|
||||
from bumble import avc, avctp, core, hci, l2cap, utils
|
||||
from bumble.colors import color
|
||||
@@ -1762,7 +1750,11 @@ class Protocol(utils.EventEmitter):
|
||||
),
|
||||
)
|
||||
response = self._check_response(response_context, GetCapabilitiesResponse)
|
||||
return cast(List[EventId], response.capabilities)
|
||||
return list(
|
||||
capability
|
||||
for capability in response.capabilities
|
||||
if isinstance(capability, EventId)
|
||||
)
|
||||
|
||||
async def get_play_status(self) -> SongAndPlayStatus:
|
||||
"""Get the play status of the connected peer."""
|
||||
@@ -2012,9 +2004,12 @@ class Protocol(utils.EventEmitter):
|
||||
|
||||
self.emit(self.EVENT_STOP)
|
||||
|
||||
def _on_avctp_command(
|
||||
self, transaction_label: int, command: avc.CommandFrame
|
||||
) -> None:
|
||||
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"
|
||||
)
|
||||
logger.debug(
|
||||
f"<<< AVCTP Command, transaction_label={transaction_label}: " f"{command}"
|
||||
)
|
||||
@@ -2073,8 +2068,13 @@ class Protocol(utils.EventEmitter):
|
||||
self.send_not_implemented_response(transaction_label, command)
|
||||
|
||||
def _on_avctp_response(
|
||||
self, transaction_label: int, response: Optional[avc.ResponseFrame]
|
||||
self, transaction_label: int, payload: Optional[bytes]
|
||||
) -> 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.IMPLEMENTED_OR_STABLE,
|
||||
avc.ResponseFrame.ResponseCode.ACCEPTED,
|
||||
SetAbsoluteVolumeResponse(effective_volume),
|
||||
)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
126
bumble/device.py
126
bumble/device.py
@@ -2169,10 +2169,12 @@ def with_connection_from_handle(function):
|
||||
# Decorator that converts the first argument from a bluetooth address to a connection
|
||||
def with_connection_from_address(function):
|
||||
@functools.wraps(function)
|
||||
def wrapper(self, address: hci.Address, *args, **kwargs):
|
||||
for connection in self.connections.values():
|
||||
def wrapper(device: Device, address: hci.Address, *args, **kwargs):
|
||||
if connection := device.pending_connections.get(address):
|
||||
return function(device, connection, *args, **kwargs)
|
||||
for connection in device.connections.values():
|
||||
if connection.peer_address == address:
|
||||
return function(self, connection, *args, **kwargs)
|
||||
return function(device, connection, *args, **kwargs)
|
||||
raise ObjectLookupError('no connection for address')
|
||||
|
||||
return wrapper
|
||||
@@ -2182,11 +2184,13 @@ def with_connection_from_address(function):
|
||||
# connection
|
||||
def try_with_connection_from_address(function):
|
||||
@functools.wraps(function)
|
||||
def wrapper(self, address, *args, **kwargs):
|
||||
for connection in self.connections.values():
|
||||
def wrapper(device: Device, address: hci.Address, *args, **kwargs):
|
||||
if connection := device.pending_connections.get(address):
|
||||
return function(device, connection, address, *args, **kwargs)
|
||||
for connection in device.connections.values():
|
||||
if connection.peer_address == address:
|
||||
return function(self, connection, address, *args, **kwargs)
|
||||
return function(self, None, address, *args, **kwargs)
|
||||
return function(device, connection, address, *args, **kwargs)
|
||||
return function(device, None, address, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -2234,7 +2238,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
scan_response_data: bytes
|
||||
cs_capabilities: ChannelSoundingCapabilities | None = None
|
||||
connections: dict[int, Connection]
|
||||
connection_roles: dict[hci.Address, hci.Role]
|
||||
pending_connections: dict[hci.Address, Connection]
|
||||
classic_pending_accepts: dict[
|
||||
hci.Address,
|
||||
list[asyncio.Future[Union[Connection, tuple[hci.Address, int, int]]]],
|
||||
@@ -2259,8 +2263,6 @@ 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"
|
||||
@@ -2356,9 +2358,9 @@ class Device(utils.CompositeEventEmitter):
|
||||
self.le_connecting = False
|
||||
self.disconnecting = False
|
||||
self.connections = {} # Connections, by connection handle
|
||||
self.connection_roles = (
|
||||
self.pending_connections = (
|
||||
{}
|
||||
) # Local connection roles, by BD address (BR/EDR only)
|
||||
) # Pending connections, by BD address (BR/EDR only)
|
||||
self.sco_links = {} # ScoLinks, by connection handle (BR/EDR only)
|
||||
self.cis_links = {} # CisLinks, by connection handle (LE only)
|
||||
self._pending_cis = {} # (CIS_ID, CIG_ID), by CIS_handle
|
||||
@@ -3827,7 +3829,17 @@ class Device(utils.CompositeEventEmitter):
|
||||
)
|
||||
else:
|
||||
# Save pending connection
|
||||
self.connection_roles[peer_address] = hci.Role.CENTRAL
|
||||
self.pending_connections[peer_address] = Connection(
|
||||
device=self,
|
||||
handle=0,
|
||||
transport=core.PhysicalTransport.BR_EDR,
|
||||
self_address=self.public_address,
|
||||
self_resolvable_address=None,
|
||||
peer_address=peer_address,
|
||||
peer_resolvable_address=None,
|
||||
role=hci.Role.CENTRAL,
|
||||
parameters=Connection.Parameters(0, 0, 0),
|
||||
)
|
||||
|
||||
# TODO: allow passing other settings
|
||||
result = await self.send_command(
|
||||
@@ -3880,7 +3892,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
self.le_connecting = False
|
||||
self.connect_own_address_type = None
|
||||
else:
|
||||
self.connection_roles.pop(peer_address, None)
|
||||
self.pending_connections.pop(peer_address, None)
|
||||
|
||||
async def accept(
|
||||
self,
|
||||
@@ -3978,7 +3990,17 @@ class Device(utils.CompositeEventEmitter):
|
||||
# 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.connection_roles[peer_address] = hci.Role.PERIPHERAL
|
||||
self.pending_connections[peer_address] = Connection(
|
||||
device=self,
|
||||
handle=0,
|
||||
transport=core.PhysicalTransport.BR_EDR,
|
||||
self_address=self.public_address,
|
||||
self_resolvable_address=None,
|
||||
peer_address=peer_address,
|
||||
peer_resolvable_address=None,
|
||||
role=hci.Role.PERIPHERAL,
|
||||
parameters=Connection.Parameters(0, 0, 0),
|
||||
)
|
||||
|
||||
try:
|
||||
# Accept connection request
|
||||
@@ -3996,7 +4018,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
finally:
|
||||
self.remove_listener(self.EVENT_CONNECTION, on_connection)
|
||||
self.remove_listener(self.EVENT_CONNECTION_FAILURE, on_connection_failure)
|
||||
self.connection_roles.pop(peer_address, None)
|
||||
self.pending_connections.pop(peer_address, None)
|
||||
|
||||
@asynccontextmanager
|
||||
async def connect_as_gatt(self, peer_address: Union[hci.Address, str]):
|
||||
@@ -4703,7 +4725,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.pop(cis_handle)
|
||||
cis_id, cig_id = self._pending_cis[cis_handle]
|
||||
self.cis_links[cis_handle] = CisLink(
|
||||
device=self,
|
||||
acl_connection=acl_connection,
|
||||
@@ -4719,6 +4741,7 @@ 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)
|
||||
|
||||
@@ -5441,29 +5464,27 @@ class Device(utils.CompositeEventEmitter):
|
||||
connection_handle: int,
|
||||
peer_address: hci.Address,
|
||||
) -> None:
|
||||
connection_role = self.connection_roles.pop(peer_address, hci.Role.PERIPHERAL)
|
||||
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(
|
||||
f'*** Connection: [0x{connection_handle:04X}] '
|
||||
f'{peer_address} {hci.HCI_Constant.role_name(connection_role)}'
|
||||
)
|
||||
logger.debug('*** %s', connection)
|
||||
if connection_handle in self.connections:
|
||||
logger.warning(
|
||||
'new connection reuses the same handle as a previous connection'
|
||||
)
|
||||
|
||||
# Create a new connection
|
||||
connection = Connection(
|
||||
device=self,
|
||||
handle=connection_handle,
|
||||
transport=PhysicalTransport.BR_EDR,
|
||||
self_address=self.public_address,
|
||||
self_resolvable_address=None,
|
||||
peer_address=peer_address,
|
||||
peer_resolvable_address=None,
|
||||
role=connection_role,
|
||||
parameters=Connection.Parameters(0.0, 0, 0.0),
|
||||
)
|
||||
self.connections[connection_handle] = connection
|
||||
|
||||
self.emit(self.EVENT_CONNECTION, connection)
|
||||
@@ -5618,7 +5639,9 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
# FIXME: Explore a delegate-model for BR/EDR wait connection #56.
|
||||
@host_event_handler
|
||||
def on_connection_request(self, bd_addr, class_of_device, link_type):
|
||||
def on_connection_request(
|
||||
self, bd_addr: hci.Address, class_of_device: int, link_type: int
|
||||
):
|
||||
logger.debug(f'*** Connection request: {bd_addr}')
|
||||
|
||||
# Handle SCO request.
|
||||
@@ -5647,7 +5670,17 @@ class Device(utils.CompositeEventEmitter):
|
||||
# device configuration is set to accept any incoming connection
|
||||
elif self.classic_accept_any:
|
||||
# Save pending connection
|
||||
self.connection_roles[bd_addr] = hci.Role.PERIPHERAL
|
||||
self.pending_connections[bd_addr] = Connection(
|
||||
device=self,
|
||||
handle=0,
|
||||
transport=core.PhysicalTransport.BR_EDR,
|
||||
self_address=self.public_address,
|
||||
self_resolvable_address=None,
|
||||
peer_address=bd_addr,
|
||||
peer_resolvable_address=None,
|
||||
role=hci.Role.PERIPHERAL,
|
||||
parameters=Connection.Parameters(0, 0, 0),
|
||||
)
|
||||
|
||||
self.host.send_command_sync(
|
||||
hci.HCI_Accept_Connection_Request_Command(
|
||||
@@ -5958,7 +5991,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
@host_event_handler
|
||||
@try_with_connection_from_address
|
||||
def on_remote_name(
|
||||
self, connection: Connection, address: hci.Address, remote_name: bytes
|
||||
self, connection: Optional[Connection], address: hci.Address, remote_name: bytes
|
||||
):
|
||||
# Try to decode the name
|
||||
try:
|
||||
@@ -5977,7 +6010,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
@host_event_handler
|
||||
@try_with_connection_from_address
|
||||
def on_remote_name_failure(
|
||||
self, connection: Connection, address: hci.Address, error: int
|
||||
self, connection: Optional[Connection], address: hci.Address, error: int
|
||||
):
|
||||
if connection:
|
||||
connection.emit(connection.EVENT_REMOTE_NAME_FAILURE, error)
|
||||
@@ -6409,21 +6442,20 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@try_with_connection_from_address
|
||||
@with_connection_from_address
|
||||
def on_role_change(
|
||||
self, connection: Connection, peer_address: hci.Address, new_role: hci.Role
|
||||
self,
|
||||
connection: Connection,
|
||||
new_role: hci.Role,
|
||||
):
|
||||
if connection:
|
||||
connection.role = new_role
|
||||
connection.emit(connection.EVENT_ROLE_CHANGE, new_role)
|
||||
else:
|
||||
self.connection_roles[peer_address] = new_role
|
||||
connection.role = new_role
|
||||
connection.emit(connection.EVENT_ROLE_CHANGE, new_role)
|
||||
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@try_with_connection_from_address
|
||||
def on_role_change_failure(
|
||||
self, connection: Connection, address: hci.Address, error: int
|
||||
self, connection: Optional[Connection], address: hci.Address, error: int
|
||||
):
|
||||
if connection:
|
||||
connection.emit(connection.EVENT_ROLE_CHANGE_FAILURE, error)
|
||||
|
||||
@@ -49,6 +49,10 @@ 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:
|
||||
|
||||
@@ -459,6 +459,10 @@ 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.
|
||||
@@ -598,17 +602,39 @@ class Driver(common.Driver):
|
||||
await self.reset_complete.wait()
|
||||
logger.debug("reset complete")
|
||||
|
||||
# Load the device config if there is one.
|
||||
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.
|
||||
if self.ddc_override:
|
||||
logger.debug("loading overridden DDC")
|
||||
await self.load_device_config(self.ddc_override)
|
||||
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)
|
||||
# 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)
|
||||
if self.ddc_addon:
|
||||
logger.debug("loading DDC addon")
|
||||
await self.load_device_config(self.ddc_addon)
|
||||
|
||||
@@ -115,12 +115,14 @@ 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),
|
||||
|
||||
@@ -3441,6 +3441,17 @@ 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
|
||||
@@ -4338,6 +4349,15 @@ 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
|
||||
@@ -4365,6 +4385,15 @@ 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
|
||||
@@ -5028,6 +5057,15 @@ 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
|
||||
|
||||
@@ -489,9 +489,9 @@ STATUS_CODES = {
|
||||
|
||||
@dataclasses.dataclass
|
||||
class HfConfiguration:
|
||||
supported_hf_features: list[HfFeature]
|
||||
supported_hf_indicators: list[HfIndicator]
|
||||
supported_audio_codecs: list[AudioCodec]
|
||||
supported_hf_features: collections.abc.Sequence[HfFeature]
|
||||
supported_hf_indicators: collections.abc.Sequence[HfIndicator]
|
||||
supported_audio_codecs: collections.abc.Sequence[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 = configuration.supported_audio_codecs
|
||||
self.supported_audio_codecs = list(configuration.supported_audio_codecs)
|
||||
|
||||
self.hf_indicators = {
|
||||
indicator: HfIndicatorState(indicator=indicator)
|
||||
|
||||
@@ -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_CONTROL_PSM)
|
||||
l2cap.ClassicChannelSpec(HID_INTERRUPT_PSM)
|
||||
)
|
||||
channel.sink = self.on_intr_pdu
|
||||
self.l2cap_intr_channel = channel
|
||||
|
||||
@@ -550,7 +550,7 @@ class Host(utils.EventEmitter):
|
||||
logger.debug(
|
||||
'HCI LE flow control: '
|
||||
f'le_acl_data_packet_length={le_acl_data_packet_length},'
|
||||
f'total_num_le_acl_data_packets={total_num_le_acl_data_packets}'
|
||||
f'total_num_le_acl_data_packets={total_num_le_acl_data_packets},'
|
||||
f'iso_data_packet_length={iso_data_packet_length},'
|
||||
f'total_num_iso_data_packets={total_num_iso_data_packets}'
|
||||
)
|
||||
|
||||
@@ -273,12 +273,19 @@ class HearingAccessService(gatt.TemplateService):
|
||||
def on_disconnection(_reason) -> None:
|
||||
self.currently_connected_clients.discard(connection)
|
||||
|
||||
@connection.on(connection.EVENT_CONNECTION_ATT_MTU_UPDATE)
|
||||
def on_mtu_update(*_: Any) -> None:
|
||||
self.on_incoming_connection(connection)
|
||||
|
||||
@connection.on(connection.EVENT_CONNECTION_ENCRYPTION_CHANGE)
|
||||
def on_encryption_change(*_: Any) -> None:
|
||||
self.on_incoming_connection(connection)
|
||||
|
||||
@connection.on(connection.EVENT_PAIRING)
|
||||
def on_pairing(*_: Any) -> None:
|
||||
self.on_incoming_paired_connection(connection)
|
||||
self.on_incoming_connection(connection)
|
||||
|
||||
if connection.peer_resolvable_address:
|
||||
self.on_incoming_paired_connection(connection)
|
||||
self.on_incoming_connection(connection)
|
||||
|
||||
self.hearing_aid_features_characteristic = gatt.Characteristic(
|
||||
uuid=gatt.GATT_HEARING_AID_FEATURES_CHARACTERISTIC,
|
||||
@@ -315,9 +322,30 @@ class HearingAccessService(gatt.TemplateService):
|
||||
]
|
||||
)
|
||||
|
||||
def on_incoming_paired_connection(self, connection: Connection):
|
||||
def on_incoming_connection(self, connection: Connection):
|
||||
'''Setup initial operations to handle a remote bonded HAP device'''
|
||||
# TODO Should we filter on HAP device only ?
|
||||
|
||||
if not connection.is_encrypted:
|
||||
logging.debug(f'HAS: {connection.peer_address} is not encrypted')
|
||||
return
|
||||
|
||||
if not connection.peer_resolvable_address:
|
||||
logging.debug(f'HAS: {connection.peer_address} is not paired')
|
||||
return
|
||||
|
||||
if connection.att_mtu < 49:
|
||||
logging.debug(
|
||||
f'HAS: {connection.peer_address} invalid MTU={connection.att_mtu}'
|
||||
)
|
||||
return
|
||||
|
||||
if connection.peer_address in self.currently_connected_clients:
|
||||
logging.debug(
|
||||
f'HAS: Already connected to {connection.peer_address} nothing to do'
|
||||
)
|
||||
return
|
||||
|
||||
self.currently_connected_clients.add(connection)
|
||||
if (
|
||||
connection.peer_address
|
||||
@@ -457,6 +485,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
connection,
|
||||
self.hearing_aid_preset_control_point,
|
||||
value=op_list[0].to_bytes(len(op_list) == 1),
|
||||
force=True, # TODO GATT notification subscription should be persistent
|
||||
)
|
||||
# Remove item once sent, and keep the non sent item in the list
|
||||
op_list.pop(0)
|
||||
|
||||
@@ -674,10 +674,14 @@ 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]) + self.tx_buffer[: self.mtu - 1]
|
||||
self.tx_buffer = self.tx_buffer[len(chunk) - 1 :]
|
||||
chunk = bytes([rx_credits_needed])
|
||||
self.rx_credits += rx_credits_needed
|
||||
tx_credit_spent = len(chunk) > 1
|
||||
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
|
||||
else:
|
||||
chunk = self.tx_buffer[: self.mtu]
|
||||
self.tx_buffer = self.tx_buffer[len(chunk) :]
|
||||
|
||||
@@ -84,7 +84,12 @@ async def open_transport(name: str) -> Transport:
|
||||
scheme, *tail = name.split(':', 1)
|
||||
spec = tail[0] if tail else None
|
||||
metadata = None
|
||||
if spec and (m := re.search(r'\[(\w+=\w+(?:,\w+=\w+)*,?)\]', spec)):
|
||||
# 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>
|
||||
|
||||
@@ -131,7 +131,11 @@ def publish_grpc_port(grpc_port: int, instance_number: int) -> bool:
|
||||
|
||||
def cleanup():
|
||||
logger.debug("removing .ini file")
|
||||
ini_file.unlink()
|
||||
try:
|
||||
ini_file.unlink()
|
||||
except OSError as error:
|
||||
# Don't log at exception level, since this may happen normally.
|
||||
logger.debug(f'failed to remove .ini file ({error})')
|
||||
|
||||
atexit.register(cleanup)
|
||||
return True
|
||||
|
||||
@@ -22,6 +22,7 @@ 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
|
||||
@@ -389,15 +390,17 @@ class PumpedPacketSource(ParserSource):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PumpedPacketSink:
|
||||
def __init__(self, send):
|
||||
pump_task: Optional[asyncio.Task[None]]
|
||||
|
||||
def __init__(self, send: Callable[[bytes], Awaitable[Any]]):
|
||||
self.send_function = send
|
||||
self.packet_queue = asyncio.Queue()
|
||||
self.packet_queue = asyncio.Queue[bytes]()
|
||||
self.pump_task = None
|
||||
|
||||
def on_packet(self, packet: bytes) -> None:
|
||||
self.packet_queue.put_nowait(packet)
|
||||
|
||||
def start(self):
|
||||
def start(self) -> None:
|
||||
async def pump_packets():
|
||||
while True:
|
||||
try:
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import serial_asyncio
|
||||
|
||||
@@ -28,25 +29,56 @@ from bumble.transport.common import StreamPacketSink, StreamPacketSource, Transp
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
DEFAULT_POST_OPEN_DELAY = 0.5 # in seconds
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes and Functions
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class SerialPacketSource(StreamPacketSource):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._ready = asyncio.Event()
|
||||
|
||||
async def wait_until_ready(self) -> None:
|
||||
await self._ready.wait()
|
||||
|
||||
def connection_made(self, transport: asyncio.BaseTransport) -> None:
|
||||
logger.debug('connection made')
|
||||
self._ready.set()
|
||||
|
||||
def connection_lost(self, exc: Optional[Exception]) -> None:
|
||||
logger.debug('connection lost')
|
||||
self.on_transport_lost()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_serial_transport(spec: str) -> Transport:
|
||||
'''
|
||||
Open a serial port transport.
|
||||
The parameter string has this syntax:
|
||||
<device-path>[,<speed>][,rtscts][,dsrdtr]
|
||||
<device-path>[,<speed>][,rtscts][,dsrdtr][,delay]
|
||||
When <speed> is omitted, the default value of 1000000 is used
|
||||
When "rtscts" is specified, RTS/CTS hardware flow control is enabled
|
||||
When "dsrdtr" is specified, DSR/DTR hardware flow control is enabled
|
||||
When "delay" is specified, a short delay is added after opening the port
|
||||
|
||||
Examples:
|
||||
/dev/tty.usbmodem0006839912172
|
||||
/dev/tty.usbmodem0006839912172,1000000
|
||||
/dev/tty.usbmodem0006839912172,rtscts
|
||||
/dev/tty.usbmodem0006839912172,rtscts,delay
|
||||
'''
|
||||
|
||||
speed = 1000000
|
||||
rtscts = False
|
||||
dsrdtr = False
|
||||
delay = 0.0
|
||||
if ',' in spec:
|
||||
parts = spec.split(',')
|
||||
device = parts[0]
|
||||
@@ -55,13 +87,16 @@ async def open_serial_transport(spec: str) -> Transport:
|
||||
rtscts = True
|
||||
elif part == 'dsrdtr':
|
||||
dsrdtr = True
|
||||
elif part == 'delay':
|
||||
delay = DEFAULT_POST_OPEN_DELAY
|
||||
elif part.isnumeric():
|
||||
speed = int(part)
|
||||
else:
|
||||
device = spec
|
||||
|
||||
serial_transport, packet_source = await serial_asyncio.create_serial_connection(
|
||||
asyncio.get_running_loop(),
|
||||
StreamPacketSource,
|
||||
SerialPacketSource,
|
||||
device,
|
||||
baudrate=speed,
|
||||
rtscts=rtscts,
|
||||
@@ -69,4 +104,23 @@ async def open_serial_transport(spec: str) -> Transport:
|
||||
)
|
||||
packet_sink = StreamPacketSink(serial_transport)
|
||||
|
||||
logger.debug('waiting for the port to be ready')
|
||||
await packet_source.wait_until_ready()
|
||||
logger.debug('port is ready')
|
||||
|
||||
# Try to assert DTR
|
||||
assert serial_transport.serial is not None
|
||||
try:
|
||||
serial_transport.serial.dtr = True
|
||||
logger.debug(
|
||||
f"DSR={serial_transport.serial.dsr}, DTR={serial_transport.serial.dtr}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f'could not assert DTR: {e}')
|
||||
|
||||
# Wait a bit after opening the port, if requested
|
||||
if delay > 0.0:
|
||||
logger.debug(f'waiting {delay} seconds after opening the port')
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
return Transport(packet_source, packet_sink)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
|
||||
import websockets.client
|
||||
import websockets.asyncio.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.client.connect(spec)
|
||||
websocket = await websockets.asyncio.client.connect(spec)
|
||||
|
||||
class WsTransport(PumpedTransport):
|
||||
async def close(self):
|
||||
|
||||
@@ -16,8 +16,9 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import websockets
|
||||
import websockets.asyncio.server
|
||||
|
||||
from bumble.transport.common import ParserSource, PumpedPacketSink, Transport
|
||||
|
||||
@@ -40,7 +41,12 @@ async def open_ws_server_transport(spec: str) -> Transport:
|
||||
'''
|
||||
|
||||
class WsServerTransport(Transport):
|
||||
def __init__(self):
|
||||
sink: PumpedPacketSink
|
||||
source: ParserSource
|
||||
connection: Optional[websockets.asyncio.server.ServerConnection]
|
||||
server: Optional[websockets.asyncio.server.Server]
|
||||
|
||||
def __init__(self) -> None:
|
||||
source = ParserSource()
|
||||
sink = PumpedPacketSink(self.send_packet)
|
||||
self.connection = None
|
||||
@@ -48,17 +54,19 @@ async def open_ws_server_transport(spec: str) -> Transport:
|
||||
|
||||
super().__init__(source, sink)
|
||||
|
||||
async def serve(self, local_host, local_port):
|
||||
async def serve(self, local_host: str, local_port: str) -> None:
|
||||
self.sink.start()
|
||||
# pylint: disable-next=no-member
|
||||
self.server = await websockets.serve(
|
||||
ws_handler=self.on_connection,
|
||||
self.server = await websockets.asyncio.server.serve(
|
||||
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):
|
||||
async def on_connection(
|
||||
self, connection: websockets.asyncio.server.ServerConnection
|
||||
) -> None:
|
||||
logger.debug(
|
||||
f'new connection on {connection.local_address} '
|
||||
f'from {connection.remote_address}'
|
||||
@@ -77,11 +85,11 @@ async def open_ws_server_transport(spec: str) -> Transport:
|
||||
# We're now disconnected
|
||||
self.connection = None
|
||||
|
||||
async def send_packet(self, packet):
|
||||
async def send_packet(self, packet: bytes) -> None:
|
||||
if self.connection is None:
|
||||
logger.debug('no connection, dropping packet')
|
||||
return
|
||||
return await self.connection.send(packet)
|
||||
await self.connection.send(packet)
|
||||
|
||||
local_host, local_port = spec.rsplit(':', maxsplit=1)
|
||||
transport = WsServerTransport()
|
||||
|
||||
@@ -4,9 +4,18 @@ SERIAL TRANSPORT
|
||||
The serial transport implements sending/receiving HCI packets over a UART (a.k.a serial port).
|
||||
|
||||
## Moniker
|
||||
The moniker syntax for a serial transport is: `serial:<device-path>[,<speed>]`
|
||||
When `<speed>` is omitted, the default value of 1000000 is used
|
||||
The moniker syntax for a serial transport is:
|
||||
`<device-path>[,<speed>][,rtscts][,dsrdtr][,delay]`
|
||||
|
||||
When `<speed>` is omitted, the default value of 1000000 is used.
|
||||
When `rtscts` is specified, RTS/CTS hardware flow control is enabled.
|
||||
When `dsrdtr` is specified, DSR/DTR hardware flow control is enabled.
|
||||
When `delay` is specified, a short delay is added after opening the port.
|
||||
|
||||
!!! example
|
||||
`serial:/dev/tty.usbmodem0006839912172,1000000`
|
||||
Opens the serial port `/dev/tty.usbmodem0006839912172` at `1000000`bps
|
||||
```
|
||||
/dev/tty.usbmodem0006839912172
|
||||
/dev/tty.usbmodem0006839912172,1000000
|
||||
/dev/tty.usbmodem0006839912172,rtscts
|
||||
/dev/tty.usbmodem0006839912172,rtscts,delay
|
||||
```
|
||||
@@ -20,7 +20,7 @@ import json
|
||||
import struct
|
||||
import sys
|
||||
|
||||
import websockets
|
||||
import websockets.asyncio.server
|
||||
|
||||
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, _path):
|
||||
async def serve(websocket: websockets.asyncio.server.ServerConnection):
|
||||
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.serve(serve, 'localhost', 8989)
|
||||
await websockets.asyncio.server.serve(serve, 'localhost', 8989)
|
||||
await asyncio.get_event_loop().create_future()
|
||||
else:
|
||||
message = bytes('hello', 'ascii')
|
||||
|
||||
@@ -20,7 +20,7 @@ import logging
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
import websockets
|
||||
import websockets.asyncio.server
|
||||
|
||||
import bumble.logging
|
||||
from bumble import data_types, decoder, gatt
|
||||
@@ -29,12 +29,11 @@ from bumble.device import AdvertisingParameters, Device
|
||||
from bumble.profiles import asha
|
||||
from bumble.transport import open_transport
|
||||
|
||||
ws_connection: Optional[websockets.WebSocketServerProtocol] = None
|
||||
ws_connection: Optional[websockets.asyncio.server.ServerConnection] = None
|
||||
g722_decoder = decoder.G722Decoder()
|
||||
|
||||
|
||||
async def ws_server(ws_client: websockets.WebSocketServerProtocol, path: str):
|
||||
del path
|
||||
async def ws_server(ws_client: websockets.asyncio.server.ServerConnection):
|
||||
global ws_connection
|
||||
ws_connection = ws_client
|
||||
|
||||
@@ -100,7 +99,7 @@ async def main() -> None:
|
||||
),
|
||||
)
|
||||
|
||||
await websockets.serve(ws_server, port=8888)
|
||||
await websockets.asyncio.server.serve(ws_server, port=8888)
|
||||
|
||||
await hci_transport.source.terminated
|
||||
|
||||
|
||||
@@ -21,8 +21,9 @@ import asyncio
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
import websockets
|
||||
import websockets.asyncio.server
|
||||
|
||||
import bumble.logging
|
||||
from bumble import a2dp, avc, avdtp, avrcp, utils
|
||||
@@ -217,6 +218,8 @@ 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:
|
||||
@@ -227,9 +230,9 @@ class WebSocketServer:
|
||||
|
||||
async def start(self) -> None:
|
||||
# pylint: disable-next=no-member
|
||||
await websockets.serve(self.serve, 'localhost', 8989) # type: ignore
|
||||
await websockets.asyncio.server.serve(self.serve, 'localhost', 8989) # type: ignore
|
||||
|
||||
async def serve(self, socket, _path) -> None:
|
||||
async def serve(self, socket: websockets.asyncio.server.ServerConnection) -> None:
|
||||
print('### WebSocket connected')
|
||||
self.socket = socket
|
||||
while True:
|
||||
|
||||
@@ -19,6 +19,7 @@ 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 (
|
||||
@@ -61,7 +62,7 @@ async def main() -> None:
|
||||
host_sink=hci_transport.sink,
|
||||
link=link,
|
||||
)
|
||||
controller1.random_address = sys.argv[1]
|
||||
controller1.random_address = hci.Address(sys.argv[1])
|
||||
|
||||
# Create a second controller using the same link
|
||||
controller2 = Controller('C2', link=link)
|
||||
|
||||
@@ -22,7 +22,7 @@ import logging
|
||||
import sys
|
||||
from typing import Iterable, Optional
|
||||
|
||||
import websockets
|
||||
import websockets.asyncio.server
|
||||
|
||||
import bumble.core
|
||||
import bumble.logging
|
||||
@@ -33,7 +33,7 @@ from bumble.transport import open_transport
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ws: Optional[websockets.WebSocketServerProtocol] = None
|
||||
ws: Optional[websockets.asyncio.server.ServerConnection] = None
|
||||
ag_protocol: Optional[hfp.AgProtocol] = None
|
||||
source_file: Optional[io.BufferedReader] = None
|
||||
|
||||
@@ -114,8 +114,7 @@ def on_hfp_state_change(connected: bool):
|
||||
send_message(type='hfp_state_change', connected=connected)
|
||||
|
||||
|
||||
async def ws_server(ws_client: websockets.WebSocketServerProtocol, path: str):
|
||||
del path
|
||||
async def ws_server(ws_client: websockets.asyncio.server.ServerConnection):
|
||||
global ws
|
||||
ws = ws_client
|
||||
|
||||
@@ -273,7 +272,7 @@ async def main() -> None:
|
||||
|
||||
on_dlc(session)
|
||||
|
||||
await websockets.serve(ws_server, port=8888)
|
||||
await websockets.asyncio.server.serve(ws_server, port=8888)
|
||||
|
||||
if len(sys.argv) >= 5:
|
||||
global source_file
|
||||
|
||||
@@ -22,7 +22,7 @@ import json
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
import websockets
|
||||
import websockets.asyncio.server
|
||||
|
||||
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.WebSocketServerProtocol] = None
|
||||
ws: Optional[websockets.asyncio.server.ServerConnection] = 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.WebSocketServerProtocol, _path):
|
||||
async def serve(websocket: websockets.asyncio.server.ServerConnection):
|
||||
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.serve(serve, 'localhost', 8989)
|
||||
await websockets.asyncio.server.serve(serve, 'localhost', 8989)
|
||||
|
||||
await hci_transport.source.wait_for_termination()
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import json
|
||||
import struct
|
||||
import sys
|
||||
|
||||
import websockets
|
||||
import websockets.asyncio.server
|
||||
|
||||
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, _path):
|
||||
async def serve(websocket: websockets.asyncio.server.ServerConnection):
|
||||
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.serve(serve, 'localhost', 8989)
|
||||
await websockets.asyncio.server.serve(serve, 'localhost', 8989)
|
||||
await asyncio.get_event_loop().create_future()
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import json
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
import websockets
|
||||
import websockets.asyncio.server
|
||||
|
||||
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.WebSocketServerProtocol] = None
|
||||
ws: Optional[websockets.asyncio.server.ServerConnection] = 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.WebSocketServerProtocol, _path):
|
||||
async def serve(websocket: websockets.asyncio.server.ServerConnection):
|
||||
nonlocal ws
|
||||
ws = websocket
|
||||
async for message in websocket:
|
||||
@@ -173,7 +173,7 @@ async def main() -> None:
|
||||
)
|
||||
ws = None
|
||||
|
||||
await websockets.serve(serve, 'localhost', 8989)
|
||||
await websockets.asyncio.server.serve(serve, 'localhost', 8989)
|
||||
|
||||
await hci_transport.source.terminated
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import secrets
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
import websockets
|
||||
import websockets.asyncio.server
|
||||
|
||||
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.WebSocketServerProtocol] = None
|
||||
ws: Optional[websockets.asyncio.server.ServerConnection] = 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.WebSocketServerProtocol, _path):
|
||||
async def serve(websocket: websockets.asyncio.server.ServerConnection):
|
||||
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.serve(serve, 'localhost', 8989)
|
||||
await websockets.asyncio.server.serve(serve, 'localhost', 8989)
|
||||
|
||||
await hci_transport.source.terminated
|
||||
|
||||
|
||||
@@ -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 == 13.1; platform_system!='Emscripten'",
|
||||
"websockets >= 15.0.1; platform_system!='Emscripten'",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
196
rust/Cargo.lock
generated
196
rust/Cargo.lock
generated
@@ -61,7 +61,7 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -71,7 +71,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -249,7 +249,7 @@ dependencies = [
|
||||
"atty",
|
||||
"bitflags 1.3.2",
|
||||
"clap_lex 0.2.4",
|
||||
"indexmap",
|
||||
"indexmap 1.9.3",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
@@ -451,7 +451,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -483,24 +483,19 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.3"
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
|
||||
dependencies = [
|
||||
"errno-dragonfly",
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno-dragonfly"
|
||||
version = "0.1.2"
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -683,9 +678,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.21"
|
||||
version = "0.3.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833"
|
||||
checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
@@ -693,7 +688,7 @@ dependencies = [
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http",
|
||||
"indexmap",
|
||||
"indexmap 2.11.3",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -706,6 +701,12 @@ version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
@@ -827,7 +828,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
"hashbrown 0.12.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92119844f513ffa41556430369ab02c295a3578af21cf945caa3e9e0c2481ac3"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -856,7 +867,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.2",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -891,9 +902,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.147"
|
||||
version = "0.2.175"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
|
||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
|
||||
[[package]]
|
||||
name = "libusb1-sys"
|
||||
@@ -920,9 +931,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.5"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
|
||||
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
@@ -996,13 +1007,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.8"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1073,9 +1084,9 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.60"
|
||||
version = "0.10.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800"
|
||||
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"cfg-if",
|
||||
@@ -1105,9 +1116,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.96"
|
||||
version = "0.9.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f"
|
||||
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -1153,7 +1164,7 @@ dependencies = [
|
||||
"libc",
|
||||
"redox_syscall 0.3.5",
|
||||
"smallvec",
|
||||
"windows-targets",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1542,15 +1553,15 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.10"
|
||||
version = "0.38.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed6248e1caa625eb708e266e06159f135e8c26f2bb7ceb72dc4b2766d0340964"
|
||||
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1580,7 +1591,7 @@ version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1702,12 +1713,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.3"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877"
|
||||
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1773,7 +1784,7 @@ dependencies = [
|
||||
"fastrand",
|
||||
"redox_syscall 0.3.5",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1828,9 +1839,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.32.0"
|
||||
version = "1.38.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
|
||||
checksum = "68722da18b0fc4a05fdc1120b302b82051265792a1e1b399086e9b204b10ad3d"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@@ -1840,16 +1851,16 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.5.3",
|
||||
"socket2 0.5.10",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.1.0"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
|
||||
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2130,7 +2141,16 @@ version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2139,13 +2159,29 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
"windows_aarch64_gnullvm 0.48.5",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm 0.48.5",
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2154,42 +2190,90 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
@@ -2197,5 +2281,5 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
@@ -22,7 +22,7 @@ always_include_features = ["anyhow", "pyo3-asyncio-attributes", "dev-tools", "bu
|
||||
[dependencies]
|
||||
pyo3 = { version = "0.18.3", features = ["macros"] }
|
||||
pyo3-asyncio = { version = "0.18.0", features = ["tokio-runtime"] }
|
||||
tokio = { version = "1.28.2", features = ["macros", "signal"] }
|
||||
tokio = { version = "1.38.2", features = ["macros", "signal"] }
|
||||
nom = "7.1.3"
|
||||
strum = "0.25.0"
|
||||
strum_macros = "0.25.0"
|
||||
@@ -50,7 +50,7 @@ reqwest = { version = "0.11.20", features = ["blocking"], optional = true }
|
||||
rusb = { version = "0.9.2", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.28.2", features = ["full"] }
|
||||
tokio = { version = "1.38.2", features = ["full"] }
|
||||
tempfile = "3.6.0"
|
||||
nix = "0.26.2"
|
||||
anyhow = "1.0.71"
|
||||
|
||||
@@ -23,18 +23,7 @@ from typing import Awaitable
|
||||
|
||||
import pytest
|
||||
|
||||
from bumble import a2dp
|
||||
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 import a2dp, avdtp
|
||||
from bumble.controller import Controller
|
||||
from bumble.core import PhysicalTransport
|
||||
from bumble.device import Device
|
||||
@@ -135,9 +124,9 @@ async def test_self_connection():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def source_codec_capabilities():
|
||||
return MediaCodecCapabilities(
|
||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||
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,
|
||||
@@ -152,9 +141,9 @@ def source_codec_capabilities():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def sink_codec_capabilities():
|
||||
return MediaCodecCapabilities(
|
||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||
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
|
||||
@@ -201,7 +190,7 @@ async def test_source_sink_1():
|
||||
sink.on('rtp_packet', on_rtp_packet)
|
||||
|
||||
# Create a listener to wait for AVDTP connections
|
||||
listener = Listener.for_device(two_devices.devices[1])
|
||||
listener = avdtp.Listener.for_device(two_devices.devices[1])
|
||||
listener.on('connection', on_avdtp_connection)
|
||||
|
||||
async def make_connection():
|
||||
@@ -214,13 +203,13 @@ async def test_source_sink_1():
|
||||
return connections[0]
|
||||
|
||||
connection = await make_connection()
|
||||
client = await Protocol.connect(connection)
|
||||
client = await avdtp.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_AUDIO_MEDIA_TYPE
|
||||
assert remote_sink.tsep == AVDTP_TSEP_SNK
|
||||
assert remote_sink.media_type == avdtp.MediaType.AUDIO
|
||||
assert remote_sink.tsep == avdtp.StreamEndPointType.SNK
|
||||
|
||||
async def generate_packets(packet_count):
|
||||
sequence_number = 0
|
||||
@@ -239,24 +228,24 @@ async def test_source_sink_1():
|
||||
rtp_packets_fully_received = asyncio.get_running_loop().create_future()
|
||||
rtp_packets_expected = 3
|
||||
rtp_packets = []
|
||||
pump = MediaPacketPump(generate_packets(3))
|
||||
pump = avdtp.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_STREAMING_STATE
|
||||
assert stream.state == avdtp.State.STREAMING
|
||||
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_STREAMING_STATE
|
||||
assert sink.stream.state == avdtp.State.STREAMING
|
||||
await rtp_packets_fully_received
|
||||
|
||||
await stream.close()
|
||||
assert stream.rtp_channel is None
|
||||
assert source.in_use == 0
|
||||
assert source.stream.state == AVDTP_IDLE_STATE
|
||||
assert source.stream.state == avdtp.State.IDLE
|
||||
assert sink.in_use == 0
|
||||
assert sink.stream.state == AVDTP_IDLE_STATE
|
||||
assert sink.stream.state == avdtp.State.IDLE
|
||||
|
||||
# Send packets manually
|
||||
rtp_packets_fully_received = asyncio.get_running_loop().create_future()
|
||||
@@ -268,12 +257,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_STREAMING_STATE
|
||||
assert stream.state == avdtp.State.STREAMING
|
||||
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_STREAMING_STATE
|
||||
assert sink.stream.state == avdtp.State.STREAMING
|
||||
|
||||
stream.send_media_packet(source_packets[0])
|
||||
stream.send_media_packet(source_packets[1])
|
||||
@@ -283,9 +272,9 @@ 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_IDLE_STATE
|
||||
assert source.stream.state == avdtp.State.IDLE
|
||||
assert sink.in_use == 0
|
||||
assert sink.stream.state == AVDTP_IDLE_STATE
|
||||
assert sink.stream.state == avdtp.State.IDLE
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -15,43 +15,108 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# 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
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
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'),
|
||||
@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,
|
||||
)
|
||||
]
|
||||
),
|
||||
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
|
||||
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,
|
||||
)
|
||||
assert message == parsed
|
||||
assert message.payload == parsed.payload
|
||||
|
||||
|
||||
@@ -62,9 +127,3 @@ def test_rtp():
|
||||
)
|
||||
media_packet = MediaPacket.from_bytes(packet)
|
||||
print(media_packet)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
test_messages()
|
||||
test_rtp()
|
||||
|
||||
@@ -761,6 +761,34 @@ async def test_inquiry_result_with_rssi():
|
||||
m.assert_called_with(hci.Address("00:11:22:33:44:55/P"), 3, mock.ANY, 5)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.parametrize(
|
||||
"roles",
|
||||
(
|
||||
(hci.Role.PERIPHERAL, hci.Role.CENTRAL),
|
||||
(hci.Role.CENTRAL, hci.Role.PERIPHERAL),
|
||||
),
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_accept_classic_connection(roles: tuple[hci.Role, hci.Role]):
|
||||
devices = TwoDevices()
|
||||
devices[0].classic_enabled = True
|
||||
devices[1].classic_enabled = True
|
||||
await devices[0].power_on()
|
||||
await devices[1].power_on()
|
||||
|
||||
accept_task = asyncio.create_task(devices[1].accept(role=roles[1]))
|
||||
await devices[0].connect(
|
||||
devices[1].public_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
await accept_task
|
||||
|
||||
assert devices.connections[0]
|
||||
assert devices.connections[0].role == roles[0]
|
||||
assert devices.connections[1]
|
||||
assert devices.connections[1].role == roles[1]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def run_test_device():
|
||||
await test_device_connect_parallel()
|
||||
|
||||
@@ -82,7 +82,6 @@ async def hap_client():
|
||||
)
|
||||
|
||||
await devices.setup_connection()
|
||||
# TODO negotiate MTU > 49 to not truncate preset names
|
||||
|
||||
# Mock encryption.
|
||||
devices.connections[0].encryption = 1 # type: ignore
|
||||
@@ -93,6 +92,9 @@ async def hap_client():
|
||||
)
|
||||
|
||||
peer = device.Peer(devices.connections[1]) # type: ignore
|
||||
await peer.request_mtu(49)
|
||||
peer2 = device.Peer(devices.connections[0]) # type: ignore
|
||||
await peer2.request_mtu(49)
|
||||
hap_client = await peer.discover_service_and_create_proxy(
|
||||
hap.HearingAccessServiceProxy
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user