forked from auracaster/bumble_mirror
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42711d3d31 | ||
|
|
67a61ae34d | ||
|
|
a62f981556 | ||
|
|
6b56b10b6e | ||
|
|
e0dee2135f | ||
|
|
bb9aa12a74 | ||
|
|
da64f66bce | ||
|
|
f000a3f30a | ||
|
|
8ad48f92b3 | ||
|
|
a827669f62 | ||
|
|
4bee8d5287 | ||
|
|
5431941fe7 | ||
|
|
d112901a17 | ||
|
|
2d74aef0e9 | ||
|
|
f06e19e1ca | ||
|
|
36aefb280d | ||
|
|
227f5cf62e | ||
|
|
1336cfa42c | ||
|
|
0ca7b8b322 | ||
|
|
eef5304a36 | ||
|
|
1a2141126c | ||
|
|
6ed9a98490 | ||
|
|
19b7660f88 | ||
|
|
1932f14fb6 | ||
|
|
b70b92097f | ||
|
|
b6a800c692 | ||
|
|
d43f5573a6 | ||
|
|
1982168a9f | ||
|
|
5e1794a15b | ||
|
|
578f7f054d | ||
|
|
4b25b3581d | ||
|
|
9601c7f287 | ||
|
|
dae3ec5cba | ||
|
|
95225a1774 | ||
|
|
e54a26393e | ||
|
|
5dc76cf7b4 | ||
|
|
6c68115660 | ||
|
|
88ef65a4e2 | ||
|
|
324b26d8f2 | ||
|
|
a43b403511 | ||
|
|
c657494362 | ||
|
|
11505f08b7 | ||
|
|
9bf9ed5f59 | ||
|
|
0fa517a4f6 | ||
|
|
a11962a487 | ||
|
|
374a1c623f | ||
|
|
82ffc6b23b | ||
|
|
589bbfcf19 |
2
.github/workflows/code-check.yml
vendored
2
.github/workflows/code-check.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13.0"]
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13.0", "3.14"]
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
|
||||
5
.github/workflows/python-build-test.yml
vendored
5
.github/workflows/python-build-test.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: ['ubuntu-latest', 'macos-latest', 'windows-latest']
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
@@ -48,7 +48,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
# Rust runtime doesn't support 3.14 yet.
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
rust-version: [ "1.80.0", "stable" ]
|
||||
fail-fast: false
|
||||
steps:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -742,10 +742,9 @@ async def run_receive(
|
||||
]
|
||||
packet_stats = [0, 0]
|
||||
|
||||
audio_output = await audio_io.create_audio_output(output)
|
||||
# This try should be replaced with contextlib.aclosing() when python 3.9 is no
|
||||
# longer needed.
|
||||
try:
|
||||
async with contextlib.AsyncExitStack() as stack:
|
||||
audio_output = await audio_io.create_audio_output(output)
|
||||
stack.push_async_callback(audio_output.aclose)
|
||||
await audio_output.open(
|
||||
audio_io.PcmFormat(
|
||||
audio_io.PcmFormat.Endianness.LITTLE,
|
||||
@@ -793,8 +792,6 @@ async def run_receive(
|
||||
terminated = asyncio.Event()
|
||||
big_sync.on(big_sync.Event.TERMINATION, lambda _: terminated.set())
|
||||
await terminated.wait()
|
||||
finally:
|
||||
await audio_output.aclose()
|
||||
|
||||
|
||||
async def run_transmit(
|
||||
@@ -891,11 +888,10 @@ async def run_transmit(
|
||||
print('Start Periodic Advertising')
|
||||
await advertising_set.start_periodic()
|
||||
|
||||
audio_input = await audio_io.create_audio_input(input, input_format)
|
||||
pcm_format = await audio_input.open()
|
||||
# This try should be replaced with contextlib.aclosing() when python 3.9 is no
|
||||
# longer needed.
|
||||
try:
|
||||
async with contextlib.AsyncExitStack() as stack:
|
||||
audio_input = await audio_io.create_audio_input(input, input_format)
|
||||
pcm_format = await audio_input.open()
|
||||
stack.push_async_callback(audio_input.aclose)
|
||||
if pcm_format.channels != 2:
|
||||
print("Only 2 channels PCM configurations are supported")
|
||||
return
|
||||
@@ -967,8 +963,6 @@ async def run_transmit(
|
||||
await iso_queues[1].write(lc3_frame[mid:])
|
||||
|
||||
frame_count += 1
|
||||
finally:
|
||||
await audio_input.aclose()
|
||||
|
||||
|
||||
def run_async(async_command: Coroutine) -> None:
|
||||
|
||||
@@ -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,13 +19,13 @@ from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import dataclasses
|
||||
import enum
|
||||
import logging
|
||||
import pathlib
|
||||
import sys
|
||||
import wave
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import TYPE_CHECKING, AsyncGenerator, BinaryIO
|
||||
|
||||
from bumble.colors import color
|
||||
@@ -176,7 +176,7 @@ class ThreadedAudioOutput(AudioOutput):
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._thread_pool = ThreadPoolExecutor(1)
|
||||
self._thread_pool = concurrent.futures.ThreadPoolExecutor(1)
|
||||
self._pcm_samples: asyncio.Queue[bytes] = asyncio.Queue()
|
||||
self._write_task = asyncio.create_task(self._write_loop())
|
||||
|
||||
@@ -405,7 +405,7 @@ class ThreadedAudioInput(AudioInput):
|
||||
"""Base class for AudioInput implementation where reading samples may block."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._thread_pool = ThreadPoolExecutor(1)
|
||||
self._thread_pool = concurrent.futures.ThreadPoolExecutor(1)
|
||||
self._pcm_samples: asyncio.Queue[bytes] = asyncio.Queue()
|
||||
|
||||
@abc.abstractmethod
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
1502
bumble/controller.py
1502
bumble/controller.py
File diff suppressed because it is too large
Load Diff
296
bumble/device.py
296
bumble/device.py
@@ -907,7 +907,7 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
|
||||
hci.HCI_LE_Periodic_Advertising_Create_Sync_Command.Options.DUPLICATE_FILTERING_INITIALLY_ENABLED
|
||||
)
|
||||
|
||||
response = await self.device.send_command(
|
||||
await self.device.send_command(
|
||||
hci.HCI_LE_Periodic_Advertising_Create_Sync_Command(
|
||||
options=options,
|
||||
advertising_sid=self.sid,
|
||||
@@ -916,10 +916,9 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
|
||||
skip=self.skip,
|
||||
sync_timeout=int(self.sync_timeout * 100),
|
||||
sync_cte_type=0,
|
||||
)
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
if response.status != hci.HCI_Command_Status_Event.PENDING:
|
||||
raise hci.HCI_StatusError(response)
|
||||
|
||||
self.state = self.State.PENDING
|
||||
|
||||
@@ -1915,16 +1914,13 @@ class Connection(utils.CompositeEventEmitter):
|
||||
"""Idles the current task waiting for a disconnect or timeout"""
|
||||
|
||||
abort = asyncio.get_running_loop().create_future()
|
||||
self.on(self.EVENT_DISCONNECTION, abort.set_result)
|
||||
self.on(self.EVENT_DISCONNECTION_FAILURE, abort.set_exception)
|
||||
with closing(utils.EventWatcher()) as watcher:
|
||||
watcher.on(self, self.EVENT_DISCONNECTION, abort.set_result)
|
||||
watcher.on(self, self.EVENT_DISCONNECTION_FAILURE, abort.set_exception)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
utils.cancel_on_event(self.device, Device.EVENT_FLUSH, abort), timeout
|
||||
)
|
||||
finally:
|
||||
self.remove_listener(self.EVENT_DISCONNECTION, abort.set_result)
|
||||
self.remove_listener(self.EVENT_DISCONNECTION_FAILURE, abort.set_exception)
|
||||
|
||||
async def set_data_length(self, tx_octets: int, tx_time: int) -> None:
|
||||
return await self.device.set_data_length(self, tx_octets, tx_time)
|
||||
@@ -2263,8 +2259,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"
|
||||
@@ -2376,11 +2370,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
hci.Address.ANY: []
|
||||
} # Futures, by BD address OR [Futures] for hci.Address.ANY
|
||||
|
||||
# In Python <= 3.9 + Rust Runtime, asyncio.Lock cannot be properly initiated.
|
||||
if sys.version_info >= (3, 10):
|
||||
self._cis_lock = asyncio.Lock()
|
||||
else:
|
||||
self._cis_lock = AsyncExitStack()
|
||||
self._cis_lock = asyncio.Lock()
|
||||
|
||||
# Own address type cache
|
||||
self.connect_own_address_type = None
|
||||
@@ -2889,7 +2879,9 @@ class Device(utils.CompositeEventEmitter):
|
||||
self.address_resolver = smp.AddressResolver(resolving_keys)
|
||||
|
||||
if self.address_resolution_offload or self.address_generation_offload:
|
||||
await self.send_command(hci.HCI_LE_Clear_Resolving_List_Command())
|
||||
await self.send_command(
|
||||
hci.HCI_LE_Clear_Resolving_List_Command(), check_result=True
|
||||
)
|
||||
|
||||
# Add an empty entry for non-directed address generation.
|
||||
await self.send_command(
|
||||
@@ -2898,7 +2890,8 @@ class Device(utils.CompositeEventEmitter):
|
||||
peer_identity_address=hci.Address.ANY,
|
||||
peer_irk=bytes(16),
|
||||
local_irk=self.irk,
|
||||
)
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
for irk, address in resolving_keys:
|
||||
@@ -2908,7 +2901,8 @@ class Device(utils.CompositeEventEmitter):
|
||||
peer_identity_address=address,
|
||||
peer_irk=irk,
|
||||
local_irk=self.irk,
|
||||
)
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
def supports_le_features(self, feature: hci.LeFeatureMask) -> bool:
|
||||
@@ -3503,16 +3497,15 @@ class Device(utils.CompositeEventEmitter):
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
response = await self.send_command(
|
||||
self.discovering = False
|
||||
await self.send_command(
|
||||
hci.HCI_Inquiry_Command(
|
||||
lap=hci.HCI_GENERAL_INQUIRY_LAP,
|
||||
inquiry_length=DEVICE_DEFAULT_INQUIRY_LENGTH,
|
||||
num_responses=0, # Unlimited number of responses.
|
||||
)
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
if response.status != hci.HCI_Command_Status_Event.PENDING:
|
||||
self.discovering = False
|
||||
raise hci.HCI_StatusError(response)
|
||||
|
||||
self.auto_restart_inquiry = auto_restart
|
||||
self.discovering = True
|
||||
@@ -3548,7 +3541,8 @@ class Device(utils.CompositeEventEmitter):
|
||||
scan_enable = 0x00
|
||||
|
||||
return await self.send_command(
|
||||
hci.HCI_Write_Scan_Enable_Command(scan_enable=scan_enable)
|
||||
hci.HCI_Write_Scan_Enable_Command(scan_enable=scan_enable),
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
async def set_discoverable(self, discoverable: bool = True) -> None:
|
||||
@@ -3777,7 +3771,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
for phy in phys
|
||||
]
|
||||
|
||||
result = await self.send_command(
|
||||
await self.send_command(
|
||||
hci.HCI_LE_Extended_Create_Connection_Command(
|
||||
initiator_filter_policy=0,
|
||||
own_address_type=own_address_type,
|
||||
@@ -3798,14 +3792,15 @@ class Device(utils.CompositeEventEmitter):
|
||||
supervision_timeouts=supervision_timeouts,
|
||||
min_ce_lengths=min_ce_lengths,
|
||||
max_ce_lengths=max_ce_lengths,
|
||||
)
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
else:
|
||||
if hci.HCI_LE_1M_PHY not in connection_parameters_preferences:
|
||||
raise InvalidArgumentError('1M PHY preferences required')
|
||||
|
||||
prefs = connection_parameters_preferences[hci.HCI_LE_1M_PHY]
|
||||
result = await self.send_command(
|
||||
await self.send_command(
|
||||
hci.HCI_LE_Create_Connection_Command(
|
||||
le_scan_interval=int(
|
||||
DEVICE_DEFAULT_CONNECT_SCAN_INTERVAL / 0.625
|
||||
@@ -3827,7 +3822,8 @@ class Device(utils.CompositeEventEmitter):
|
||||
supervision_timeout=int(prefs.supervision_timeout / 10),
|
||||
min_ce_length=int(prefs.min_ce_length / 0.625),
|
||||
max_ce_length=int(prefs.max_ce_length / 0.625),
|
||||
)
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
else:
|
||||
# Save pending connection
|
||||
@@ -3844,7 +3840,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
)
|
||||
|
||||
# TODO: allow passing other settings
|
||||
result = await self.send_command(
|
||||
await self.send_command(
|
||||
hci.HCI_Create_Connection_Command(
|
||||
bd_addr=peer_address,
|
||||
packet_type=0xCC18, # FIXME: change
|
||||
@@ -3852,12 +3848,10 @@ class Device(utils.CompositeEventEmitter):
|
||||
clock_offset=0x0000,
|
||||
allow_role_switch=0x01,
|
||||
reserved=0,
|
||||
)
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
if result.status != hci.HCI_Command_Status_Event.PENDING:
|
||||
raise hci.HCI_StatusError(result)
|
||||
|
||||
# Wait for the connection process to complete
|
||||
if transport == PhysicalTransport.LE:
|
||||
self.le_connecting = True
|
||||
@@ -4009,7 +4003,8 @@ class Device(utils.CompositeEventEmitter):
|
||||
await self.send_command(
|
||||
hci.HCI_Accept_Connection_Request_Command(
|
||||
bd_addr=peer_address, role=role
|
||||
)
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
# Wait for connection complete
|
||||
@@ -4079,19 +4074,17 @@ class Device(utils.CompositeEventEmitter):
|
||||
connection.EVENT_DISCONNECTION_FAILURE, pending_disconnection.set_exception
|
||||
)
|
||||
|
||||
# Request a disconnection
|
||||
result = await self.send_command(
|
||||
hci.HCI_Disconnect_Command(
|
||||
connection_handle=connection.handle, reason=reason
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
if result.status != hci.HCI_Command_Status_Event.PENDING:
|
||||
raise hci.HCI_StatusError(result)
|
||||
|
||||
# Wait for the disconnection process to complete
|
||||
self.disconnecting = True
|
||||
|
||||
# Request a disconnection
|
||||
await self.send_command(
|
||||
hci.HCI_Disconnect_Command(
|
||||
connection_handle=connection.handle, reason=reason
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
return await utils.cancel_on_event(
|
||||
self, Device.EVENT_FLUSH, pending_disconnection
|
||||
)
|
||||
@@ -4177,7 +4170,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
return
|
||||
|
||||
result = await self.send_command(
|
||||
await self.send_command(
|
||||
hci.HCI_LE_Connection_Update_Command(
|
||||
connection_handle=connection.handle,
|
||||
connection_interval_min=connection_interval_min,
|
||||
@@ -4186,10 +4179,9 @@ class Device(utils.CompositeEventEmitter):
|
||||
supervision_timeout=supervision_timeout,
|
||||
min_ce_length=min_ce_length,
|
||||
max_ce_length=max_ce_length,
|
||||
)
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
if result.status != hci.HCI_Command_Status_Event.PENDING:
|
||||
raise hci.HCI_StatusError(result)
|
||||
|
||||
async def get_connection_rssi(self, connection):
|
||||
result = await self.send_command(
|
||||
@@ -4224,23 +4216,17 @@ class Device(utils.CompositeEventEmitter):
|
||||
(1 if rx_phys is None else 0) << 1
|
||||
)
|
||||
|
||||
result = await self.send_command(
|
||||
await self.send_command(
|
||||
hci.HCI_LE_Set_PHY_Command(
|
||||
connection_handle=connection.handle,
|
||||
all_phys=all_phys_bits,
|
||||
tx_phys=hci.phy_list_to_bits(tx_phys),
|
||||
rx_phys=hci.phy_list_to_bits(rx_phys),
|
||||
phy_options=phy_options,
|
||||
)
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
if result.status != hci.HCI_COMMAND_STATUS_PENDING:
|
||||
logger.warning(
|
||||
'HCI_LE_Set_PHY_Command failed: '
|
||||
f'{hci.HCI_Constant.error_name(result.status)}'
|
||||
)
|
||||
raise hci.HCI_StatusError(result)
|
||||
|
||||
async def set_default_phy(
|
||||
self,
|
||||
tx_phys: Optional[Iterable[hci.Phy]] = None,
|
||||
@@ -4457,43 +4443,26 @@ class Device(utils.CompositeEventEmitter):
|
||||
async def authenticate(self, connection: Connection) -> None:
|
||||
# Set up event handlers
|
||||
pending_authentication = asyncio.get_running_loop().create_future()
|
||||
with closing(utils.EventWatcher()) as watcher:
|
||||
|
||||
def on_authentication():
|
||||
pending_authentication.set_result(None)
|
||||
@watcher.on(connection, connection.EVENT_CONNECTION_AUTHENTICATION)
|
||||
def on_authentication() -> None:
|
||||
pending_authentication.set_result(None)
|
||||
|
||||
def on_authentication_failure(error_code):
|
||||
pending_authentication.set_exception(hci.HCI_Error(error_code))
|
||||
@watcher.on(connection, connection.EVENT_CONNECTION_AUTHENTICATION_FAILURE)
|
||||
def on_authentication_failure(error_code: int) -> None:
|
||||
pending_authentication.set_exception(hci.HCI_Error(error_code))
|
||||
|
||||
connection.on(connection.EVENT_CONNECTION_AUTHENTICATION, on_authentication)
|
||||
connection.on(
|
||||
connection.EVENT_CONNECTION_AUTHENTICATION_FAILURE,
|
||||
on_authentication_failure,
|
||||
)
|
||||
|
||||
# Request the authentication
|
||||
try:
|
||||
result = await self.send_command(
|
||||
# Request the authentication
|
||||
await self.send_command(
|
||||
hci.HCI_Authentication_Requested_Command(
|
||||
connection_handle=connection.handle
|
||||
)
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
if result.status != hci.HCI_COMMAND_STATUS_PENDING:
|
||||
logger.warning(
|
||||
'HCI_Authentication_Requested_Command failed: '
|
||||
f'{hci.HCI_Constant.error_name(result.status)}'
|
||||
)
|
||||
raise hci.HCI_StatusError(result)
|
||||
|
||||
# Wait for the authentication to complete
|
||||
await connection.cancel_on_disconnection(pending_authentication)
|
||||
finally:
|
||||
connection.remove_listener(
|
||||
connection.EVENT_CONNECTION_AUTHENTICATION, on_authentication
|
||||
)
|
||||
connection.remove_listener(
|
||||
connection.EVENT_CONNECTION_AUTHENTICATION_FAILURE,
|
||||
on_authentication_failure,
|
||||
)
|
||||
|
||||
async def encrypt(self, connection: Connection, enable: bool = True):
|
||||
if not enable and connection.transport == PhysicalTransport.LE:
|
||||
@@ -4502,21 +4471,17 @@ class Device(utils.CompositeEventEmitter):
|
||||
# Set up event handlers
|
||||
pending_encryption = asyncio.get_running_loop().create_future()
|
||||
|
||||
def on_encryption_change():
|
||||
pending_encryption.set_result(None)
|
||||
|
||||
def on_encryption_failure(error_code: int):
|
||||
pending_encryption.set_exception(hci.HCI_Error(error_code))
|
||||
|
||||
connection.on(
|
||||
connection.EVENT_CONNECTION_ENCRYPTION_CHANGE, on_encryption_change
|
||||
)
|
||||
connection.on(
|
||||
connection.EVENT_CONNECTION_ENCRYPTION_FAILURE, on_encryption_failure
|
||||
)
|
||||
|
||||
# Request the encryption
|
||||
try:
|
||||
with closing(utils.EventWatcher()) as watcher:
|
||||
|
||||
@watcher.on(connection, connection.EVENT_CONNECTION_ENCRYPTION_CHANGE)
|
||||
def _() -> None:
|
||||
pending_encryption.set_result(None)
|
||||
|
||||
@watcher.on(connection, connection.EVENT_CONNECTION_ENCRYPTION_FAILURE)
|
||||
def _(error_code: int):
|
||||
pending_encryption.set_exception(hci.HCI_Error(error_code))
|
||||
|
||||
if connection.transport == PhysicalTransport.LE:
|
||||
# Look for a key in the key store
|
||||
if self.keystore is None:
|
||||
@@ -4541,45 +4506,26 @@ class Device(utils.CompositeEventEmitter):
|
||||
if connection.role != hci.Role.CENTRAL:
|
||||
raise InvalidStateError('only centrals can start encryption')
|
||||
|
||||
result = await self.send_command(
|
||||
await self.send_command(
|
||||
hci.HCI_LE_Enable_Encryption_Command(
|
||||
connection_handle=connection.handle,
|
||||
random_number=rand,
|
||||
encrypted_diversifier=ediv,
|
||||
long_term_key=ltk,
|
||||
)
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
if result.status != hci.HCI_COMMAND_STATUS_PENDING:
|
||||
logger.warning(
|
||||
'HCI_LE_Enable_Encryption_Command failed: '
|
||||
f'{hci.HCI_Constant.error_name(result.status)}'
|
||||
)
|
||||
raise hci.HCI_StatusError(result)
|
||||
else:
|
||||
result = await self.send_command(
|
||||
await self.send_command(
|
||||
hci.HCI_Set_Connection_Encryption_Command(
|
||||
connection_handle=connection.handle,
|
||||
encryption_enable=0x01 if enable else 0x00,
|
||||
)
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
if result.status != hci.HCI_COMMAND_STATUS_PENDING:
|
||||
logger.warning(
|
||||
'HCI_Set_Connection_Encryption_Command failed: '
|
||||
f'{hci.HCI_Constant.error_name(result.status)}'
|
||||
)
|
||||
raise hci.HCI_StatusError(result)
|
||||
|
||||
# Wait for the result
|
||||
await connection.cancel_on_disconnection(pending_encryption)
|
||||
finally:
|
||||
connection.remove_listener(
|
||||
connection.EVENT_CONNECTION_ENCRYPTION_CHANGE, on_encryption_change
|
||||
)
|
||||
connection.remove_listener(
|
||||
connection.EVENT_CONNECTION_ENCRYPTION_FAILURE, on_encryption_failure
|
||||
)
|
||||
|
||||
async def update_keys(self, address: str, keys: PairingKeys) -> None:
|
||||
if self.keystore is None:
|
||||
@@ -4597,80 +4543,55 @@ class Device(utils.CompositeEventEmitter):
|
||||
async def switch_role(self, connection: Connection, role: hci.Role):
|
||||
pending_role_change = asyncio.get_running_loop().create_future()
|
||||
|
||||
def on_role_change(new_role: hci.Role):
|
||||
pending_role_change.set_result(new_role)
|
||||
with closing(utils.EventWatcher()) as watcher:
|
||||
|
||||
def on_role_change_failure(error_code: int):
|
||||
pending_role_change.set_exception(hci.HCI_Error(error_code))
|
||||
@watcher.on(connection, connection.EVENT_ROLE_CHANGE)
|
||||
def _(new_role: hci.Role):
|
||||
pending_role_change.set_result(new_role)
|
||||
|
||||
connection.on(connection.EVENT_ROLE_CHANGE, on_role_change)
|
||||
connection.on(connection.EVENT_ROLE_CHANGE_FAILURE, on_role_change_failure)
|
||||
@watcher.on(connection, connection.EVENT_ROLE_CHANGE_FAILURE)
|
||||
def _(error_code: int):
|
||||
pending_role_change.set_exception(hci.HCI_Error(error_code))
|
||||
|
||||
try:
|
||||
result = await self.send_command(
|
||||
hci.HCI_Switch_Role_Command(bd_addr=connection.peer_address, role=role)
|
||||
await self.send_command(
|
||||
hci.HCI_Switch_Role_Command(bd_addr=connection.peer_address, role=role),
|
||||
check_result=True,
|
||||
)
|
||||
if result.status != hci.HCI_COMMAND_STATUS_PENDING:
|
||||
logger.warning(
|
||||
'HCI_Switch_Role_Command failed: '
|
||||
f'{hci.HCI_Constant.error_name(result.status)}'
|
||||
)
|
||||
raise hci.HCI_StatusError(result)
|
||||
await connection.cancel_on_disconnection(pending_role_change)
|
||||
finally:
|
||||
connection.remove_listener(connection.EVENT_ROLE_CHANGE, on_role_change)
|
||||
connection.remove_listener(
|
||||
connection.EVENT_ROLE_CHANGE_FAILURE, on_role_change_failure
|
||||
)
|
||||
|
||||
# [Classic only]
|
||||
async def request_remote_name(self, remote: Union[hci.Address, Connection]) -> str:
|
||||
# Set up event handlers
|
||||
pending_name = asyncio.get_running_loop().create_future()
|
||||
pending_name: asyncio.Future[str] = asyncio.get_running_loop().create_future()
|
||||
|
||||
peer_address = (
|
||||
remote if isinstance(remote, hci.Address) else remote.peer_address
|
||||
)
|
||||
|
||||
handler = self.on(
|
||||
self.EVENT_REMOTE_NAME,
|
||||
lambda address, remote_name: (
|
||||
pending_name.set_result(remote_name)
|
||||
if address == peer_address
|
||||
else None
|
||||
),
|
||||
)
|
||||
failure_handler = self.on(
|
||||
self.EVENT_REMOTE_NAME_FAILURE,
|
||||
lambda address, error_code: (
|
||||
pending_name.set_exception(hci.HCI_Error(error_code))
|
||||
if address == peer_address
|
||||
else None
|
||||
),
|
||||
)
|
||||
with closing(utils.EventWatcher()) as watcher:
|
||||
|
||||
try:
|
||||
result = await self.send_command(
|
||||
@watcher.on(self, self.EVENT_REMOTE_NAME)
|
||||
def _(address: hci.Address, remote_name: str) -> None:
|
||||
if address == peer_address:
|
||||
pending_name.set_result(remote_name)
|
||||
|
||||
@watcher.on(self, self.EVENT_REMOTE_NAME_FAILURE)
|
||||
def _(address: hci.Address, error_code: int) -> None:
|
||||
if address == peer_address:
|
||||
pending_name.set_exception(hci.HCI_Error(error_code))
|
||||
|
||||
await self.send_command(
|
||||
hci.HCI_Remote_Name_Request_Command(
|
||||
bd_addr=peer_address,
|
||||
page_scan_repetition_mode=hci.HCI_Remote_Name_Request_Command.R2,
|
||||
reserved=0,
|
||||
clock_offset=0, # TODO investigate non-0 values
|
||||
)
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
if result.status != hci.HCI_COMMAND_STATUS_PENDING:
|
||||
logger.warning(
|
||||
'HCI_Remote_Name_Request_Command failed: '
|
||||
f'{hci.HCI_Constant.error_name(result.status)}'
|
||||
)
|
||||
raise hci.HCI_StatusError(result)
|
||||
|
||||
# Wait for the result
|
||||
return await utils.cancel_on_event(self, Device.EVENT_FLUSH, pending_name)
|
||||
finally:
|
||||
self.remove_listener(self.EVENT_REMOTE_NAME, handler)
|
||||
self.remove_listener(self.EVENT_REMOTE_NAME_FAILURE, failure_handler)
|
||||
|
||||
# [LE only]
|
||||
@utils.experimental('Only for testing.')
|
||||
@@ -4686,8 +4607,6 @@ class Device(utils.CompositeEventEmitter):
|
||||
Returns:
|
||||
List of created CIS handles corresponding to the same order of [cid_id].
|
||||
"""
|
||||
num_cis = len(parameters.cis_parameters)
|
||||
|
||||
response = await self.send_command(
|
||||
hci.HCI_LE_Set_CIG_Parameters_Command(
|
||||
cig_id=parameters.cig_id,
|
||||
@@ -5755,9 +5674,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_authentication_failure(
|
||||
self, connection: Connection, error: core.ConnectionError
|
||||
):
|
||||
def on_connection_authentication_failure(self, connection: Connection, error: int):
|
||||
logger.debug(
|
||||
f'*** Connection Authentication Failure: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}, error={error}'
|
||||
@@ -5812,11 +5729,15 @@ class Device(utils.CompositeEventEmitter):
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_authentication_user_confirmation_request(self, connection, code) -> None:
|
||||
def on_authentication_user_confirmation_request(
|
||||
self, connection: Connection, code: int
|
||||
) -> None:
|
||||
# Ask what the pairing config should be for this connection
|
||||
pairing_config = self.pairing_config_factory(connection)
|
||||
io_capability = pairing_config.delegate.classic_io_capability
|
||||
peer_io_capability = connection.pairing_peer_io_capability
|
||||
if peer_io_capability is None:
|
||||
raise core.InvalidStateError("Unknown pairing_peer_io_capability")
|
||||
|
||||
async def confirm() -> bool:
|
||||
# Ask the user to confirm the pairing, without display
|
||||
@@ -5943,15 +5864,16 @@ class Device(utils.CompositeEventEmitter):
|
||||
# Respond
|
||||
if io_capability == hci.IoCapability.KEYBOARD_ONLY:
|
||||
# Ask the user to enter a string
|
||||
async def get_pin_code():
|
||||
pin_code = await connection.cancel_on_disconnection(
|
||||
async def get_pin_code() -> None:
|
||||
pin_code_str = await connection.cancel_on_disconnection(
|
||||
pairing_config.delegate.get_string(16)
|
||||
)
|
||||
|
||||
if pin_code is not None:
|
||||
pin_code = bytes(pin_code, encoding='utf-8')
|
||||
if pin_code_str is not None:
|
||||
pin_code = bytes(pin_code_str, encoding='utf-8')
|
||||
pin_code_len = len(pin_code)
|
||||
assert 0 < pin_code_len <= 16, "pin_code should be 1-16 bytes"
|
||||
if not 1 <= pin_code_len <= 16:
|
||||
raise core.InvalidArgumentError("pin_code should be 1-16 bytes")
|
||||
await self.host.send_command(
|
||||
hci.HCI_PIN_Code_Request_Reply_Command(
|
||||
bd_addr=connection.peer_address,
|
||||
|
||||
@@ -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
|
||||
|
||||
233
bumble/link.py
233
bumble/link.py
@@ -11,6 +11,7 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
@@ -20,16 +21,7 @@ import asyncio
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from bumble import controller, core
|
||||
from bumble.hci import (
|
||||
HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR,
|
||||
HCI_PAGE_TIMEOUT_ERROR,
|
||||
HCI_SUCCESS,
|
||||
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
||||
Address,
|
||||
HCI_Connection_Complete_Event,
|
||||
Role,
|
||||
)
|
||||
from bumble import controller, core, hci, lmp
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -40,13 +32,6 @@ logger = logging.getLogger(__name__)
|
||||
# -----------------------------------------------------------------------------
|
||||
# Utils
|
||||
# -----------------------------------------------------------------------------
|
||||
def parse_parameters(params_str):
|
||||
result = {}
|
||||
for param_str in params_str.split(','):
|
||||
if '=' in param_str:
|
||||
key, value = param_str.split('=')
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -69,21 +54,21 @@ class LocalLink:
|
||||
# Common utils
|
||||
############################################################
|
||||
|
||||
def add_controller(self, controller):
|
||||
def add_controller(self, controller: controller.Controller):
|
||||
logger.debug(f'new controller: {controller}')
|
||||
self.controllers.add(controller)
|
||||
|
||||
def remove_controller(self, controller):
|
||||
def remove_controller(self, controller: controller.Controller):
|
||||
self.controllers.remove(controller)
|
||||
|
||||
def find_controller(self, address):
|
||||
def find_controller(self, address: hci.Address) -> controller.Controller | None:
|
||||
for controller in self.controllers:
|
||||
if controller.random_address == address:
|
||||
return controller
|
||||
return None
|
||||
|
||||
def find_classic_controller(
|
||||
self, address: Address
|
||||
self, address: hci.Address
|
||||
) -> Optional[controller.Controller]:
|
||||
for controller in self.controllers:
|
||||
if controller.public_address == address:
|
||||
@@ -100,13 +85,19 @@ class LocalLink:
|
||||
def on_address_changed(self, controller):
|
||||
pass
|
||||
|
||||
def send_advertising_data(self, sender_address, data):
|
||||
def send_advertising_data(self, sender_address: hci.Address, data: bytes):
|
||||
# Send the advertising data to all controllers, except the sender
|
||||
for controller in self.controllers:
|
||||
if controller.random_address != sender_address:
|
||||
controller.on_link_advertising_data(sender_address, data)
|
||||
|
||||
def send_acl_data(self, sender_controller, destination_address, transport, data):
|
||||
def send_acl_data(
|
||||
self,
|
||||
sender_controller: controller.Controller,
|
||||
destination_address: hci.Address,
|
||||
transport: core.PhysicalTransport,
|
||||
data: bytes,
|
||||
):
|
||||
# Send the data to the first controller with a matching address
|
||||
if transport == core.PhysicalTransport.LE:
|
||||
destination_controller = self.find_controller(destination_address)
|
||||
@@ -118,9 +109,13 @@ class LocalLink:
|
||||
raise ValueError("unsupported transport type")
|
||||
|
||||
if destination_controller is not None:
|
||||
destination_controller.on_link_acl_data(source_address, transport, data)
|
||||
asyncio.get_running_loop().call_soon(
|
||||
lambda: destination_controller.on_link_acl_data(
|
||||
source_address, transport, data
|
||||
)
|
||||
)
|
||||
|
||||
def on_connection_complete(self):
|
||||
def on_connection_complete(self) -> None:
|
||||
# Check that we expect this call
|
||||
if not self.pending_connection:
|
||||
logger.warning('on_connection_complete with no pending connection')
|
||||
@@ -139,17 +134,21 @@ class LocalLink:
|
||||
le_create_connection_command.peer_address
|
||||
):
|
||||
central_controller.on_link_peripheral_connection_complete(
|
||||
le_create_connection_command, HCI_SUCCESS
|
||||
le_create_connection_command, hci.HCI_SUCCESS
|
||||
)
|
||||
peripheral_controller.on_link_central_connected(central_address)
|
||||
return
|
||||
|
||||
# No peripheral found
|
||||
central_controller.on_link_peripheral_connection_complete(
|
||||
le_create_connection_command, HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR
|
||||
le_create_connection_command, hci.HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR
|
||||
)
|
||||
|
||||
def connect(self, central_address, le_create_connection_command):
|
||||
def connect(
|
||||
self,
|
||||
central_address: hci.Address,
|
||||
le_create_connection_command: hci.HCI_LE_Create_Connection_Command,
|
||||
):
|
||||
logger.debug(
|
||||
f'$$$ CONNECTION {central_address} -> '
|
||||
f'{le_create_connection_command.peer_address}'
|
||||
@@ -158,7 +157,10 @@ class LocalLink:
|
||||
asyncio.get_running_loop().call_soon(self.on_connection_complete)
|
||||
|
||||
def on_disconnection_complete(
|
||||
self, initiating_address, target_address, disconnect_command
|
||||
self,
|
||||
initiating_address: hci.Address,
|
||||
target_address: hci.Address,
|
||||
disconnect_command: hci.HCI_Disconnect_Command,
|
||||
):
|
||||
# Find the controller that initiated the disconnection
|
||||
if not (initiating_controller := self.find_controller(initiating_address)):
|
||||
@@ -172,20 +174,32 @@ class LocalLink:
|
||||
)
|
||||
|
||||
initiating_controller.on_link_disconnection_complete(
|
||||
disconnect_command, HCI_SUCCESS
|
||||
disconnect_command, hci.HCI_SUCCESS
|
||||
)
|
||||
|
||||
def disconnect(self, initiating_address, target_address, disconnect_command):
|
||||
def disconnect(
|
||||
self,
|
||||
initiating_address: hci.Address,
|
||||
target_address: hci.Address,
|
||||
disconnect_command: hci.HCI_Disconnect_Command,
|
||||
):
|
||||
logger.debug(
|
||||
f'$$$ DISCONNECTION {initiating_address} -> '
|
||||
f'{target_address}: reason = {disconnect_command.reason}'
|
||||
)
|
||||
args = [initiating_address, target_address, disconnect_command]
|
||||
asyncio.get_running_loop().call_soon(self.on_disconnection_complete, *args)
|
||||
asyncio.get_running_loop().call_soon(
|
||||
lambda: self.on_disconnection_complete(
|
||||
initiating_address, target_address, disconnect_command
|
||||
)
|
||||
)
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def on_connection_encrypted(
|
||||
self, central_address, peripheral_address, rand, ediv, ltk
|
||||
self,
|
||||
central_address: hci.Address,
|
||||
peripheral_address: hci.Address,
|
||||
rand: bytes,
|
||||
ediv: int,
|
||||
ltk: bytes,
|
||||
):
|
||||
logger.debug(f'*** ENCRYPTION {central_address} -> {peripheral_address}')
|
||||
|
||||
@@ -198,7 +212,7 @@ class LocalLink:
|
||||
def create_cis(
|
||||
self,
|
||||
central_controller: controller.Controller,
|
||||
peripheral_address: Address,
|
||||
peripheral_address: hci.Address,
|
||||
cig_id: int,
|
||||
cis_id: int,
|
||||
) -> None:
|
||||
@@ -216,7 +230,7 @@ class LocalLink:
|
||||
def accept_cis(
|
||||
self,
|
||||
peripheral_controller: controller.Controller,
|
||||
central_address: Address,
|
||||
central_address: hci.Address,
|
||||
cig_id: int,
|
||||
cis_id: int,
|
||||
) -> None:
|
||||
@@ -224,17 +238,16 @@ class LocalLink:
|
||||
f'$$$ CIS Accept {peripheral_controller.random_address} -> {central_address}'
|
||||
)
|
||||
if central_controller := self.find_controller(central_address):
|
||||
asyncio.get_running_loop().call_soon(
|
||||
central_controller.on_link_cis_established, cig_id, cis_id
|
||||
)
|
||||
asyncio.get_running_loop().call_soon(
|
||||
loop = asyncio.get_running_loop()
|
||||
loop.call_soon(central_controller.on_link_cis_established, cig_id, cis_id)
|
||||
loop.call_soon(
|
||||
peripheral_controller.on_link_cis_established, cig_id, cis_id
|
||||
)
|
||||
|
||||
def disconnect_cis(
|
||||
self,
|
||||
initiator_controller: controller.Controller,
|
||||
peer_address: Address,
|
||||
peer_address: hci.Address,
|
||||
cig_id: int,
|
||||
cis_id: int,
|
||||
) -> None:
|
||||
@@ -242,138 +255,28 @@ class LocalLink:
|
||||
f'$$$ CIS Disconnect {initiator_controller.random_address} -> {peer_address}'
|
||||
)
|
||||
if peer_controller := self.find_controller(peer_address):
|
||||
asyncio.get_running_loop().call_soon(
|
||||
loop = asyncio.get_running_loop()
|
||||
loop.call_soon(
|
||||
initiator_controller.on_link_cis_disconnected, cig_id, cis_id
|
||||
)
|
||||
asyncio.get_running_loop().call_soon(
|
||||
peer_controller.on_link_cis_disconnected, cig_id, cis_id
|
||||
)
|
||||
loop.call_soon(peer_controller.on_link_cis_disconnected, cig_id, cis_id)
|
||||
|
||||
############################################################
|
||||
# Classic handlers
|
||||
############################################################
|
||||
|
||||
def classic_connect(self, initiator_controller, responder_address):
|
||||
logger.debug(
|
||||
f'[Classic] {initiator_controller.public_address} connects to {responder_address}'
|
||||
)
|
||||
responder_controller = self.find_classic_controller(responder_address)
|
||||
if responder_controller is None:
|
||||
initiator_controller.on_classic_connection_complete(
|
||||
responder_address, HCI_PAGE_TIMEOUT_ERROR
|
||||
)
|
||||
return
|
||||
self.pending_classic_connection = (initiator_controller, responder_controller)
|
||||
|
||||
responder_controller.on_classic_connection_request(
|
||||
initiator_controller.public_address,
|
||||
HCI_Connection_Complete_Event.LinkType.ACL,
|
||||
)
|
||||
|
||||
def classic_accept_connection(
|
||||
self, responder_controller, initiator_address, responder_role
|
||||
):
|
||||
logger.debug(
|
||||
f'[Classic] {responder_controller.public_address} accepts to connect {initiator_address}'
|
||||
)
|
||||
initiator_controller = self.find_classic_controller(initiator_address)
|
||||
if initiator_controller is None:
|
||||
responder_controller.on_classic_connection_complete(
|
||||
responder_controller.public_address, HCI_PAGE_TIMEOUT_ERROR
|
||||
)
|
||||
return
|
||||
|
||||
async def task():
|
||||
if responder_role != Role.PERIPHERAL:
|
||||
initiator_controller.on_classic_role_change(
|
||||
responder_controller.public_address, int(not (responder_role))
|
||||
)
|
||||
initiator_controller.on_classic_connection_complete(
|
||||
responder_controller.public_address, HCI_SUCCESS
|
||||
)
|
||||
|
||||
asyncio.create_task(task())
|
||||
responder_controller.on_classic_role_change(
|
||||
initiator_controller.public_address, responder_role
|
||||
)
|
||||
responder_controller.on_classic_connection_complete(
|
||||
initiator_controller.public_address, HCI_SUCCESS
|
||||
)
|
||||
self.pending_classic_connection = None
|
||||
|
||||
def classic_disconnect(self, initiator_controller, responder_address, reason):
|
||||
logger.debug(
|
||||
f'[Classic] {initiator_controller.public_address} disconnects {responder_address}'
|
||||
)
|
||||
responder_controller = self.find_classic_controller(responder_address)
|
||||
|
||||
async def task():
|
||||
initiator_controller.on_classic_disconnected(responder_address, reason)
|
||||
|
||||
asyncio.create_task(task())
|
||||
responder_controller.on_classic_disconnected(
|
||||
initiator_controller.public_address, reason
|
||||
)
|
||||
|
||||
def classic_switch_role(
|
||||
self, initiator_controller, responder_address, initiator_new_role
|
||||
):
|
||||
responder_controller = self.find_classic_controller(responder_address)
|
||||
if responder_controller is None:
|
||||
return
|
||||
|
||||
async def task():
|
||||
initiator_controller.on_classic_role_change(
|
||||
responder_address, initiator_new_role
|
||||
)
|
||||
|
||||
asyncio.create_task(task())
|
||||
responder_controller.on_classic_role_change(
|
||||
initiator_controller.public_address, int(not (initiator_new_role))
|
||||
)
|
||||
|
||||
def classic_sco_connect(
|
||||
def send_lmp_packet(
|
||||
self,
|
||||
initiator_controller: controller.Controller,
|
||||
responder_address: Address,
|
||||
link_type: int,
|
||||
sender_controller: controller.Controller,
|
||||
receiver_address: hci.Address,
|
||||
packet: lmp.Packet,
|
||||
):
|
||||
logger.debug(
|
||||
f'[Classic] {initiator_controller.public_address} connects SCO to {responder_address}'
|
||||
)
|
||||
responder_controller = self.find_classic_controller(responder_address)
|
||||
# Initiator controller should handle it.
|
||||
assert responder_controller
|
||||
|
||||
responder_controller.on_classic_connection_request(
|
||||
initiator_controller.public_address,
|
||||
link_type,
|
||||
)
|
||||
|
||||
def classic_accept_sco_connection(
|
||||
self,
|
||||
responder_controller: controller.Controller,
|
||||
initiator_address: Address,
|
||||
link_type: int,
|
||||
):
|
||||
logger.debug(
|
||||
f'[Classic] {responder_controller.public_address} accepts to connect SCO {initiator_address}'
|
||||
)
|
||||
initiator_controller = self.find_classic_controller(initiator_address)
|
||||
if initiator_controller is None:
|
||||
responder_controller.on_classic_sco_connection_complete(
|
||||
responder_controller.public_address,
|
||||
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
||||
link_type,
|
||||
if not (receiver_controller := self.find_classic_controller(receiver_address)):
|
||||
raise core.InvalidArgumentError(
|
||||
f"Unable to find controller for address {receiver_address}"
|
||||
)
|
||||
return
|
||||
|
||||
async def task():
|
||||
initiator_controller.on_classic_sco_connection_complete(
|
||||
responder_controller.public_address, HCI_SUCCESS, link_type
|
||||
asyncio.get_running_loop().call_soon(
|
||||
lambda: receiver_controller.on_lmp_packet(
|
||||
sender_controller.public_address, packet
|
||||
)
|
||||
|
||||
asyncio.create_task(task())
|
||||
responder_controller.on_classic_sco_connection_complete(
|
||||
initiator_controller.public_address, HCI_SUCCESS, link_type
|
||||
)
|
||||
|
||||
324
bumble/lmp.py
Normal file
324
bumble/lmp.py
Normal file
@@ -0,0 +1,324 @@
|
||||
# Copyright 2021-2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TypeVar
|
||||
|
||||
from bumble import hci, utils
|
||||
|
||||
|
||||
class Opcode(utils.OpenIntEnum):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 2, Part C - 5.1 PDU summary.
|
||||
|
||||
Follow the alphabetical order defined there.
|
||||
'''
|
||||
|
||||
# fmt: off
|
||||
LMP_ACCEPTED = 3
|
||||
LMP_ACCEPTED_EXT = 127 << 8 + 1
|
||||
LMP_AU_RAND = 11
|
||||
LMP_AUTO_RATE = 35
|
||||
LMP_CHANNEL_CLASSIFICATION = 127 << 8 + 17
|
||||
LMP_CHANNEL_CLASSIFICATION_REQ = 127 << 8 + 16
|
||||
LMP_CLK_ADJ = 127 << 8 + 5
|
||||
LMP_CLK_ADJ_ACK = 127 << 8 + 6
|
||||
LMP_CLK_ADJ_REQ = 127 << 8 + 7
|
||||
LMP_CLKOFFSET_REQ = 5
|
||||
LMP_CLKOFFSET_RES = 6
|
||||
LMP_COMB_KEY = 9
|
||||
LMP_DECR_POWER_REQ = 32
|
||||
LMP_DETACH = 7
|
||||
LMP_DHKEY_CHECK = 65
|
||||
LMP_ENCAPSULATED_HEADER = 61
|
||||
LMP_ENCAPSULATED_PAYLOAD = 62
|
||||
LMP_ENCRYPTION_KEY_SIZE_MASK_REQ= 58
|
||||
LMP_ENCRYPTION_KEY_SIZE_MASK_RES= 59
|
||||
LMP_ENCRYPTION_KEY_SIZE_REQ = 16
|
||||
LMP_ENCRYPTION_MODE_REQ = 15
|
||||
LMP_ESCO_LINK_REQ = 127 << 8 + 12
|
||||
LMP_FEATURES_REQ = 39
|
||||
LMP_FEATURES_REQ_EXT = 127 << 8 + 3
|
||||
LMP_FEATURES_RES = 40
|
||||
LMP_FEATURES_RES_EXT = 127 << 8 + 4
|
||||
LMP_HOLD = 20
|
||||
LMP_HOLD_REQ = 21
|
||||
LMP_HOST_CONNECTION_REQ = 51
|
||||
LMP_IN_RAND = 8
|
||||
LMP_INCR_POWER_REQ = 31
|
||||
LMP_IO_CAPABILITY_REQ = 127 << 8 + 25
|
||||
LMP_IO_CAPABILITY_RES = 127 << 8 + 26
|
||||
LMP_KEYPRESS_NOTIFICATION = 127 << 8 + 30
|
||||
LMP_MAX_POWER = 33
|
||||
LMP_MAX_SLOT = 45
|
||||
LMP_MAX_SLOT_REQ = 46
|
||||
LMP_MIN_POWER = 34
|
||||
LMP_NAME_REQ = 1
|
||||
LMP_NAME_RES = 2
|
||||
LMP_NOT_ACCEPTED = 4
|
||||
LMP_NOT_ACCEPTED_EXT = 127 << 8 + 2
|
||||
LMP_NUMERIC_COMPARISON_FAILED = 127 << 8 + 27
|
||||
LMP_OOB_FAILED = 127 << 8 + 29
|
||||
LMP_PACKET_TYPE_TABLE_REQ = 127 << 8 + 11
|
||||
LMP_PAGE_MODE_REQ = 53
|
||||
LMP_PAGE_SCAN_MODE_REQ = 54
|
||||
LMP_PASSKEY_FAILED = 127 << 8 + 28
|
||||
LMP_PAUSE_ENCRYPTION_AES_REQ = 66
|
||||
LMP_PAUSE_ENCRYPTION_REQ = 127 << 8 + 23
|
||||
LMP_PING_REQ = 127 << 8 + 33
|
||||
LMP_PING_RES = 127 << 8 + 34
|
||||
LMP_POWER_CONTROL_REQ = 127 << 8 + 31
|
||||
LMP_POWER_CONTROL_RES = 127 << 8 + 32
|
||||
LMP_PREFERRED_RATE = 36
|
||||
LMP_QUALITY_OF_SERVICE = 41
|
||||
LMP_QUALITY_OF_SERVICE_REQ = 42
|
||||
LMP_REMOVE_ESCO_LINK_REQ = 127 << 8 + 13
|
||||
LMP_REMOVE_SCO_LINK_REQ = 44
|
||||
LMP_RESUME_ENCRYPTION_REQ = 127 << 8 + 24
|
||||
LMP_SAM_DEFINE_MAP = 127 << 8 + 36
|
||||
LMP_SAM_SET_TYPE0 = 127 << 8 + 35
|
||||
LMP_SAM_SWITCH = 127 << 8 + 37
|
||||
LMP_SCO_LINK_REQ = 43
|
||||
LMP_SET_AFH = 60
|
||||
LMP_SETUP_COMPLETE = 49
|
||||
LMP_SIMPLE_PAIRING_CONFIRM = 63
|
||||
LMP_SIMPLE_PAIRING_NUMBER = 64
|
||||
LMP_SLOT_OFFSET = 52
|
||||
LMP_SNIFF_REQ = 23
|
||||
LMP_SNIFF_SUBRATING_REQ = 127 << 8 + 21
|
||||
LMP_SNIFF_SUBRATING_RES = 127 << 8 + 22
|
||||
LMP_SRES = 12
|
||||
LMP_START_ENCRYPTION_REQ = 17
|
||||
LMP_STOP_ENCRYPTION_REQ = 18
|
||||
LMP_SUPERVISION_TIMEOUT = 55
|
||||
LMP_SWITCH_REQ = 19
|
||||
LMP_TEMP_KEY = 14
|
||||
LMP_TEMP_RAND = 13
|
||||
LMP_TEST_ACTIVATE = 56
|
||||
LMP_TEST_CONTROL = 57
|
||||
LMP_TIMING_ACCURACY_REQ = 47
|
||||
LMP_TIMING_ACCURACY_RES = 48
|
||||
LMP_UNIT_KEY = 10
|
||||
LMP_UNSNIFF_REQ = 24
|
||||
LMP_USE_SEMI_PERMANENT_KEY = 50
|
||||
LMP_VERSION_REQ = 37
|
||||
LMP_VERSION_RES = 38
|
||||
# fmt: on
|
||||
|
||||
@classmethod
|
||||
def parse_from(cls, data: bytes, offset: int = 0) -> tuple[int, Opcode]:
|
||||
opcode = data[offset]
|
||||
if opcode in (124, 127):
|
||||
opcode = struct.unpack('>H', data)[0]
|
||||
return offset + 2, Opcode(opcode)
|
||||
return offset + 1, Opcode(opcode)
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
if self.value >> 8:
|
||||
return struct.pack('>H', self.value)
|
||||
return bytes([self.value])
|
||||
|
||||
@classmethod
|
||||
def type_metadata(cls):
|
||||
return hci.metadata(
|
||||
{
|
||||
'serializer': bytes,
|
||||
'parser': lambda data, offset: (Opcode.parse_from(data, offset)),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class Packet:
|
||||
'''
|
||||
See Bluetooth spec @ Vol 2, Part C - 5.1 PDU summary
|
||||
'''
|
||||
|
||||
subclasses: dict[int, type[Packet]] = {}
|
||||
opcode: Opcode
|
||||
fields: hci.Fields = ()
|
||||
_payload: bytes = b''
|
||||
|
||||
_Packet = TypeVar("_Packet", bound="Packet")
|
||||
|
||||
@classmethod
|
||||
def subclass(cls, subclass: type[_Packet]) -> type[_Packet]:
|
||||
# Register a factory for this class
|
||||
cls.subclasses[subclass.opcode] = subclass
|
||||
subclass.fields = hci.HCI_Object.fields_from_dataclass(subclass)
|
||||
|
||||
return subclass
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data: bytes) -> Packet:
|
||||
offset, opcode = Opcode.parse_from(data)
|
||||
if not (subclass := cls.subclasses.get(opcode)):
|
||||
instance = Packet()
|
||||
instance.opcode = opcode
|
||||
else:
|
||||
instance = subclass(
|
||||
**hci.HCI_Object.dict_from_bytes(data, offset, subclass.fields)
|
||||
)
|
||||
instance.payload = data[offset:]
|
||||
return instance
|
||||
|
||||
@property
|
||||
def payload(self) -> bytes:
|
||||
if self._payload is None:
|
||||
self._payload = hci.HCI_Object.dict_to_bytes(self.__dict__, self.fields)
|
||||
return self._payload
|
||||
|
||||
@payload.setter
|
||||
def payload(self, value: bytes) -> None:
|
||||
self._payload = value
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return bytes(self.opcode) + self.payload
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpAccepted(Packet):
|
||||
opcode = Opcode.LMP_ACCEPTED
|
||||
|
||||
response_opcode: Opcode = field(metadata=Opcode.type_metadata())
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpNotAccepted(Packet):
|
||||
opcode = Opcode.LMP_NOT_ACCEPTED
|
||||
|
||||
response_opcode: Opcode = field(metadata=Opcode.type_metadata())
|
||||
error_code: int = field(metadata=hci.metadata(1))
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpAcceptedExt(Packet):
|
||||
opcode = Opcode.LMP_ACCEPTED_EXT
|
||||
|
||||
response_opcode: Opcode = field(metadata=Opcode.type_metadata())
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpNotAcceptedExt(Packet):
|
||||
opcode = Opcode.LMP_NOT_ACCEPTED_EXT
|
||||
|
||||
response_opcode: Opcode = field(metadata=Opcode.type_metadata())
|
||||
error_code: int = field(metadata=hci.metadata(1))
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpAuRand(Packet):
|
||||
opcode = Opcode.LMP_AU_RAND
|
||||
|
||||
random_number: bytes = field(metadata=hci.metadata(16))
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpDetach(Packet):
|
||||
opcode = Opcode.LMP_DETACH
|
||||
|
||||
error_code: int = field(metadata=hci.metadata(1))
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpEscoLinkReq(Packet):
|
||||
opcode = Opcode.LMP_ESCO_LINK_REQ
|
||||
|
||||
esco_handle: int = field(metadata=hci.metadata(1))
|
||||
esco_lt_addr: int = field(metadata=hci.metadata(1))
|
||||
timing_control_flags: int = field(metadata=hci.metadata(1))
|
||||
d_esco: int = field(metadata=hci.metadata(1))
|
||||
t_esco: int = field(metadata=hci.metadata(1))
|
||||
w_esco: int = field(metadata=hci.metadata(1))
|
||||
esco_packet_type_c_to_p: int = field(metadata=hci.metadata(1))
|
||||
esco_packet_type_p_to_c: int = field(metadata=hci.metadata(1))
|
||||
packet_length_c_to_p: int = field(metadata=hci.metadata(2))
|
||||
packet_length_p_to_c: int = field(metadata=hci.metadata(2))
|
||||
air_mode: int = field(metadata=hci.metadata(1))
|
||||
negotiation_state: int = field(metadata=hci.metadata(1))
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpHostConnectionReq(Packet):
|
||||
opcode = Opcode.LMP_HOST_CONNECTION_REQ
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpRemoveEscoLinkReq(Packet):
|
||||
opcode = Opcode.LMP_REMOVE_ESCO_LINK_REQ
|
||||
|
||||
esco_handle: int = field(metadata=hci.metadata(1))
|
||||
error_code: int = field(metadata=hci.metadata(1))
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpRemoveScoLinkReq(Packet):
|
||||
opcode = Opcode.LMP_REMOVE_SCO_LINK_REQ
|
||||
|
||||
sco_handle: int = field(metadata=hci.metadata(1))
|
||||
error_code: int = field(metadata=hci.metadata(1))
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpScoLinkReq(Packet):
|
||||
opcode = Opcode.LMP_SCO_LINK_REQ
|
||||
|
||||
sco_handle: int = field(metadata=hci.metadata(1))
|
||||
timing_control_flags: int = field(metadata=hci.metadata(1))
|
||||
d_sco: int = field(metadata=hci.metadata(1))
|
||||
t_sco: int = field(metadata=hci.metadata(1))
|
||||
sco_packet: int = field(metadata=hci.metadata(1))
|
||||
air_mode: int = field(metadata=hci.metadata(1))
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpSwitchReq(Packet):
|
||||
opcode = Opcode.LMP_SWITCH_REQ
|
||||
|
||||
switch_instant: int = field(metadata=hci.metadata(4), default=0)
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpNameReq(Packet):
|
||||
opcode = Opcode.LMP_NAME_REQ
|
||||
|
||||
name_offset: int = field(metadata=hci.metadata(2))
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpNameRes(Packet):
|
||||
opcode = Opcode.LMP_NAME_RES
|
||||
|
||||
name_offset: int = field(metadata=hci.metadata(2))
|
||||
name_length: int = field(metadata=hci.metadata(3))
|
||||
name_fregment: bytes = field(metadata=hci.metadata('*'))
|
||||
@@ -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) :]
|
||||
|
||||
@@ -65,7 +65,7 @@ class BtSnooper(Snooper):
|
||||
"""
|
||||
|
||||
IDENTIFICATION_PATTERN = b'btsnoop\0'
|
||||
TIMESTAMP_ANCHOR = datetime.datetime(2000, 1, 1)
|
||||
TIMESTAMP_ANCHOR = datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc)
|
||||
TIMESTAMP_DELTA = 0x00E03AB44A676000
|
||||
ONE_MS = datetime.timedelta(microseconds=1)
|
||||
|
||||
@@ -85,7 +85,13 @@ class BtSnooper(Snooper):
|
||||
|
||||
# Compute the current timestamp
|
||||
timestamp = (
|
||||
int((datetime.datetime.utcnow() - self.TIMESTAMP_ANCHOR) / self.ONE_MS)
|
||||
int(
|
||||
(
|
||||
datetime.datetime.now(tz=datetime.timezone.utc)
|
||||
- self.TIMESTAMP_ANCHOR
|
||||
)
|
||||
/ self.ONE_MS
|
||||
)
|
||||
+ self.TIMESTAMP_DELTA
|
||||
)
|
||||
|
||||
@@ -129,7 +135,7 @@ def create_snooper(spec: str) -> Generator[Snooper, None, None]:
|
||||
records will be written to that file if it can be opened/created.
|
||||
The keyword args that may be referenced by the string pattern are:
|
||||
now: the value of `datetime.now()`
|
||||
utcnow: the value of `datetime.utcnow()`
|
||||
utcnow: the value of `datetime.now(tz=datetime.timezone.utc)`
|
||||
pid: the current process ID.
|
||||
instance: the instance ID in the current process.
|
||||
|
||||
@@ -153,7 +159,7 @@ def create_snooper(spec: str) -> Generator[Snooper, None, None]:
|
||||
global _SNOOPER_INSTANCE_COUNT
|
||||
file_path = io_name.format(
|
||||
now=datetime.datetime.now(),
|
||||
utcnow=datetime.datetime.utcnow(),
|
||||
utcnow=datetime.datetime.now(tz=datetime.timezone.utc),
|
||||
pid=os.getpid(),
|
||||
instance=_SNOOPER_INSTANCE_COUNT,
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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()
|
||||
|
||||
@@ -241,11 +241,7 @@ def cancel_on_event(
|
||||
return
|
||||
msg = f'abort: {event} event occurred.'
|
||||
if isinstance(future, asyncio.Task):
|
||||
# python < 3.9 does not support passing a message on `Task.cancel`
|
||||
if sys.version_info < (3, 9, 0):
|
||||
future.cancel()
|
||||
else:
|
||||
future.cancel(msg)
|
||||
future.cancel(msg)
|
||||
else:
|
||||
future.set_exception(asyncio.CancelledError(msg))
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ GETTING STARTED WITH BUMBLE
|
||||
|
||||
# Prerequisites
|
||||
|
||||
You need Python 3.9 or above.
|
||||
You need Python 3.10 or above.
|
||||
Visit the [Python site](https://www.python.org/) for instructions on how to install Python
|
||||
for your platform.
|
||||
Throughout the documentation, when shell commands are shown, it is assumed that you can
|
||||
|
||||
@@ -31,7 +31,7 @@ Some of the configurations that may be useful:
|
||||
|
||||
See the [use cases page](use_cases/index.md) for more use cases.
|
||||
|
||||
The project is implemented in Python (Python >= 3.9 is required). A number of APIs for functionality that is inherently I/O bound is implemented in terms of python coroutines with async IO. This means that all of the concurrent tasks run in the same thread, which makes everything much simpler and more predictable.
|
||||
The project is implemented in Python (Python >= 3.10 is required). A number of APIs for functionality that is inherently I/O bound is implemented in terms of python coroutines with async IO. This means that all of the concurrent tasks run in the same thread, which makes everything much simpler and more predictable.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
PLATFORMS
|
||||
=========
|
||||
|
||||
Most of the code included in the project should run on any platform that supports Python >= 3.9. Not all features are supported on all platforms (for example, USB dongle support is only available on platforms where the python USB library is functional).
|
||||
Most of the code included in the project should run on any platform that supports Python >= 3.10. Not all features are supported on all platforms (for example, USB dongle support is only available on platforms where the python USB library is functional).
|
||||
|
||||
For platform-specific information, see the following pages:
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
name: bumble
|
||||
channels:
|
||||
- defaults
|
||||
- conda-forge
|
||||
dependencies:
|
||||
- pip=23
|
||||
- python=3.9
|
||||
- pip:
|
||||
- --editable .[development,documentation,test]
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ readme = "README.md"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE"]
|
||||
authors = [{ name = "Google", email = "bumble-dev@google.com" }]
|
||||
requires-python = ">=3.9"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"aiohttp ~= 3.8; platform_system!='Emscripten'",
|
||||
"appdirs >= 1.4; platform_system!='Emscripten'",
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -789,6 +789,19 @@ async def test_accept_classic_connection(roles: tuple[hci.Role, hci.Role]):
|
||||
assert devices.connections[1].role == roles[1]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_remote_name_request():
|
||||
devices = TwoDevices()
|
||||
devices[0].classic_enabled = True
|
||||
devices[1].classic_enabled = True
|
||||
expected_name = devices[1].name = "An Awesome Name"
|
||||
await devices[0].power_on()
|
||||
await devices[1].power_on()
|
||||
actual_name = await devices[0].request_remote_name(devices[1].public_address)
|
||||
assert actual_name == expected_name
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def run_test_device():
|
||||
await test_device_connect_parallel()
|
||||
|
||||
@@ -99,6 +99,8 @@ class TwoDevices:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def async_barrier():
|
||||
ready = asyncio.get_running_loop().create_future()
|
||||
asyncio.get_running_loop().call_soon(ready.set_result, None)
|
||||
await ready
|
||||
# TODO: Remove async barrier - this doesn't always mean what we want.
|
||||
for _ in range(3):
|
||||
ready = asyncio.get_running_loop().create_future()
|
||||
asyncio.get_running_loop().call_soon(ready.set_result, None)
|
||||
await ready
|
||||
|
||||
Reference in New Issue
Block a user