mirror of
https://github.com/google/bumble.git
synced 2026-05-06 03:38:01 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bd83273e8 | ||
|
|
5e9fc89f80 | ||
|
|
2686663eb2 | ||
|
|
55801bc2ca | ||
|
|
6cecc16519 | ||
|
|
a57cf13e2e | ||
|
|
58f153afc4 | ||
|
|
7569da37e4 | ||
|
|
a8019a70da | ||
|
|
685f1dc43e | ||
|
|
220b3b0236 | ||
|
|
3495eb52ba | ||
|
|
1f7a1401eb | ||
|
|
ce2b02b62a | ||
|
|
5e55c0e358 | ||
|
|
ebeb0dc9f1 | ||
|
|
776bdae519 | ||
|
|
b2d9541f8f | ||
|
|
637224d5bc | ||
|
|
92ab171013 | ||
|
|
592475e2ed | ||
|
|
12bcdb7770 | ||
|
|
7a58f36020 |
@@ -18,7 +18,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import asyncio.subprocess
|
||||
import collections
|
||||
import contextlib
|
||||
import dataclasses
|
||||
@@ -36,7 +35,6 @@ from typing import (
|
||||
)
|
||||
|
||||
import click
|
||||
import pyee
|
||||
|
||||
try:
|
||||
import lc3 # type: ignore # pylint: disable=E0401
|
||||
@@ -99,12 +97,31 @@ def codec_config_string(
|
||||
return '\n'.join(indent + line for line in lines)
|
||||
|
||||
|
||||
def broadcast_code_bytes(broadcast_code: str) -> bytes:
|
||||
"""
|
||||
Convert a broadcast code string to a 16-byte value.
|
||||
|
||||
If `broadcast_code` is `0x` followed by 32 hex characters, it is interpreted as a
|
||||
raw 16-byte raw broadcast code in big-endian byte order.
|
||||
Otherwise, `broadcast_code` is converted to a 16-byte value as specified in
|
||||
BLUETOOTH CORE SPECIFICATION Version 6.0 | Vol 3, Part C , section 3.2.6.3
|
||||
"""
|
||||
if broadcast_code.startswith("0x") and len(broadcast_code) == 34:
|
||||
return bytes.fromhex(broadcast_code[2:])[::-1]
|
||||
|
||||
broadcast_code_utf8 = broadcast_code.encode("utf-8")
|
||||
if len(broadcast_code_utf8) > 16:
|
||||
raise ValueError("broadcast code must be <= 16 bytes in utf-8 encoding")
|
||||
padding = bytes(16 - len(broadcast_code_utf8))
|
||||
return broadcast_code_utf8 + padding
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Scan For Broadcasts
|
||||
# -----------------------------------------------------------------------------
|
||||
class BroadcastScanner(pyee.EventEmitter):
|
||||
class BroadcastScanner(bumble.utils.EventEmitter):
|
||||
@dataclasses.dataclass
|
||||
class Broadcast(pyee.EventEmitter):
|
||||
class Broadcast(bumble.utils.EventEmitter):
|
||||
name: str | None
|
||||
sync: bumble.device.PeriodicAdvertisingSync
|
||||
broadcast_id: int
|
||||
@@ -234,22 +251,14 @@ class BroadcastScanner(pyee.EventEmitter):
|
||||
|
||||
if self.biginfo:
|
||||
print(color(' BIG:', 'cyan'))
|
||||
print(
|
||||
color(' Number of BIS:', 'magenta'),
|
||||
self.biginfo.num_bis,
|
||||
)
|
||||
print(
|
||||
color(' PHY: ', 'magenta'),
|
||||
self.biginfo.phy.name,
|
||||
)
|
||||
print(
|
||||
color(' Framed: ', 'magenta'),
|
||||
self.biginfo.framed,
|
||||
)
|
||||
print(
|
||||
color(' Encrypted: ', 'magenta'),
|
||||
self.biginfo.encrypted,
|
||||
)
|
||||
print(color(' Number of BIS:', 'magenta'), self.biginfo.num_bis)
|
||||
print(color(' ISO Interval: ', 'magenta'), self.biginfo.iso_interval)
|
||||
print(color(' Max PDU: ', 'magenta'), self.biginfo.max_pdu)
|
||||
print(color(' SDU Interval: ', 'magenta'), self.biginfo.sdu_interval)
|
||||
print(color(' Max SDU: ', 'magenta'), self.biginfo.max_sdu)
|
||||
print(color(' PHY: ', 'magenta'), self.biginfo.phy.name)
|
||||
print(color(' Framed: ', 'magenta'), self.biginfo.framed)
|
||||
print(color(' Encrypted: ', 'magenta'), self.biginfo.encrypted)
|
||||
|
||||
def on_sync_establishment(self) -> None:
|
||||
self.emit('sync_establishment')
|
||||
@@ -365,7 +374,7 @@ class BroadcastScanner(pyee.EventEmitter):
|
||||
self.emit('broadcast_loss', broadcast)
|
||||
|
||||
|
||||
class PrintingBroadcastScanner(pyee.EventEmitter):
|
||||
class PrintingBroadcastScanner(bumble.utils.EventEmitter):
|
||||
def __init__(
|
||||
self, device: bumble.device.Device, filter_duplicates: bool, sync_timeout: float
|
||||
) -> None:
|
||||
@@ -702,14 +711,13 @@ async def run_receive(
|
||||
|
||||
def on_change() -> None:
|
||||
if (
|
||||
broadcast.basic_audio_announcement
|
||||
and not basic_audio_announcement_scanned.is_set()
|
||||
):
|
||||
broadcast.basic_audio_announcement and broadcast.biginfo
|
||||
) and not basic_audio_announcement_scanned.is_set():
|
||||
basic_audio_announcement_scanned.set()
|
||||
|
||||
broadcast.on('change', on_change)
|
||||
if not broadcast.basic_audio_announcement:
|
||||
print('Wait for Basic Audio Announcement...')
|
||||
if not broadcast.basic_audio_announcement or not broadcast.biginfo:
|
||||
print('Wait for Basic Audio Announcement and BIG Info...')
|
||||
await basic_audio_announcement_scanned.wait()
|
||||
print('Basic Audio Announcement found')
|
||||
broadcast.print()
|
||||
@@ -730,7 +738,7 @@ async def run_receive(
|
||||
big_sync_timeout=0x4000,
|
||||
bis=[bis.index for bis in subgroup.bis],
|
||||
broadcast_code=(
|
||||
bytes.fromhex(broadcast_code) if broadcast_code else None
|
||||
broadcast_code_bytes(broadcast_code) if broadcast_code else None
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -944,7 +952,7 @@ async def run_transmit(
|
||||
max_transport_latency=65,
|
||||
rtn=4,
|
||||
broadcast_code=(
|
||||
bytes.fromhex(broadcast_code) if broadcast_code else None
|
||||
broadcast_code_bytes(broadcast_code) if broadcast_code else None
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -1088,7 +1096,7 @@ def pair(ctx, transport, address):
|
||||
'--broadcast-code',
|
||||
metavar='BROADCAST_CODE',
|
||||
type=str,
|
||||
help='Broadcast encryption code in hex format',
|
||||
help='Broadcast encryption code (string or raw hex format prefixed with 0x)',
|
||||
)
|
||||
@click.option(
|
||||
'--sync-timeout',
|
||||
|
||||
@@ -28,8 +28,7 @@ import click
|
||||
|
||||
from bumble import l2cap
|
||||
from bumble.core import (
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
BT_LE_TRANSPORT,
|
||||
PhysicalTransport,
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
BT_RFCOMM_PROTOCOL_ID,
|
||||
UUID,
|
||||
@@ -42,8 +41,7 @@ from bumble.hci import (
|
||||
HCI_LE_1M_PHY,
|
||||
HCI_LE_2M_PHY,
|
||||
HCI_LE_CODED_PHY,
|
||||
HCI_CENTRAL_ROLE,
|
||||
HCI_PERIPHERAL_ROLE,
|
||||
Role,
|
||||
HCI_Constant,
|
||||
HCI_Error,
|
||||
HCI_StatusError,
|
||||
@@ -113,7 +111,7 @@ def print_connection_phy(phy):
|
||||
|
||||
def print_connection(connection):
|
||||
params = []
|
||||
if connection.transport == BT_LE_TRANSPORT:
|
||||
if connection.transport == PhysicalTransport.LE:
|
||||
params.append(
|
||||
'DL=('
|
||||
f'TX:{connection.data_length[0]}/{connection.data_length[1]},'
|
||||
@@ -189,7 +187,7 @@ def log_stats(title, stats, precision=2):
|
||||
|
||||
|
||||
async def switch_roles(connection, role):
|
||||
target_role = HCI_CENTRAL_ROLE if role == "central" else HCI_PERIPHERAL_ROLE
|
||||
target_role = Role.CENTRAL if role == "central" else Role.PERIPHERAL
|
||||
if connection.role != target_role:
|
||||
logging.info(f'{color("### Switching roles to:", "cyan")} {role}')
|
||||
try:
|
||||
@@ -1275,7 +1273,11 @@ class Central(Connection.Listener):
|
||||
self.connection = await self.device.connect(
|
||||
self.peripheral_address,
|
||||
connection_parameters_preferences=self.connection_parameter_preferences,
|
||||
transport=BT_BR_EDR_TRANSPORT if self.classic else BT_LE_TRANSPORT,
|
||||
transport=(
|
||||
PhysicalTransport.BR_EDR
|
||||
if self.classic
|
||||
else PhysicalTransport.LE
|
||||
),
|
||||
)
|
||||
except CommandTimeoutError:
|
||||
logging.info(color('!!! Connection timed out', 'red'))
|
||||
|
||||
@@ -55,7 +55,7 @@ from prompt_toolkit.layout import (
|
||||
from bumble import __version__
|
||||
import bumble.core
|
||||
from bumble import colors
|
||||
from bumble.core import UUID, AdvertisingData, BT_LE_TRANSPORT
|
||||
from bumble.core import UUID, AdvertisingData, PhysicalTransport
|
||||
from bumble.device import (
|
||||
ConnectionParametersPreferences,
|
||||
ConnectionPHY,
|
||||
|
||||
@@ -37,6 +37,7 @@ import click
|
||||
import aiohttp.web
|
||||
|
||||
import bumble
|
||||
from bumble import utils
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device, DeviceConfiguration, AdvertisingParameters, CisLink
|
||||
@@ -359,7 +360,9 @@ class Speaker:
|
||||
pcm = decoder.decode(
|
||||
pdu.iso_sdu_fragment, bit_depth=DEFAULT_PCM_BYTES_PER_SAMPLE * 8
|
||||
)
|
||||
self.device.abort_on('disconnection', self.ui_server.send_audio(pcm))
|
||||
utils.cancel_on_event(
|
||||
self.device, 'disconnection', self.ui_server.send_audio(pcm)
|
||||
)
|
||||
|
||||
def on_ase_state_change(ase: ascs.AseStateMachine) -> None:
|
||||
codec_config = ase.codec_specific_configuration
|
||||
@@ -373,7 +376,8 @@ class Speaker:
|
||||
or codec_config.codec_frames_per_sdu is None
|
||||
):
|
||||
return
|
||||
ase.cis_link.abort_on(
|
||||
utils.cancel_on_event(
|
||||
ase.cis_link,
|
||||
'disconnection',
|
||||
lc3_source_task(
|
||||
filename=self.lc3_input_file_path,
|
||||
|
||||
@@ -31,8 +31,7 @@ from bumble.keys import JsonKeyStore
|
||||
from bumble.core import (
|
||||
AdvertisingData,
|
||||
ProtocolError,
|
||||
BT_LE_TRANSPORT,
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
PhysicalTransport,
|
||||
)
|
||||
from bumble.gatt import (
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||
@@ -422,7 +421,9 @@ async def pair(
|
||||
print(color(f'=== Connecting to {address_or_name}...', 'green'))
|
||||
connection = await device.connect(
|
||||
address_or_name,
|
||||
transport=BT_LE_TRANSPORT if mode == 'le' else BT_BR_EDR_TRANSPORT,
|
||||
transport=(
|
||||
PhysicalTransport.LE if mode == 'le' else PhysicalTransport.BR_EDR
|
||||
),
|
||||
)
|
||||
|
||||
if not request:
|
||||
|
||||
@@ -56,7 +56,7 @@ from bumble.core import (
|
||||
AdvertisingData,
|
||||
ConnectionError as BumbleConnectionError,
|
||||
DeviceClass,
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
PhysicalTransport,
|
||||
)
|
||||
from bumble.device import Connection, Device, DeviceConfiguration
|
||||
from bumble.hci import Address, HCI_CONNECTION_ALREADY_EXISTS_ERROR, HCI_Constant
|
||||
@@ -286,7 +286,7 @@ class Player:
|
||||
|
||||
async def connect(self, device: Device, address: str) -> Connection:
|
||||
print(color(f"Connecting to {address}...", "green"))
|
||||
connection = await device.connect(address, transport=BT_BR_EDR_TRANSPORT)
|
||||
connection = await device.connect(address, transport=PhysicalTransport.BR_EDR)
|
||||
|
||||
# Request authentication
|
||||
if self.authenticate:
|
||||
@@ -402,7 +402,7 @@ class Player:
|
||||
|
||||
async def pair(self, device: Device, address: str) -> None:
|
||||
print(color(f"Connecting to {address}...", "green"))
|
||||
connection = await device.connect(address, transport=BT_BR_EDR_TRANSPORT)
|
||||
connection = await device.connect(address, transport=PhysicalTransport.BR_EDR)
|
||||
|
||||
print(color("Pairing...", "magenta"))
|
||||
await connection.authenticate()
|
||||
|
||||
@@ -271,7 +271,7 @@ class ClientBridge:
|
||||
print(color(f"@@@ Connecting to Bluetooth {self.address}", "blue"))
|
||||
assert self.device
|
||||
self.connection = await self.device.connect(
|
||||
self.address, transport=core.BT_BR_EDR_TRANSPORT
|
||||
self.address, transport=core.PhysicalTransport.BR_EDR
|
||||
)
|
||||
print(color(f"@@@ Bluetooth connection: {self.connection}", "blue"))
|
||||
self.connection.on("disconnection", self.on_disconnection)
|
||||
|
||||
@@ -34,7 +34,7 @@ from aiohttp import web
|
||||
|
||||
import bumble
|
||||
from bumble.colors import color
|
||||
from bumble.core import BT_BR_EDR_TRANSPORT, CommandTimeoutError
|
||||
from bumble.core import PhysicalTransport, CommandTimeoutError
|
||||
from bumble.device import Connection, Device, DeviceConfiguration
|
||||
from bumble.hci import HCI_StatusError
|
||||
from bumble.pairing import PairingConfig
|
||||
@@ -568,7 +568,9 @@ class Speaker:
|
||||
async def connect(self, address):
|
||||
# Connect to the source
|
||||
print(f'=== Connecting to {address}...')
|
||||
connection = await self.device.connect(address, transport=BT_BR_EDR_TRANSPORT)
|
||||
connection = await self.device.connect(
|
||||
address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
print(f'=== Connected to {connection.peer_address}')
|
||||
|
||||
# Request authentication
|
||||
|
||||
@@ -26,9 +26,9 @@ from typing import Awaitable, Callable
|
||||
from typing_extensions import ClassVar, Self
|
||||
|
||||
|
||||
from .codecs import AacAudioRtpPacket
|
||||
from .company_ids import COMPANY_IDENTIFIERS
|
||||
from .sdp import (
|
||||
from bumble.codecs import AacAudioRtpPacket
|
||||
from bumble.company_ids import COMPANY_IDENTIFIERS
|
||||
from bumble.sdp import (
|
||||
DataElement,
|
||||
ServiceAttribute,
|
||||
SDP_PUBLIC_BROWSE_ROOT,
|
||||
@@ -38,7 +38,7 @@ from .sdp import (
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
)
|
||||
from .core import (
|
||||
from bumble.core import (
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
BT_AUDIO_SOURCE_SERVICE,
|
||||
BT_AUDIO_SINK_SERVICE,
|
||||
@@ -46,7 +46,7 @@ from .core import (
|
||||
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
||||
name_or_number,
|
||||
)
|
||||
from .rtp import MediaPacket
|
||||
from bumble.rtp import MediaPacket
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -155,7 +155,7 @@ def flags_to_list(flags, values):
|
||||
# -----------------------------------------------------------------------------
|
||||
def make_audio_source_service_sdp_records(service_record_handle, version=(1, 3)):
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from .avdtp import AVDTP_PSM
|
||||
from bumble.avdtp import AVDTP_PSM
|
||||
|
||||
version_int = version[0] << 8 | version[1]
|
||||
return [
|
||||
@@ -209,7 +209,7 @@ def make_audio_source_service_sdp_records(service_record_handle, version=(1, 3))
|
||||
# -----------------------------------------------------------------------------
|
||||
def make_audio_sink_service_sdp_records(service_record_handle, version=(1, 3)):
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from .avdtp import AVDTP_PSM
|
||||
from bumble.avdtp import AVDTP_PSM
|
||||
|
||||
version_int = version[0] << 8 | version[1]
|
||||
return [
|
||||
|
||||
@@ -41,7 +41,6 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from pyee import EventEmitter
|
||||
|
||||
from bumble import utils
|
||||
from bumble.core import UUID, name_or_number, InvalidOperationError, ProtocolError
|
||||
@@ -223,7 +222,12 @@ UUID_2_FIELD_SPEC = lambda x, y: UUID.parse_uuid_2(x, y) # noqa: E731
|
||||
# Exceptions
|
||||
# -----------------------------------------------------------------------------
|
||||
class ATT_Error(ProtocolError):
|
||||
def __init__(self, error_code, att_handle=0x0000, message=''):
|
||||
error_code: int
|
||||
att_handle: int
|
||||
|
||||
def __init__(
|
||||
self, error_code: int, att_handle: int = 0x0000, message: str = ''
|
||||
) -> None:
|
||||
super().__init__(
|
||||
error_code,
|
||||
error_namespace='att',
|
||||
@@ -233,7 +237,10 @@ class ATT_Error(ProtocolError):
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return f'ATT_Error(error={self.error_name}, handle={self.att_handle:04X}): {self.message}'
|
||||
return (
|
||||
f'ATT_Error(error={self.error_name}, '
|
||||
f'handle={self.att_handle:04X}): {self.message}'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -790,7 +797,7 @@ class AttributeValue(Generic[_T]):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Attribute(EventEmitter, Generic[_T]):
|
||||
class Attribute(utils.EventEmitter, Generic[_T]):
|
||||
class Permissions(enum.IntFlag):
|
||||
READABLE = 0x01
|
||||
WRITEABLE = 0x02
|
||||
@@ -837,7 +844,7 @@ class Attribute(EventEmitter, Generic[_T]):
|
||||
permissions: Union[str, Attribute.Permissions],
|
||||
value: Union[AttributeValue[_T], _T, None] = None,
|
||||
) -> None:
|
||||
EventEmitter.__init__(self)
|
||||
utils.EventEmitter.__init__(self)
|
||||
self.handle = 0
|
||||
self.end_group_handle = 0
|
||||
if isinstance(permissions, str):
|
||||
|
||||
@@ -21,7 +21,7 @@ import struct
|
||||
from typing import Dict, Type, Union, Tuple
|
||||
|
||||
from bumble import core
|
||||
from bumble.utils import OpenIntEnum
|
||||
from bumble import utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -43,7 +43,7 @@ class Frame:
|
||||
EXTENDED = 0x1E
|
||||
UNIT = 0x1F
|
||||
|
||||
class OperationCode(OpenIntEnum):
|
||||
class OperationCode(utils.OpenIntEnum):
|
||||
# 0x00 - 0x0F: Unit and subunit commands
|
||||
VENDOR_DEPENDENT = 0x00
|
||||
RESERVE = 0x01
|
||||
@@ -204,7 +204,7 @@ class Frame:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class CommandFrame(Frame):
|
||||
class CommandType(OpenIntEnum):
|
||||
class CommandType(utils.OpenIntEnum):
|
||||
# AV/C Digital Interface Command Set General Specification Version 4.1
|
||||
# Table 7.1
|
||||
CONTROL = 0x00
|
||||
@@ -240,7 +240,7 @@ class CommandFrame(Frame):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class ResponseFrame(Frame):
|
||||
class ResponseCode(OpenIntEnum):
|
||||
class ResponseCode(utils.OpenIntEnum):
|
||||
# AV/C Digital Interface Command Set General Specification Version 4.1
|
||||
# Table 7.2
|
||||
NOT_IMPLEMENTED = 0x08
|
||||
@@ -368,7 +368,7 @@ class PassThroughFrame:
|
||||
PRESSED = 0
|
||||
RELEASED = 1
|
||||
|
||||
class OperationId(OpenIntEnum):
|
||||
class OperationId(utils.OpenIntEnum):
|
||||
SELECT = 0x00
|
||||
UP = 0x01
|
||||
DOWN = 0x01
|
||||
|
||||
@@ -37,16 +37,15 @@ from typing import (
|
||||
cast,
|
||||
)
|
||||
|
||||
from pyee import EventEmitter
|
||||
|
||||
from .core import (
|
||||
from bumble.core import (
|
||||
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
||||
InvalidStateError,
|
||||
ProtocolError,
|
||||
InvalidArgumentError,
|
||||
name_or_number,
|
||||
)
|
||||
from .a2dp import (
|
||||
from bumble.a2dp import (
|
||||
A2DP_CODEC_TYPE_NAMES,
|
||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||
A2DP_NON_A2DP_CODEC_TYPE,
|
||||
@@ -56,9 +55,9 @@ from .a2dp import (
|
||||
SbcMediaCodecInformation,
|
||||
VendorSpecificMediaCodecInformation,
|
||||
)
|
||||
from .rtp import MediaPacket
|
||||
from . import sdp, device, l2cap
|
||||
from .colors import color
|
||||
from bumble.rtp import MediaPacket
|
||||
from bumble import sdp, device, l2cap, utils
|
||||
from bumble.colors import color
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1194,7 +1193,7 @@ class DelayReport_Reject(Simple_Reject):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Protocol(EventEmitter):
|
||||
class Protocol(utils.EventEmitter):
|
||||
local_endpoints: List[LocalStreamEndPoint]
|
||||
remote_endpoints: Dict[int, DiscoveredStreamEndPoint]
|
||||
streams: Dict[int, Stream]
|
||||
@@ -1680,7 +1679,7 @@ class Protocol(EventEmitter):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Listener(EventEmitter):
|
||||
class Listener(utils.EventEmitter):
|
||||
servers: Dict[int, Protocol]
|
||||
|
||||
@staticmethod
|
||||
@@ -2063,7 +2062,7 @@ class DiscoveredStreamEndPoint(StreamEndPoint, StreamEndPointProxy):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class LocalStreamEndPoint(StreamEndPoint, EventEmitter):
|
||||
class LocalStreamEndPoint(StreamEndPoint, utils.EventEmitter):
|
||||
stream: Optional[Stream]
|
||||
|
||||
def __init__(
|
||||
@@ -2076,7 +2075,7 @@ class LocalStreamEndPoint(StreamEndPoint, EventEmitter):
|
||||
configuration: Optional[Iterable[ServiceCapabilities]] = None,
|
||||
):
|
||||
StreamEndPoint.__init__(self, seid, media_type, tsep, 0, capabilities)
|
||||
EventEmitter.__init__(self)
|
||||
utils.EventEmitter.__init__(self)
|
||||
self.protocol = protocol
|
||||
self.configuration = configuration if configuration is not None else []
|
||||
self.stream = None
|
||||
|
||||
@@ -38,7 +38,6 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
import pyee
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device, Connection
|
||||
@@ -53,7 +52,7 @@ from bumble.sdp import (
|
||||
DataElement,
|
||||
ServiceAttribute,
|
||||
)
|
||||
from bumble.utils import AsyncRunner, OpenIntEnum
|
||||
from bumble import utils
|
||||
from bumble.core import (
|
||||
InvalidArgumentError,
|
||||
ProtocolError,
|
||||
@@ -307,7 +306,7 @@ class Command:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class GetCapabilitiesCommand(Command):
|
||||
class CapabilityId(OpenIntEnum):
|
||||
class CapabilityId(utils.OpenIntEnum):
|
||||
COMPANY_ID = 0x02
|
||||
EVENTS_SUPPORTED = 0x03
|
||||
|
||||
@@ -637,7 +636,7 @@ class RegisterNotificationResponse(Response):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class EventId(OpenIntEnum):
|
||||
class EventId(utils.OpenIntEnum):
|
||||
PLAYBACK_STATUS_CHANGED = 0x01
|
||||
TRACK_CHANGED = 0x02
|
||||
TRACK_REACHED_END = 0x03
|
||||
@@ -657,12 +656,12 @@ class EventId(OpenIntEnum):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class CharacterSetId(OpenIntEnum):
|
||||
class CharacterSetId(utils.OpenIntEnum):
|
||||
UTF_8 = 0x06
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class MediaAttributeId(OpenIntEnum):
|
||||
class MediaAttributeId(utils.OpenIntEnum):
|
||||
TITLE = 0x01
|
||||
ARTIST_NAME = 0x02
|
||||
ALBUM_NAME = 0x03
|
||||
@@ -682,7 +681,7 @@ class MediaAttribute:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PlayStatus(OpenIntEnum):
|
||||
class PlayStatus(utils.OpenIntEnum):
|
||||
STOPPED = 0x00
|
||||
PLAYING = 0x01
|
||||
PAUSED = 0x02
|
||||
@@ -701,33 +700,33 @@ class SongAndPlayStatus:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class ApplicationSetting:
|
||||
class AttributeId(OpenIntEnum):
|
||||
class AttributeId(utils.OpenIntEnum):
|
||||
EQUALIZER_ON_OFF = 0x01
|
||||
REPEAT_MODE = 0x02
|
||||
SHUFFLE_ON_OFF = 0x03
|
||||
SCAN_ON_OFF = 0x04
|
||||
|
||||
class EqualizerOnOffStatus(OpenIntEnum):
|
||||
class EqualizerOnOffStatus(utils.OpenIntEnum):
|
||||
OFF = 0x01
|
||||
ON = 0x02
|
||||
|
||||
class RepeatModeStatus(OpenIntEnum):
|
||||
class RepeatModeStatus(utils.OpenIntEnum):
|
||||
OFF = 0x01
|
||||
SINGLE_TRACK_REPEAT = 0x02
|
||||
ALL_TRACK_REPEAT = 0x03
|
||||
GROUP_REPEAT = 0x04
|
||||
|
||||
class ShuffleOnOffStatus(OpenIntEnum):
|
||||
class ShuffleOnOffStatus(utils.OpenIntEnum):
|
||||
OFF = 0x01
|
||||
ALL_TRACKS_SHUFFLE = 0x02
|
||||
GROUP_SHUFFLE = 0x03
|
||||
|
||||
class ScanOnOffStatus(OpenIntEnum):
|
||||
class ScanOnOffStatus(utils.OpenIntEnum):
|
||||
OFF = 0x01
|
||||
ALL_TRACKS_SCAN = 0x02
|
||||
GROUP_SCAN = 0x03
|
||||
|
||||
class GenericValue(OpenIntEnum):
|
||||
class GenericValue(utils.OpenIntEnum):
|
||||
pass
|
||||
|
||||
|
||||
@@ -816,7 +815,7 @@ class PlayerApplicationSettingChangedEvent(Event):
|
||||
@dataclass
|
||||
class Setting:
|
||||
attribute_id: ApplicationSetting.AttributeId
|
||||
value_id: OpenIntEnum
|
||||
value_id: utils.OpenIntEnum
|
||||
|
||||
player_application_settings: List[Setting]
|
||||
|
||||
@@ -824,7 +823,7 @@ class PlayerApplicationSettingChangedEvent(Event):
|
||||
def from_bytes(cls, pdu: bytes) -> PlayerApplicationSettingChangedEvent:
|
||||
def setting(attribute_id_int: int, value_id_int: int):
|
||||
attribute_id = ApplicationSetting.AttributeId(attribute_id_int)
|
||||
value_id: OpenIntEnum
|
||||
value_id: utils.OpenIntEnum
|
||||
if attribute_id == ApplicationSetting.AttributeId.EQUALIZER_ON_OFF:
|
||||
value_id = ApplicationSetting.EqualizerOnOffStatus(value_id_int)
|
||||
elif attribute_id == ApplicationSetting.AttributeId.REPEAT_MODE:
|
||||
@@ -994,7 +993,7 @@ class Delegate:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Protocol(pyee.EventEmitter):
|
||||
class Protocol(utils.EventEmitter):
|
||||
"""AVRCP Controller and Target protocol."""
|
||||
|
||||
class PacketType(enum.IntEnum):
|
||||
@@ -1003,7 +1002,7 @@ class Protocol(pyee.EventEmitter):
|
||||
CONTINUE = 0b10
|
||||
END = 0b11
|
||||
|
||||
class PduId(OpenIntEnum):
|
||||
class PduId(utils.OpenIntEnum):
|
||||
GET_CAPABILITIES = 0x10
|
||||
LIST_PLAYER_APPLICATION_SETTING_ATTRIBUTES = 0x11
|
||||
LIST_PLAYER_APPLICATION_SETTING_VALUES = 0x12
|
||||
@@ -1024,7 +1023,7 @@ class Protocol(pyee.EventEmitter):
|
||||
GET_FOLDER_ITEMS = 0x71
|
||||
GET_TOTAL_NUMBER_OF_ITEMS = 0x75
|
||||
|
||||
class StatusCode(OpenIntEnum):
|
||||
class StatusCode(utils.OpenIntEnum):
|
||||
INVALID_COMMAND = 0x00
|
||||
INVALID_PARAMETER = 0x01
|
||||
PARAMETER_CONTENT_ERROR = 0x02
|
||||
@@ -1466,7 +1465,7 @@ class Protocol(pyee.EventEmitter):
|
||||
if self.avctp_protocol is not None:
|
||||
# TODO: find a better strategy instead of just closing
|
||||
logger.warning("AVCTP protocol already active, closing connection")
|
||||
AsyncRunner.spawn(l2cap_channel.disconnect())
|
||||
utils.AsyncRunner.spawn(l2cap_channel.disconnect())
|
||||
return
|
||||
|
||||
self.avctp_protocol = avctp.Protocol(l2cap_channel)
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
|
||||
from .hci import HCI_Packet
|
||||
from .helpers import PacketTracer
|
||||
from bumble.hci import HCI_Packet
|
||||
from bumble.helpers import PacketTracer
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
@@ -25,10 +25,7 @@ import random
|
||||
import struct
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
BT_CENTRAL_ROLE,
|
||||
BT_PERIPHERAL_ROLE,
|
||||
BT_LE_TRANSPORT,
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
PhysicalTransport,
|
||||
)
|
||||
|
||||
from bumble.hci import (
|
||||
@@ -47,6 +44,7 @@ from bumble.hci import (
|
||||
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
||||
HCI_VERSION_BLUETOOTH_CORE_5_0,
|
||||
Address,
|
||||
Role,
|
||||
HCI_AclDataPacket,
|
||||
HCI_AclDataPacketAssembler,
|
||||
HCI_Command_Complete_Event,
|
||||
@@ -98,7 +96,7 @@ class CisLink:
|
||||
class Connection:
|
||||
controller: Controller
|
||||
handle: int
|
||||
role: int
|
||||
role: Role
|
||||
peer_address: Address
|
||||
link: Any
|
||||
transport: int
|
||||
@@ -390,10 +388,10 @@ class Controller:
|
||||
connection = Connection(
|
||||
controller=self,
|
||||
handle=connection_handle,
|
||||
role=BT_PERIPHERAL_ROLE,
|
||||
role=Role.PERIPHERAL,
|
||||
peer_address=peer_address,
|
||||
link=self.link,
|
||||
transport=BT_LE_TRANSPORT,
|
||||
transport=PhysicalTransport.LE,
|
||||
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
)
|
||||
self.peripheral_connections[peer_address] = connection
|
||||
@@ -450,10 +448,10 @@ class Controller:
|
||||
connection = Connection(
|
||||
controller=self,
|
||||
handle=connection_handle,
|
||||
role=BT_CENTRAL_ROLE,
|
||||
role=Role.CENTRAL,
|
||||
peer_address=peer_address,
|
||||
link=self.link,
|
||||
transport=BT_LE_TRANSPORT,
|
||||
transport=PhysicalTransport.LE,
|
||||
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
)
|
||||
self.central_connections[peer_address] = connection
|
||||
@@ -469,7 +467,7 @@ class Controller:
|
||||
HCI_LE_Connection_Complete_Event(
|
||||
status=status,
|
||||
connection_handle=connection.handle if connection else 0,
|
||||
role=BT_CENTRAL_ROLE,
|
||||
role=Role.CENTRAL,
|
||||
peer_address_type=le_create_connection_command.peer_address_type,
|
||||
peer_address=le_create_connection_command.peer_address,
|
||||
connection_interval=le_create_connection_command.connection_interval_min,
|
||||
@@ -531,7 +529,7 @@ class Controller:
|
||||
|
||||
def on_link_acl_data(self, sender_address, transport, data):
|
||||
# Look for the connection to which this data belongs
|
||||
if transport == BT_LE_TRANSPORT:
|
||||
if transport == PhysicalTransport.LE:
|
||||
connection = self.find_le_connection_by_address(sender_address)
|
||||
else:
|
||||
connection = self.find_classic_connection_by_address(sender_address)
|
||||
@@ -693,10 +691,10 @@ class Controller:
|
||||
controller=self,
|
||||
handle=connection_handle,
|
||||
# Role doesn't matter in Classic because they are managed by HCI_Role_Change and HCI_Role_Discovery
|
||||
role=BT_CENTRAL_ROLE,
|
||||
role=Role.CENTRAL,
|
||||
peer_address=peer_address,
|
||||
link=self.link,
|
||||
transport=BT_BR_EDR_TRANSPORT,
|
||||
transport=PhysicalTransport.BR_EDR,
|
||||
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
)
|
||||
self.classic_connections[peer_address] = connection
|
||||
@@ -761,10 +759,10 @@ class Controller:
|
||||
controller=self,
|
||||
handle=connection_handle,
|
||||
# Role doesn't matter in SCO.
|
||||
role=BT_CENTRAL_ROLE,
|
||||
role=Role.CENTRAL,
|
||||
peer_address=peer_address,
|
||||
link=self.link,
|
||||
transport=BT_BR_EDR_TRANSPORT,
|
||||
transport=PhysicalTransport.BR_EDR,
|
||||
link_type=link_type,
|
||||
)
|
||||
self.classic_connections[peer_address] = connection
|
||||
|
||||
121
bumble/core.py
121
bumble/core.py
@@ -23,7 +23,7 @@ from typing import cast, overload, Literal, Union, Optional
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble.company_ids import COMPANY_IDENTIFIERS
|
||||
from bumble.utils import OpenIntEnum
|
||||
from bumble import utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -31,11 +31,12 @@ from bumble.utils import OpenIntEnum
|
||||
# -----------------------------------------------------------------------------
|
||||
# fmt: off
|
||||
|
||||
BT_CENTRAL_ROLE = 0
|
||||
BT_PERIPHERAL_ROLE = 1
|
||||
class PhysicalTransport(enum.IntEnum):
|
||||
BR_EDR = 0
|
||||
LE = 1
|
||||
|
||||
BT_BR_EDR_TRANSPORT = 0
|
||||
BT_LE_TRANSPORT = 1
|
||||
BT_BR_EDR_TRANSPORT = PhysicalTransport.BR_EDR
|
||||
BT_LE_TRANSPORT = PhysicalTransport.LE
|
||||
|
||||
|
||||
# fmt: on
|
||||
@@ -729,7 +730,7 @@ class DeviceClass:
|
||||
# Appearance
|
||||
# -----------------------------------------------------------------------------
|
||||
class Appearance:
|
||||
class Category(OpenIntEnum):
|
||||
class Category(utils.OpenIntEnum):
|
||||
UNKNOWN = 0x0000
|
||||
PHONE = 0x0001
|
||||
COMPUTER = 0x0002
|
||||
@@ -783,13 +784,13 @@ class Appearance:
|
||||
SPIROMETER = 0x0037
|
||||
OUTDOOR_SPORTS_ACTIVITY = 0x0051
|
||||
|
||||
class UnknownSubcategory(OpenIntEnum):
|
||||
class UnknownSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_UNKNOWN = 0x00
|
||||
|
||||
class PhoneSubcategory(OpenIntEnum):
|
||||
class PhoneSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_PHONE = 0x00
|
||||
|
||||
class ComputerSubcategory(OpenIntEnum):
|
||||
class ComputerSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_COMPUTER = 0x00
|
||||
DESKTOP_WORKSTATION = 0x01
|
||||
SERVER_CLASS_COMPUTER = 0x02
|
||||
@@ -807,49 +808,49 @@ class Appearance:
|
||||
MINI_PC = 0x0E
|
||||
STICK_PC = 0x0F
|
||||
|
||||
class WatchSubcategory(OpenIntEnum):
|
||||
class WatchSubcategory(utils.OpenIntEnum):
|
||||
GENENERIC_WATCH = 0x00
|
||||
SPORTS_WATCH = 0x01
|
||||
SMARTWATCH = 0x02
|
||||
|
||||
class ClockSubcategory(OpenIntEnum):
|
||||
class ClockSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_CLOCK = 0x00
|
||||
|
||||
class DisplaySubcategory(OpenIntEnum):
|
||||
class DisplaySubcategory(utils.OpenIntEnum):
|
||||
GENERIC_DISPLAY = 0x00
|
||||
|
||||
class RemoteControlSubcategory(OpenIntEnum):
|
||||
class RemoteControlSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_REMOTE_CONTROL = 0x00
|
||||
|
||||
class EyeglassesSubcategory(OpenIntEnum):
|
||||
class EyeglassesSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_EYEGLASSES = 0x00
|
||||
|
||||
class TagSubcategory(OpenIntEnum):
|
||||
class TagSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_TAG = 0x00
|
||||
|
||||
class KeyringSubcategory(OpenIntEnum):
|
||||
class KeyringSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_KEYRING = 0x00
|
||||
|
||||
class MediaPlayerSubcategory(OpenIntEnum):
|
||||
class MediaPlayerSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_MEDIA_PLAYER = 0x00
|
||||
|
||||
class BarcodeScannerSubcategory(OpenIntEnum):
|
||||
class BarcodeScannerSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_BARCODE_SCANNER = 0x00
|
||||
|
||||
class ThermometerSubcategory(OpenIntEnum):
|
||||
class ThermometerSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_THERMOMETER = 0x00
|
||||
EAR_THERMOMETER = 0x01
|
||||
|
||||
class HeartRateSensorSubcategory(OpenIntEnum):
|
||||
class HeartRateSensorSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_HEART_RATE_SENSOR = 0x00
|
||||
HEART_RATE_BELT = 0x01
|
||||
|
||||
class BloodPressureSubcategory(OpenIntEnum):
|
||||
class BloodPressureSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_BLOOD_PRESSURE = 0x00
|
||||
ARM_BLOOD_PRESSURE = 0x01
|
||||
WRIST_BLOOD_PRESSURE = 0x02
|
||||
|
||||
class HumanInterfaceDeviceSubcategory(OpenIntEnum):
|
||||
class HumanInterfaceDeviceSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_HUMAN_INTERFACE_DEVICE = 0x00
|
||||
KEYBOARD = 0x01
|
||||
MOUSE = 0x02
|
||||
@@ -862,16 +863,16 @@ class Appearance:
|
||||
TOUCHPAD = 0x09
|
||||
PRESENTATION_REMOTE = 0x0A
|
||||
|
||||
class GlucoseMeterSubcategory(OpenIntEnum):
|
||||
class GlucoseMeterSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_GLUCOSE_METER = 0x00
|
||||
|
||||
class RunningWalkingSensorSubcategory(OpenIntEnum):
|
||||
class RunningWalkingSensorSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_RUNNING_WALKING_SENSOR = 0x00
|
||||
IN_SHOE_RUNNING_WALKING_SENSOR = 0x01
|
||||
ON_SHOW_RUNNING_WALKING_SENSOR = 0x02
|
||||
ON_HIP_RUNNING_WALKING_SENSOR = 0x03
|
||||
|
||||
class CyclingSubcategory(OpenIntEnum):
|
||||
class CyclingSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_CYCLING = 0x00
|
||||
CYCLING_COMPUTER = 0x01
|
||||
SPEED_SENSOR = 0x02
|
||||
@@ -879,7 +880,7 @@ class Appearance:
|
||||
POWER_SENSOR = 0x04
|
||||
SPEED_AND_CADENCE_SENSOR = 0x05
|
||||
|
||||
class ControlDeviceSubcategory(OpenIntEnum):
|
||||
class ControlDeviceSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_CONTROL_DEVICE = 0x00
|
||||
SWITCH = 0x01
|
||||
MULTI_SWITCH = 0x02
|
||||
@@ -894,13 +895,13 @@ class Appearance:
|
||||
ENERGY_HARVESTING_SWITCH = 0x0B
|
||||
PUSH_BUTTON = 0x0C
|
||||
|
||||
class NetworkDeviceSubcategory(OpenIntEnum):
|
||||
class NetworkDeviceSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_NETWORK_DEVICE = 0x00
|
||||
ACCESS_POINT = 0x01
|
||||
MESH_DEVICE = 0x02
|
||||
MESH_NETWORK_PROXY = 0x03
|
||||
|
||||
class SensorSubcategory(OpenIntEnum):
|
||||
class SensorSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_SENSOR = 0x00
|
||||
MOTION_SENSOR = 0x01
|
||||
AIR_QUALITY_SENSOR = 0x02
|
||||
@@ -928,7 +929,7 @@ class Appearance:
|
||||
FLAME_DETECTOR = 0x18
|
||||
VEHICLE_TIRE_PRESSURE_SENSOR = 0x19
|
||||
|
||||
class LightFixturesSubcategory(OpenIntEnum):
|
||||
class LightFixturesSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_LIGHT_FIXTURES = 0x00
|
||||
WALL_LIGHT = 0x01
|
||||
CEILING_LIGHT = 0x02
|
||||
@@ -956,7 +957,7 @@ class Appearance:
|
||||
LOW_BAY_LIGHT = 0x18
|
||||
HIGH_BAY_LIGHT = 0x19
|
||||
|
||||
class FanSubcategory(OpenIntEnum):
|
||||
class FanSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_FAN = 0x00
|
||||
CEILING_FAN = 0x01
|
||||
AXIAL_FAN = 0x02
|
||||
@@ -965,7 +966,7 @@ class Appearance:
|
||||
DESK_FAN = 0x05
|
||||
WALL_FAN = 0x06
|
||||
|
||||
class HvacSubcategory(OpenIntEnum):
|
||||
class HvacSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_HVAC = 0x00
|
||||
THERMOSTAT = 0x01
|
||||
HUMIDIFIER = 0x02
|
||||
@@ -979,13 +980,13 @@ class Appearance:
|
||||
FAN_HEATER = 0x0A
|
||||
AIR_CURTAIN = 0x0B
|
||||
|
||||
class AirConditioningSubcategory(OpenIntEnum):
|
||||
class AirConditioningSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_AIR_CONDITIONING = 0x00
|
||||
|
||||
class HumidifierSubcategory(OpenIntEnum):
|
||||
class HumidifierSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_HUMIDIFIER = 0x00
|
||||
|
||||
class HeatingSubcategory(OpenIntEnum):
|
||||
class HeatingSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_HEATING = 0x00
|
||||
RADIATOR = 0x01
|
||||
BOILER = 0x02
|
||||
@@ -995,7 +996,7 @@ class Appearance:
|
||||
FAN_HEATER = 0x06
|
||||
AIR_CURTAIN = 0x07
|
||||
|
||||
class AccessControlSubcategory(OpenIntEnum):
|
||||
class AccessControlSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_ACCESS_CONTROL = 0x00
|
||||
ACCESS_DOOR = 0x01
|
||||
GARAGE_DOOR = 0x02
|
||||
@@ -1007,7 +1008,7 @@ class Appearance:
|
||||
DOOR_LOCK = 0x08
|
||||
LOCKER = 0x09
|
||||
|
||||
class MotorizedDeviceSubcategory(OpenIntEnum):
|
||||
class MotorizedDeviceSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_MOTORIZED_DEVICE = 0x00
|
||||
MOTORIZED_GATE = 0x01
|
||||
AWNING = 0x02
|
||||
@@ -1015,7 +1016,7 @@ class Appearance:
|
||||
CURTAINS = 0x04
|
||||
SCREEN = 0x05
|
||||
|
||||
class PowerDeviceSubcategory(OpenIntEnum):
|
||||
class PowerDeviceSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_POWER_DEVICE = 0x00
|
||||
POWER_OUTLET = 0x01
|
||||
POWER_STRIP = 0x02
|
||||
@@ -1027,7 +1028,7 @@ class Appearance:
|
||||
CHARGE_CASE = 0x08
|
||||
POWER_BANK = 0x09
|
||||
|
||||
class LightSourceSubcategory(OpenIntEnum):
|
||||
class LightSourceSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_LIGHT_SOURCE = 0x00
|
||||
INCANDESCENT_LIGHT_BULB = 0x01
|
||||
LED_LAMP = 0x02
|
||||
@@ -1038,7 +1039,7 @@ class Appearance:
|
||||
LOW_VOLTAGE_HALOGEN = 0x07
|
||||
ORGANIC_LIGHT_EMITTING_DIODE = 0x08
|
||||
|
||||
class WindowCoveringSubcategory(OpenIntEnum):
|
||||
class WindowCoveringSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_WINDOW_COVERING = 0x00
|
||||
WINDOW_SHADES = 0x01
|
||||
WINDOW_BLINDS = 0x02
|
||||
@@ -1047,7 +1048,7 @@ class Appearance:
|
||||
EXTERIOR_SHUTTER = 0x05
|
||||
EXTERIOR_SCREEN = 0x06
|
||||
|
||||
class AudioSinkSubcategory(OpenIntEnum):
|
||||
class AudioSinkSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_AUDIO_SINK = 0x00
|
||||
STANDALONE_SPEAKER = 0x01
|
||||
SOUNDBAR = 0x02
|
||||
@@ -1055,7 +1056,7 @@ class Appearance:
|
||||
STANDMOUNTED_SPEAKER = 0x04
|
||||
SPEAKERPHONE = 0x05
|
||||
|
||||
class AudioSourceSubcategory(OpenIntEnum):
|
||||
class AudioSourceSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_AUDIO_SOURCE = 0x00
|
||||
MICROPHONE = 0x01
|
||||
ALARM = 0x02
|
||||
@@ -1067,7 +1068,7 @@ class Appearance:
|
||||
BROADCASTING_ROOM = 0x08
|
||||
AUDITORIUM = 0x09
|
||||
|
||||
class MotorizedVehicleSubcategory(OpenIntEnum):
|
||||
class MotorizedVehicleSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_MOTORIZED_VEHICLE = 0x00
|
||||
CAR = 0x01
|
||||
LARGE_GOODS_VEHICLE = 0x02
|
||||
@@ -1085,7 +1086,7 @@ class Appearance:
|
||||
CAMPER_CARAVAN = 0x0E
|
||||
RECREATIONAL_VEHICLE_MOTOR_HOME = 0x0F
|
||||
|
||||
class DomesticApplianceSubcategory(OpenIntEnum):
|
||||
class DomesticApplianceSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_DOMESTIC_APPLIANCE = 0x00
|
||||
REFRIGERATOR = 0x01
|
||||
FREEZER = 0x02
|
||||
@@ -1103,21 +1104,21 @@ class Appearance:
|
||||
RICE_COOKER = 0x0E
|
||||
CLOTHES_STEAMER = 0x0F
|
||||
|
||||
class WearableAudioDeviceSubcategory(OpenIntEnum):
|
||||
class WearableAudioDeviceSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_WEARABLE_AUDIO_DEVICE = 0x00
|
||||
EARBUD = 0x01
|
||||
HEADSET = 0x02
|
||||
HEADPHONES = 0x03
|
||||
NECK_BAND = 0x04
|
||||
|
||||
class AircraftSubcategory(OpenIntEnum):
|
||||
class AircraftSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_AIRCRAFT = 0x00
|
||||
LIGHT_AIRCRAFT = 0x01
|
||||
MICROLIGHT = 0x02
|
||||
PARAGLIDER = 0x03
|
||||
LARGE_PASSENGER_AIRCRAFT = 0x04
|
||||
|
||||
class AvEquipmentSubcategory(OpenIntEnum):
|
||||
class AvEquipmentSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_AV_EQUIPMENT = 0x00
|
||||
AMPLIFIER = 0x01
|
||||
RECEIVER = 0x02
|
||||
@@ -1130,65 +1131,65 @@ class Appearance:
|
||||
OPTICAL_DISC_PLAYER = 0x09
|
||||
SET_TOP_BOX = 0x0A
|
||||
|
||||
class DisplayEquipmentSubcategory(OpenIntEnum):
|
||||
class DisplayEquipmentSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_DISPLAY_EQUIPMENT = 0x00
|
||||
TELEVISION = 0x01
|
||||
MONITOR = 0x02
|
||||
PROJECTOR = 0x03
|
||||
|
||||
class HearingAidSubcategory(OpenIntEnum):
|
||||
class HearingAidSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_HEARING_AID = 0x00
|
||||
IN_EAR_HEARING_AID = 0x01
|
||||
BEHIND_EAR_HEARING_AID = 0x02
|
||||
COCHLEAR_IMPLANT = 0x03
|
||||
|
||||
class GamingSubcategory(OpenIntEnum):
|
||||
class GamingSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_GAMING = 0x00
|
||||
HOME_VIDEO_GAME_CONSOLE = 0x01
|
||||
PORTABLE_HANDHELD_CONSOLE = 0x02
|
||||
|
||||
class SignageSubcategory(OpenIntEnum):
|
||||
class SignageSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_SIGNAGE = 0x00
|
||||
DIGITAL_SIGNAGE = 0x01
|
||||
ELECTRONIC_LABEL = 0x02
|
||||
|
||||
class PulseOximeterSubcategory(OpenIntEnum):
|
||||
class PulseOximeterSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_PULSE_OXIMETER = 0x00
|
||||
FINGERTIP_PULSE_OXIMETER = 0x01
|
||||
WRIST_WORN_PULSE_OXIMETER = 0x02
|
||||
|
||||
class WeightScaleSubcategory(OpenIntEnum):
|
||||
class WeightScaleSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_WEIGHT_SCALE = 0x00
|
||||
|
||||
class PersonalMobilityDeviceSubcategory(OpenIntEnum):
|
||||
class PersonalMobilityDeviceSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_PERSONAL_MOBILITY_DEVICE = 0x00
|
||||
POWERED_WHEELCHAIR = 0x01
|
||||
MOBILITY_SCOOTER = 0x02
|
||||
|
||||
class ContinuousGlucoseMonitorSubcategory(OpenIntEnum):
|
||||
class ContinuousGlucoseMonitorSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_CONTINUOUS_GLUCOSE_MONITOR = 0x00
|
||||
|
||||
class InsulinPumpSubcategory(OpenIntEnum):
|
||||
class InsulinPumpSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_INSULIN_PUMP = 0x00
|
||||
INSULIN_PUMP_DURABLE_PUMP = 0x01
|
||||
INSULIN_PUMP_PATCH_PUMP = 0x02
|
||||
INSULIN_PEN = 0x03
|
||||
|
||||
class MedicationDeliverySubcategory(OpenIntEnum):
|
||||
class MedicationDeliverySubcategory(utils.OpenIntEnum):
|
||||
GENERIC_MEDICATION_DELIVERY = 0x00
|
||||
|
||||
class SpirometerSubcategory(OpenIntEnum):
|
||||
class SpirometerSubcategory(utils.OpenIntEnum):
|
||||
GENERIC_SPIROMETER = 0x00
|
||||
HANDHELD_SPIROMETER = 0x01
|
||||
|
||||
class OutdoorSportsActivitySubcategory(OpenIntEnum):
|
||||
class OutdoorSportsActivitySubcategory(utils.OpenIntEnum):
|
||||
GENERIC_OUTDOOR_SPORTS_ACTIVITY = 0x00
|
||||
LOCATION_DISPLAY = 0x01
|
||||
LOCATION_AND_NAVIGATION_DISPLAY = 0x02
|
||||
LOCATION_POD = 0x03
|
||||
LOCATION_AND_NAVIGATION_POD = 0x04
|
||||
|
||||
class _OpenSubcategory(OpenIntEnum):
|
||||
class _OpenSubcategory(utils.OpenIntEnum):
|
||||
GENERIC = 0x00
|
||||
|
||||
SUBCATEGORY_CLASSES = {
|
||||
@@ -1295,7 +1296,7 @@ class AdvertisingData:
|
||||
# fmt: off
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
class Type(OpenIntEnum):
|
||||
class Type(utils.OpenIntEnum):
|
||||
FLAGS = 0x01
|
||||
INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS = 0x02
|
||||
COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS = 0x03
|
||||
|
||||
353
bumble/device.py
353
bumble/device.py
File diff suppressed because it is too large
Load Diff
@@ -25,8 +25,8 @@ import pathlib
|
||||
import platform
|
||||
from typing import Dict, Iterable, Optional, Type, TYPE_CHECKING
|
||||
|
||||
from . import rtk, intel
|
||||
from .common import Driver
|
||||
from bumble.drivers import rtk, intel
|
||||
from bumble.drivers.common import Driver
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.host import Host
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from .gatt import (
|
||||
from bumble.gatt import (
|
||||
Service,
|
||||
Characteristic,
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
|
||||
@@ -286,6 +286,22 @@ GATT_ASHA_AUDIO_STATUS_CHARACTERISTIC = UUID('38663f1a-e711-4cac-b641-32
|
||||
GATT_ASHA_VOLUME_CHARACTERISTIC = UUID('00e4ca9e-ab14-41e4-8823-f9e70c7e91df', 'Volume')
|
||||
GATT_ASHA_LE_PSM_OUT_CHARACTERISTIC = UUID('2d410339-82b6-42aa-b34e-e2e01df8cc1a', 'LE_PSM_OUT')
|
||||
|
||||
# Apple Notification Center Service
|
||||
GATT_ANCS_SERVICE = UUID('7905F431-B5CE-4E99-A40F-4B1E122D00D0', 'Apple Notification Center')
|
||||
GATT_ANCS_NOTIFICATION_SOURCE_CHARACTERISTIC = UUID('9FBF120D-6301-42D9-8C58-25E699A21DBD', 'Notification Source')
|
||||
GATT_ANCS_CONTROL_POINT_CHARACTERISTIC = UUID('69D1D8F3-45E1-49A8-9821-9BBDFDAAD9D9', 'Control Point')
|
||||
GATT_ANCS_DATA_SOURCE_CHARACTERISTIC = UUID('22EAC6E9-24D6-4BB5-BE44-B36ACE7C7BFB', 'Data Source')
|
||||
|
||||
# Apple Media Service
|
||||
GATT_AMS_SERVICE = UUID('89D3502B-0F36-433A-8EF4-C502AD55F8DC', 'Apple Media')
|
||||
GATT_AMS_REMOTE_COMMAND_CHARACTERISTIC = UUID('9B3C81D8-57B1-4A8A-B8DF-0E56F7CA51C2', 'Remote Command')
|
||||
GATT_AMS_ENTITY_UPDATE_CHARACTERISTIC = UUID('2F7CABCE-808D-411F-9A0C-BB92BA96C102', 'Entity Update')
|
||||
GATT_AMS_ENTITY_ATTRIBUTE_CHARACTERISTIC = UUID('C6B2F38C-23AB-46D8-A6AB-A3A870BBD5D7', 'Entity Attribute')
|
||||
|
||||
# Misc Apple Services
|
||||
GATT_APPLE_CONTINUITY_SERVICE = UUID('D0611E78-BBB4-4591-A5F8-487910AE4366', 'Apple Continuity')
|
||||
GATT_APPLE_NEARBY_SERVICE = UUID('9FA480E0-4967-4542-9390-D343DC5D04AE', 'Apple Nearby')
|
||||
|
||||
# Misc
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC = UUID.from_16_bits(0x2A00, 'Device Name')
|
||||
GATT_APPEARANCE_CHARACTERISTIC = UUID.from_16_bits(0x2A01, 'Appearance')
|
||||
|
||||
@@ -35,15 +35,15 @@ from typing import (
|
||||
from bumble.core import InvalidOperationError
|
||||
from bumble.gatt import Characteristic
|
||||
from bumble.gatt_client import CharacteristicProxy
|
||||
from bumble.utils import ByteSerializable, IntConvertible
|
||||
from bumble import utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Typing
|
||||
# -----------------------------------------------------------------------------
|
||||
_T = TypeVar('_T')
|
||||
_T2 = TypeVar('_T2', bound=ByteSerializable)
|
||||
_T3 = TypeVar('_T3', bound=IntConvertible)
|
||||
_T2 = TypeVar('_T2', bound=utils.ByteSerializable)
|
||||
_T3 = TypeVar('_T3', bound=utils.IntConvertible)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -44,11 +44,10 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from pyee import EventEmitter
|
||||
|
||||
from .colors import color
|
||||
from .hci import HCI_Constant
|
||||
from .att import (
|
||||
from bumble.colors import color
|
||||
from bumble.hci import HCI_Constant
|
||||
from bumble.att import (
|
||||
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
ATT_CID,
|
||||
@@ -69,9 +68,10 @@ from .att import (
|
||||
ATT_Write_Request,
|
||||
ATT_Error,
|
||||
)
|
||||
from . import core
|
||||
from .core import UUID, InvalidStateError
|
||||
from .gatt import (
|
||||
from bumble import utils
|
||||
from bumble import core
|
||||
from bumble.core import UUID, InvalidStateError
|
||||
from bumble.gatt import (
|
||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
@@ -117,11 +117,11 @@ def show_services(services: Iterable[ServiceProxy]) -> None:
|
||||
# -----------------------------------------------------------------------------
|
||||
# Proxies
|
||||
# -----------------------------------------------------------------------------
|
||||
class AttributeProxy(EventEmitter, Generic[_T]):
|
||||
class AttributeProxy(utils.EventEmitter, Generic[_T]):
|
||||
def __init__(
|
||||
self, client: Client, handle: int, end_group_handle: int, attribute_type: UUID
|
||||
) -> None:
|
||||
EventEmitter.__init__(self)
|
||||
utils.EventEmitter.__init__(self)
|
||||
self.client = client
|
||||
self.handle = handle
|
||||
self.end_group_handle = end_group_handle
|
||||
@@ -149,7 +149,7 @@ class AttributeProxy(EventEmitter, Generic[_T]):
|
||||
|
||||
class ServiceProxy(AttributeProxy):
|
||||
uuid: UUID
|
||||
characteristics: List[CharacteristicProxy]
|
||||
characteristics: List[CharacteristicProxy[bytes]]
|
||||
included_services: List[ServiceProxy]
|
||||
|
||||
@staticmethod
|
||||
@@ -170,14 +170,20 @@ class ServiceProxy(AttributeProxy):
|
||||
self.uuid = uuid
|
||||
self.characteristics = []
|
||||
|
||||
async def discover_characteristics(self, uuids=()) -> list[CharacteristicProxy]:
|
||||
async def discover_characteristics(
|
||||
self, uuids=()
|
||||
) -> list[CharacteristicProxy[bytes]]:
|
||||
return await self.client.discover_characteristics(uuids, self)
|
||||
|
||||
def get_characteristics_by_uuid(self, uuid: UUID) -> list[CharacteristicProxy]:
|
||||
def get_characteristics_by_uuid(
|
||||
self, uuid: UUID
|
||||
) -> list[CharacteristicProxy[bytes]]:
|
||||
"""Get all the characteristics with a specified UUID."""
|
||||
return self.client.get_characteristics_by_uuid(uuid, self)
|
||||
|
||||
def get_required_characteristic_by_uuid(self, uuid: UUID) -> CharacteristicProxy:
|
||||
def get_required_characteristic_by_uuid(
|
||||
self, uuid: UUID
|
||||
) -> CharacteristicProxy[bytes]:
|
||||
"""
|
||||
Get the first characteristic with a specified UUID.
|
||||
|
||||
@@ -256,7 +262,7 @@ class CharacteristicProxy(AttributeProxy[_T]):
|
||||
)
|
||||
|
||||
|
||||
class DescriptorProxy(AttributeProxy):
|
||||
class DescriptorProxy(AttributeProxy[bytes]):
|
||||
def __init__(self, client: Client, handle: int, descriptor_type: UUID) -> None:
|
||||
super().__init__(client, handle, 0, descriptor_type)
|
||||
|
||||
@@ -376,7 +382,7 @@ class Client:
|
||||
|
||||
def get_characteristics_by_uuid(
|
||||
self, uuid: UUID, service: Optional[ServiceProxy] = None
|
||||
) -> List[CharacteristicProxy]:
|
||||
) -> List[CharacteristicProxy[bytes]]:
|
||||
services = [service] if service else self.services
|
||||
return [
|
||||
c
|
||||
@@ -628,7 +634,7 @@ class Client:
|
||||
|
||||
async def discover_characteristics(
|
||||
self, uuids, service: Optional[ServiceProxy]
|
||||
) -> List[CharacteristicProxy]:
|
||||
) -> List[CharacteristicProxy[bytes]]:
|
||||
'''
|
||||
See Vol 3, Part G - 4.6.1 Discover All Characteristics of a Service and 4.6.2
|
||||
Discover Characteristics by UUID
|
||||
@@ -641,12 +647,12 @@ class Client:
|
||||
services = [service] if service else self.services
|
||||
|
||||
# Perform characteristic discovery for each service
|
||||
discovered_characteristics: List[CharacteristicProxy] = []
|
||||
discovered_characteristics: List[CharacteristicProxy[bytes]] = []
|
||||
for service in services:
|
||||
starting_handle = service.handle
|
||||
ending_handle = service.end_group_handle
|
||||
|
||||
characteristics: List[CharacteristicProxy] = []
|
||||
characteristics: List[CharacteristicProxy[bytes]] = []
|
||||
while starting_handle <= ending_handle:
|
||||
response = await self.send_request(
|
||||
ATT_Read_By_Type_Request(
|
||||
@@ -686,7 +692,7 @@ class Client:
|
||||
|
||||
properties, handle = struct.unpack_from('<BH', attribute_value)
|
||||
characteristic_uuid = UUID.from_bytes(attribute_value[3:])
|
||||
characteristic: CharacteristicProxy = CharacteristicProxy(
|
||||
characteristic = CharacteristicProxy[bytes](
|
||||
self, handle, 0, characteristic_uuid, properties
|
||||
)
|
||||
|
||||
@@ -779,7 +785,7 @@ class Client:
|
||||
|
||||
return descriptors
|
||||
|
||||
async def discover_attributes(self) -> List[AttributeProxy]:
|
||||
async def discover_attributes(self) -> List[AttributeProxy[bytes]]:
|
||||
'''
|
||||
Discover all attributes, regardless of type
|
||||
'''
|
||||
@@ -812,7 +818,7 @@ class Client:
|
||||
logger.warning(f'bogus handle value: {attribute_handle}')
|
||||
return []
|
||||
|
||||
attribute: AttributeProxy = AttributeProxy(
|
||||
attribute = AttributeProxy[bytes](
|
||||
self, attribute_handle, 0, UUID.from_bytes(attribute_uuid)
|
||||
)
|
||||
attributes.append(attribute)
|
||||
|
||||
@@ -38,7 +38,6 @@ from typing import (
|
||||
Type,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
from pyee import EventEmitter
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.core import UUID
|
||||
@@ -83,7 +82,7 @@ from bumble.gatt import (
|
||||
Descriptor,
|
||||
Service,
|
||||
)
|
||||
from bumble.utils import AsyncRunner
|
||||
from bumble import utils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Device, Connection
|
||||
@@ -103,7 +102,7 @@ GATT_SERVER_DEFAULT_MAX_MTU = 517
|
||||
# -----------------------------------------------------------------------------
|
||||
# GATT Server
|
||||
# -----------------------------------------------------------------------------
|
||||
class Server(EventEmitter):
|
||||
class Server(utils.EventEmitter):
|
||||
attributes: List[Attribute]
|
||||
services: List[Service]
|
||||
attributes_by_handle: Dict[int, Attribute]
|
||||
@@ -662,7 +661,7 @@ class Server(EventEmitter):
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_find_by_type_value_request(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.3.3 Find By Type Value Request
|
||||
@@ -715,7 +714,7 @@ class Server(EventEmitter):
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_read_by_type_request(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.1 Read By Type Request
|
||||
@@ -781,7 +780,7 @@ class Server(EventEmitter):
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_read_request(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.3 Read Request
|
||||
@@ -807,7 +806,7 @@ class Server(EventEmitter):
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_read_blob_request(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.5 Read Blob Request
|
||||
@@ -852,7 +851,7 @@ class Server(EventEmitter):
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_read_by_group_type_request(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.9 Read by Group Type Request
|
||||
@@ -920,7 +919,7 @@ class Server(EventEmitter):
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_write_request(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.5.1 Write Request
|
||||
@@ -967,7 +966,7 @@ class Server(EventEmitter):
|
||||
response = ATT_Write_Response()
|
||||
self.send_response(connection, response)
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_write_command(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.5.3 Write Command
|
||||
|
||||
199
bumble/hci.py
199
bumble/hci.py
@@ -24,21 +24,23 @@ import logging
|
||||
import secrets
|
||||
import struct
|
||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Type, Union, ClassVar
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import crypto
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
PhysicalTransport,
|
||||
AdvertisingData,
|
||||
DeviceClass,
|
||||
InvalidArgumentError,
|
||||
InvalidPacketError,
|
||||
ProtocolError,
|
||||
PhysicalTransport,
|
||||
bit_flags_to_strings,
|
||||
name_or_number,
|
||||
padded_bytes,
|
||||
)
|
||||
from bumble.utils import OpenIntEnum
|
||||
from bumble import utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -94,7 +96,7 @@ def map_class_of_device(class_of_device):
|
||||
)
|
||||
|
||||
|
||||
def phy_list_to_bits(phys: Optional[Iterable[int]]) -> int:
|
||||
def phy_list_to_bits(phys: Optional[Iterable[Phy]]) -> int:
|
||||
if phys is None:
|
||||
return 0
|
||||
|
||||
@@ -700,30 +702,22 @@ HCI_ERROR_NAMES[HCI_SUCCESS] = 'HCI_SUCCESS'
|
||||
HCI_COMMAND_STATUS_PENDING = 0
|
||||
|
||||
|
||||
class Phy(enum.IntEnum):
|
||||
LE_1M = 1
|
||||
LE_2M = 2
|
||||
LE_CODED = 3
|
||||
|
||||
|
||||
# ACL
|
||||
HCI_ACL_PB_FIRST_NON_FLUSHABLE = 0
|
||||
HCI_ACL_PB_CONTINUATION = 1
|
||||
HCI_ACL_PB_FIRST_FLUSHABLE = 2
|
||||
HCI_ACK_PB_COMPLETE_L2CAP = 3
|
||||
|
||||
# Roles
|
||||
HCI_CENTRAL_ROLE = 0
|
||||
HCI_PERIPHERAL_ROLE = 1
|
||||
|
||||
HCI_ROLE_NAMES = {
|
||||
HCI_CENTRAL_ROLE: 'CENTRAL',
|
||||
HCI_PERIPHERAL_ROLE: 'PERIPHERAL'
|
||||
}
|
||||
|
||||
# LE PHY Types
|
||||
HCI_LE_1M_PHY = 1
|
||||
HCI_LE_2M_PHY = 2
|
||||
HCI_LE_CODED_PHY = 3
|
||||
|
||||
HCI_LE_PHY_NAMES = {
|
||||
HCI_LE_1M_PHY: 'LE 1M',
|
||||
HCI_LE_2M_PHY: 'LE 2M',
|
||||
HCI_LE_CODED_PHY: 'LE Coded'
|
||||
HCI_LE_PHY_NAMES: dict[int,str] = {
|
||||
Phy.LE_1M: 'LE 1M',
|
||||
Phy.LE_2M: 'LE 2M',
|
||||
Phy.LE_CODED: 'LE Coded'
|
||||
}
|
||||
|
||||
HCI_LE_1M_PHY_BIT = 0
|
||||
@@ -732,26 +726,20 @@ HCI_LE_CODED_PHY_BIT = 2
|
||||
|
||||
HCI_LE_PHY_BIT_NAMES = ['LE_1M_PHY', 'LE_2M_PHY', 'LE_CODED_PHY']
|
||||
|
||||
HCI_LE_PHY_TYPE_TO_BIT = {
|
||||
HCI_LE_1M_PHY: HCI_LE_1M_PHY_BIT,
|
||||
HCI_LE_2M_PHY: HCI_LE_2M_PHY_BIT,
|
||||
HCI_LE_CODED_PHY: HCI_LE_CODED_PHY_BIT
|
||||
HCI_LE_PHY_TYPE_TO_BIT: dict[Phy, int] = {
|
||||
Phy.LE_1M: HCI_LE_1M_PHY_BIT,
|
||||
Phy.LE_2M: HCI_LE_2M_PHY_BIT,
|
||||
Phy.LE_CODED: HCI_LE_CODED_PHY_BIT,
|
||||
}
|
||||
|
||||
|
||||
class Phy(enum.IntEnum):
|
||||
LE_1M = HCI_LE_1M_PHY
|
||||
LE_2M = HCI_LE_2M_PHY
|
||||
LE_CODED = HCI_LE_CODED_PHY
|
||||
|
||||
|
||||
class PhyBit(enum.IntFlag):
|
||||
LE_1M = 1 << HCI_LE_1M_PHY_BIT
|
||||
LE_2M = 1 << HCI_LE_2M_PHY_BIT
|
||||
LE_CODED = 1 << HCI_LE_CODED_PHY_BIT
|
||||
|
||||
|
||||
class CsRole(OpenIntEnum):
|
||||
class CsRole(utils.OpenIntEnum):
|
||||
INITIATOR = 0x00
|
||||
REFLECTOR = 0x01
|
||||
|
||||
@@ -761,7 +749,7 @@ class CsRoleMask(enum.IntFlag):
|
||||
REFLECTOR = 0x02
|
||||
|
||||
|
||||
class CsSyncPhy(OpenIntEnum):
|
||||
class CsSyncPhy(utils.OpenIntEnum):
|
||||
LE_1M = 1
|
||||
LE_2M = 2
|
||||
LE_2M_2BT = 3
|
||||
@@ -772,7 +760,7 @@ class CsSyncPhySupported(enum.IntFlag):
|
||||
LE_2M_2BT = 0x02
|
||||
|
||||
|
||||
class RttType(OpenIntEnum):
|
||||
class RttType(utils.OpenIntEnum):
|
||||
AA_ONLY = 0x00
|
||||
SOUNDING_SEQUENCE_32_BIT = 0x01
|
||||
SOUNDING_SEQUENCE_96_BIT = 0x02
|
||||
@@ -782,7 +770,7 @@ class RttType(OpenIntEnum):
|
||||
RANDOM_SEQUENCE_128_BIT = 0x06
|
||||
|
||||
|
||||
class CsSnr(OpenIntEnum):
|
||||
class CsSnr(utils.OpenIntEnum):
|
||||
SNR_18_DB = 0x00
|
||||
SNR_21_DB = 0x01
|
||||
SNR_24_DB = 0x02
|
||||
@@ -791,26 +779,39 @@ class CsSnr(OpenIntEnum):
|
||||
NOT_APPLIED = 0xFF
|
||||
|
||||
|
||||
class CsDoneStatus(OpenIntEnum):
|
||||
class CsDoneStatus(utils.OpenIntEnum):
|
||||
ALL_RESULTS_COMPLETED = 0x00
|
||||
PARTIAL = 0x01
|
||||
ABORTED = 0x0F
|
||||
|
||||
|
||||
class CsProcedureAbortReason(OpenIntEnum):
|
||||
class CsProcedureAbortReason(utils.OpenIntEnum):
|
||||
NO_ABORT = 0x00
|
||||
LOCAL_HOST_OR_REMOTE_REQUEST = 0x01
|
||||
CHANNEL_MAP_UPDATE_INSTANT_PASSED = 0x02
|
||||
UNSPECIFIED = 0x0F
|
||||
|
||||
|
||||
class CsSubeventAbortReason(OpenIntEnum):
|
||||
class CsSubeventAbortReason(utils.OpenIntEnum):
|
||||
NO_ABORT = 0x00
|
||||
LOCAL_HOST_OR_REMOTE_REQUEST = 0x01
|
||||
NO_CS_SYNC_RECEIVED = 0x02
|
||||
SCHEDULING_CONFLICT_OR_LIMITED_RESOURCES = 0x03
|
||||
UNSPECIFIED = 0x0F
|
||||
|
||||
class Role(enum.IntEnum):
|
||||
CENTRAL = 0
|
||||
PERIPHERAL = 1
|
||||
|
||||
# For Backward Compatibility.
|
||||
HCI_CENTRAL_ROLE = Role.CENTRAL
|
||||
HCI_PERIPHERAL_ROLE = Role.PERIPHERAL
|
||||
|
||||
|
||||
HCI_LE_1M_PHY = Phy.LE_1M
|
||||
HCI_LE_2M_PHY = Phy.LE_2M
|
||||
HCI_LE_CODED_PHY = Phy.LE_CODED
|
||||
|
||||
|
||||
# Connection Parameters
|
||||
HCI_CONNECTION_INTERVAL_MS_PER_UNIT = 1.25
|
||||
@@ -889,10 +890,15 @@ HCI_LINK_TYPE_NAMES = {
|
||||
}
|
||||
|
||||
# Address types
|
||||
HCI_PUBLIC_DEVICE_ADDRESS_TYPE = 0x00
|
||||
HCI_RANDOM_DEVICE_ADDRESS_TYPE = 0x01
|
||||
HCI_PUBLIC_IDENTITY_ADDRESS_TYPE = 0x02
|
||||
HCI_RANDOM_IDENTITY_ADDRESS_TYPE = 0x03
|
||||
class AddressType(utils.OpenIntEnum):
|
||||
PUBLIC_DEVICE = 0x00
|
||||
RANDOM_DEVICE = 0x01
|
||||
PUBLIC_IDENTITY = 0x02
|
||||
RANDOM_IDENTITY = 0x03
|
||||
# (Directed Only) Address is RPA, but controller cannot resolve.
|
||||
UNABLE_TO_RESOLVE = 0xFE
|
||||
# (Extended Only) No address.
|
||||
ANONYMOUS = 0xFF
|
||||
|
||||
# Supported Commands Masks
|
||||
# See Bluetooth spec @ 6.27 SUPPORTED COMMANDS
|
||||
@@ -1233,7 +1239,7 @@ HCI_SUPPORTED_COMMANDS_MASKS = {
|
||||
|
||||
# LE Supported Features
|
||||
# See Bluetooth spec @ Vol 6, Part B, 4.6 FEATURE SUPPORT
|
||||
class LeFeature(OpenIntEnum):
|
||||
class LeFeature(utils.OpenIntEnum):
|
||||
LE_ENCRYPTION = 0
|
||||
CONNECTION_PARAMETERS_REQUEST_PROCEDURE = 1
|
||||
EXTENDED_REJECT_INDICATION = 2
|
||||
@@ -1531,7 +1537,7 @@ RTT_TYPE_SPEC = {'size': 1, 'mapper': lambda x: RttType(x).name}
|
||||
CS_SNR_SPEC = {'size': 1, 'mapper': lambda x: CsSnr(x).name}
|
||||
|
||||
|
||||
class CodecID(OpenIntEnum):
|
||||
class CodecID(utils.OpenIntEnum):
|
||||
# fmt: off
|
||||
U_LOG = 0x00
|
||||
A_LOG = 0x01
|
||||
@@ -1582,8 +1588,8 @@ class HCI_Constant:
|
||||
return HCI_ERROR_NAMES.get(status, f'0x{status:02X}')
|
||||
|
||||
@staticmethod
|
||||
def role_name(role):
|
||||
return HCI_ROLE_NAMES.get(role, str(role))
|
||||
def role_name(role: int) -> str:
|
||||
return Role(role).name
|
||||
|
||||
@staticmethod
|
||||
def le_phy_name(phy):
|
||||
@@ -1949,17 +1955,10 @@ class Address:
|
||||
address[0] is the LSB of the address, address[5] is the MSB.
|
||||
'''
|
||||
|
||||
PUBLIC_DEVICE_ADDRESS = 0x00
|
||||
RANDOM_DEVICE_ADDRESS = 0x01
|
||||
PUBLIC_IDENTITY_ADDRESS = 0x02
|
||||
RANDOM_IDENTITY_ADDRESS = 0x03
|
||||
|
||||
ADDRESS_TYPE_NAMES = {
|
||||
PUBLIC_DEVICE_ADDRESS: 'PUBLIC_DEVICE_ADDRESS',
|
||||
RANDOM_DEVICE_ADDRESS: 'RANDOM_DEVICE_ADDRESS',
|
||||
PUBLIC_IDENTITY_ADDRESS: 'PUBLIC_IDENTITY_ADDRESS',
|
||||
RANDOM_IDENTITY_ADDRESS: 'RANDOM_IDENTITY_ADDRESS',
|
||||
}
|
||||
PUBLIC_DEVICE_ADDRESS = AddressType.PUBLIC_DEVICE
|
||||
RANDOM_DEVICE_ADDRESS = AddressType.RANDOM_DEVICE
|
||||
PUBLIC_IDENTITY_ADDRESS = AddressType.PUBLIC_IDENTITY
|
||||
RANDOM_IDENTITY_ADDRESS = AddressType.RANDOM_IDENTITY
|
||||
|
||||
# Type declarations
|
||||
NIL: Address
|
||||
@@ -1969,40 +1968,44 @@ class Address:
|
||||
# pylint: disable-next=unnecessary-lambda
|
||||
ADDRESS_TYPE_SPEC = {'size': 1, 'mapper': lambda x: Address.address_type_name(x)}
|
||||
|
||||
@staticmethod
|
||||
def address_type_name(address_type):
|
||||
return name_or_number(Address.ADDRESS_TYPE_NAMES, address_type)
|
||||
@classmethod
|
||||
def address_type_name(cls: type[Self], address_type: int) -> str:
|
||||
return AddressType(address_type).name
|
||||
|
||||
@staticmethod
|
||||
def from_string_for_transport(string, transport):
|
||||
if transport == BT_BR_EDR_TRANSPORT:
|
||||
@classmethod
|
||||
def from_string_for_transport(
|
||||
cls: type[Self], string: str, transport: PhysicalTransport
|
||||
) -> Self:
|
||||
if transport == PhysicalTransport.BR_EDR:
|
||||
address_type = Address.PUBLIC_DEVICE_ADDRESS
|
||||
else:
|
||||
address_type = Address.RANDOM_DEVICE_ADDRESS
|
||||
return Address(string, address_type)
|
||||
return cls(string, address_type)
|
||||
|
||||
@staticmethod
|
||||
def parse_address(data, offset):
|
||||
@classmethod
|
||||
def parse_address(cls: type[Self], data: bytes, offset: int) -> tuple[int, Self]:
|
||||
# Fix the type to a default value. This is used for parsing type-less Classic
|
||||
# addresses
|
||||
return Address.parse_address_with_type(
|
||||
data, offset, Address.PUBLIC_DEVICE_ADDRESS
|
||||
)
|
||||
return cls.parse_address_with_type(data, offset, Address.PUBLIC_DEVICE_ADDRESS)
|
||||
|
||||
@staticmethod
|
||||
def parse_random_address(data, offset):
|
||||
return Address.parse_address_with_type(
|
||||
data, offset, Address.RANDOM_DEVICE_ADDRESS
|
||||
)
|
||||
@classmethod
|
||||
def parse_random_address(
|
||||
cls: type[Self], data: bytes, offset: int
|
||||
) -> tuple[int, Self]:
|
||||
return cls.parse_address_with_type(data, offset, Address.RANDOM_DEVICE_ADDRESS)
|
||||
|
||||
@staticmethod
|
||||
def parse_address_with_type(data, offset, address_type):
|
||||
return offset + 6, Address(data[offset : offset + 6], address_type)
|
||||
@classmethod
|
||||
def parse_address_with_type(
|
||||
cls: type[Self], data: bytes, offset: int, address_type: AddressType
|
||||
) -> tuple[int, Self]:
|
||||
return offset + 6, cls(data[offset : offset + 6], address_type)
|
||||
|
||||
@staticmethod
|
||||
def parse_address_preceded_by_type(data, offset):
|
||||
address_type = data[offset - 1]
|
||||
return Address.parse_address_with_type(data, offset, address_type)
|
||||
@classmethod
|
||||
def parse_address_preceded_by_type(
|
||||
cls: type[Self], data: bytes, offset: int
|
||||
) -> tuple[int, Self]:
|
||||
address_type = AddressType(data[offset - 1])
|
||||
return cls.parse_address_with_type(data, offset, address_type)
|
||||
|
||||
@classmethod
|
||||
def generate_static_address(cls) -> Address:
|
||||
@@ -2042,8 +2045,10 @@ class Address:
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, address: Union[bytes, str], address_type: int = RANDOM_DEVICE_ADDRESS
|
||||
):
|
||||
self,
|
||||
address: Union[bytes, str],
|
||||
address_type: AddressType = RANDOM_DEVICE_ADDRESS,
|
||||
) -> None:
|
||||
'''
|
||||
Initialize an instance. `address` may be a byte array in little-endian
|
||||
format, or a hex string in big-endian format (with optional ':'
|
||||
@@ -4878,6 +4883,20 @@ class HCI_LE_Periodic_Advertising_Sync_Transfer_Command(HCI_Command):
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
fields=[('connection_handle', 2), ('service_data', 2), ('advertising_handle', 1)],
|
||||
return_parameters_fields=[
|
||||
('status', STATUS_SPEC),
|
||||
('connection_handle', 2),
|
||||
],
|
||||
)
|
||||
class HCI_LE_Periodic_Advertising_Set_Info_Transfer_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.8.90 LE Periodic Advertising Set Info Transfer Command
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
fields=[
|
||||
@@ -5351,11 +5370,11 @@ class HCI_LE_CS_Create_Config_Command(HCI_Command):
|
||||
See Bluetooth spec @ 7.8.137 LE CS Create Config command
|
||||
'''
|
||||
|
||||
class ChannelSelectionType(OpenIntEnum):
|
||||
class ChannelSelectionType(utils.OpenIntEnum):
|
||||
ALGO_3B = 0
|
||||
ALGO_3C = 1
|
||||
|
||||
class Ch3cShape(OpenIntEnum):
|
||||
class Ch3cShape(utils.OpenIntEnum):
|
||||
HAT = 0x00
|
||||
X = 0x01
|
||||
|
||||
@@ -6184,13 +6203,13 @@ class HCI_LE_Periodic_Advertising_Report_Event(HCI_LE_Meta_Event):
|
||||
TX_POWER_INFORMATION_NOT_AVAILABLE = 0x7F
|
||||
RSSI_NOT_AVAILABLE = 0x7F
|
||||
|
||||
class CteType(OpenIntEnum):
|
||||
class CteType(utils.OpenIntEnum):
|
||||
AOA_CONSTANT_TONE_EXTENSION = 0x00
|
||||
AOD_CONSTANT_TONE_EXTENSION_1US = 0x01
|
||||
AOD_CONSTANT_TONE_EXTENSION_2US = 0x02
|
||||
NO_CONSTANT_TONE_EXTENSION = 0xFF
|
||||
|
||||
class DataStatus(OpenIntEnum):
|
||||
class DataStatus(utils.OpenIntEnum):
|
||||
DATA_COMPLETE = 0x00
|
||||
DATA_INCOMPLETE_MORE_TO_COME = 0x01
|
||||
DATA_INCOMPLETE_TRUNCATED_NO_MORE_TO_COME = 0x02
|
||||
@@ -6571,7 +6590,7 @@ class HCI_LE_CS_Config_Complete_Event(HCI_LE_Meta_Event):
|
||||
See Bluetooth spec @ 7.7.65.42 LE CS Config Complete event
|
||||
'''
|
||||
|
||||
class Action(OpenIntEnum):
|
||||
class Action(utils.OpenIntEnum):
|
||||
REMOVED = 0
|
||||
CREATED = 1
|
||||
|
||||
@@ -6623,7 +6642,7 @@ class HCI_LE_CS_Procedure_Enable_Complete_Event(HCI_LE_Meta_Event):
|
||||
See Bluetooth spec @ 7.7.65.43 LE CS Procedure Enable Complete event
|
||||
'''
|
||||
|
||||
class State(OpenIntEnum):
|
||||
class State(utils.OpenIntEnum):
|
||||
DISABLED = 0
|
||||
ENABLED = 1
|
||||
|
||||
@@ -6965,7 +6984,7 @@ class HCI_QOS_Setup_Complete_Event(HCI_Event):
|
||||
See Bluetooth spec @ 7.7.13 QoS Setup Complete Event
|
||||
'''
|
||||
|
||||
class ServiceType(OpenIntEnum):
|
||||
class ServiceType(utils.OpenIntEnum):
|
||||
NO_TRAFFIC_AVAILABLE = 0x00
|
||||
BEST_EFFORT_AVAILABLE = 0x01
|
||||
GUARANTEED_AVAILABLE = 0x02
|
||||
|
||||
@@ -24,7 +24,6 @@ import asyncio
|
||||
import dataclasses
|
||||
import enum
|
||||
import traceback
|
||||
import pyee
|
||||
import re
|
||||
from typing import (
|
||||
Dict,
|
||||
@@ -45,6 +44,7 @@ from bumble import at
|
||||
from bumble import device
|
||||
from bumble import rfcomm
|
||||
from bumble import sdp
|
||||
from bumble import utils
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
ProtocolError,
|
||||
@@ -690,7 +690,7 @@ class HfIndicatorState:
|
||||
current_status: int = 0
|
||||
|
||||
|
||||
class HfProtocol(pyee.EventEmitter):
|
||||
class HfProtocol(utils.EventEmitter):
|
||||
"""
|
||||
Implementation for the Hands-Free side of the Hands-Free profile.
|
||||
|
||||
@@ -1146,7 +1146,7 @@ class HfProtocol(pyee.EventEmitter):
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
class AgProtocol(pyee.EventEmitter):
|
||||
class AgProtocol(utils.EventEmitter):
|
||||
"""
|
||||
Implementation for the Audio-Gateway side of the Hands-Free profile.
|
||||
|
||||
|
||||
@@ -22,11 +22,12 @@ import enum
|
||||
import struct
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from pyee import EventEmitter
|
||||
from typing import Optional, Callable
|
||||
from typing_extensions import override
|
||||
|
||||
from bumble import l2cap, device
|
||||
from bumble import l2cap
|
||||
from bumble import device
|
||||
from bumble import utils
|
||||
from bumble.core import InvalidStateError, ProtocolError
|
||||
from bumble.hci import Address
|
||||
|
||||
@@ -195,7 +196,7 @@ class SendHandshakeMessage(Message):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class HID(ABC, EventEmitter):
|
||||
class HID(ABC, utils.EventEmitter):
|
||||
l2cap_ctrl_channel: Optional[l2cap.ClassicChannel] = None
|
||||
l2cap_intr_channel: Optional[l2cap.ClassicChannel] = None
|
||||
connection: Optional[device.Connection] = None
|
||||
|
||||
@@ -34,7 +34,6 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
import pyee
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.l2cap import L2CAP_PDU
|
||||
@@ -42,16 +41,16 @@ from bumble.snoop import Snooper
|
||||
from bumble import drivers
|
||||
from bumble import hci
|
||||
from bumble.core import (
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
BT_LE_TRANSPORT,
|
||||
PhysicalTransport,
|
||||
PhysicalTransport,
|
||||
ConnectionPHY,
|
||||
ConnectionParameters,
|
||||
)
|
||||
from bumble.utils import AbortableEventEmitter
|
||||
from bumble import utils
|
||||
from bumble.transport.common import TransportLostError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .transport.common import TransportSink, TransportSource
|
||||
from bumble.transport.common import TransportSink, TransportSource
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -61,7 +60,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class DataPacketQueue(pyee.EventEmitter):
|
||||
class DataPacketQueue(utils.EventEmitter):
|
||||
"""
|
||||
Flow-control queue for host->controller data packets (ACL, ISO).
|
||||
|
||||
@@ -186,7 +185,11 @@ class DataPacketQueue(pyee.EventEmitter):
|
||||
# -----------------------------------------------------------------------------
|
||||
class Connection:
|
||||
def __init__(
|
||||
self, host: Host, handle: int, peer_address: hci.Address, transport: int
|
||||
self,
|
||||
host: Host,
|
||||
handle: int,
|
||||
peer_address: hci.Address,
|
||||
transport: PhysicalTransport,
|
||||
):
|
||||
self.host = host
|
||||
self.handle = handle
|
||||
@@ -195,7 +198,7 @@ class Connection:
|
||||
self.transport = transport
|
||||
acl_packet_queue: Optional[DataPacketQueue] = (
|
||||
host.le_acl_packet_queue
|
||||
if transport == BT_LE_TRANSPORT
|
||||
if transport == PhysicalTransport.LE
|
||||
else host.acl_packet_queue
|
||||
)
|
||||
assert acl_packet_queue
|
||||
@@ -230,7 +233,7 @@ class IsoLink:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Host(AbortableEventEmitter):
|
||||
class Host(utils.EventEmitter):
|
||||
connections: Dict[int, Connection]
|
||||
cis_links: Dict[int, IsoLink]
|
||||
bis_links: Dict[int, IsoLink]
|
||||
@@ -962,7 +965,7 @@ class Host(AbortableEventEmitter):
|
||||
self,
|
||||
event.connection_handle,
|
||||
event.peer_address,
|
||||
BT_LE_TRANSPORT,
|
||||
PhysicalTransport.LE,
|
||||
)
|
||||
self.connections[event.connection_handle] = connection
|
||||
|
||||
@@ -975,11 +978,11 @@ class Host(AbortableEventEmitter):
|
||||
self.emit(
|
||||
'connection',
|
||||
event.connection_handle,
|
||||
BT_LE_TRANSPORT,
|
||||
PhysicalTransport.LE,
|
||||
event.peer_address,
|
||||
getattr(event, 'local_resolvable_private_address', None),
|
||||
getattr(event, 'peer_resolvable_private_address', None),
|
||||
event.role,
|
||||
hci.Role(event.role),
|
||||
connection_parameters,
|
||||
)
|
||||
else:
|
||||
@@ -987,7 +990,10 @@ class Host(AbortableEventEmitter):
|
||||
|
||||
# Notify the listeners
|
||||
self.emit(
|
||||
'connection_failure', BT_LE_TRANSPORT, event.peer_address, event.status
|
||||
'connection_failure',
|
||||
PhysicalTransport.LE,
|
||||
event.peer_address,
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_le_enhanced_connection_complete_event(self, event):
|
||||
@@ -1012,7 +1018,7 @@ class Host(AbortableEventEmitter):
|
||||
self,
|
||||
event.connection_handle,
|
||||
event.bd_addr,
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
PhysicalTransport.BR_EDR,
|
||||
)
|
||||
self.connections[event.connection_handle] = connection
|
||||
|
||||
@@ -1020,7 +1026,7 @@ class Host(AbortableEventEmitter):
|
||||
self.emit(
|
||||
'connection',
|
||||
event.connection_handle,
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
PhysicalTransport.BR_EDR,
|
||||
event.bd_addr,
|
||||
None,
|
||||
None,
|
||||
@@ -1032,7 +1038,10 @@ class Host(AbortableEventEmitter):
|
||||
|
||||
# Notify the client
|
||||
self.emit(
|
||||
'connection_failure', BT_BR_EDR_TRANSPORT, event.bd_addr, event.status
|
||||
'connection_failure',
|
||||
PhysicalTransport.BR_EDR,
|
||||
event.bd_addr,
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_disconnection_complete_event(self, event):
|
||||
@@ -1279,7 +1288,8 @@ class Host(AbortableEventEmitter):
|
||||
logger.debug('no long term key provider')
|
||||
long_term_key = None
|
||||
else:
|
||||
long_term_key = await self.abort_on(
|
||||
long_term_key = await utils.cancel_on_event(
|
||||
self,
|
||||
'flush',
|
||||
# pylint: disable-next=not-callable
|
||||
self.long_term_key_provider(
|
||||
@@ -1337,7 +1347,7 @@ class Host(AbortableEventEmitter):
|
||||
f'role change for {event.bd_addr}: '
|
||||
f'{hci.HCI_Constant.role_name(event.new_role)}'
|
||||
)
|
||||
self.emit('role_change', event.bd_addr, event.new_role)
|
||||
self.emit('role_change', event.bd_addr, hci.Role(event.new_role))
|
||||
else:
|
||||
logger.debug(
|
||||
f'role change for {event.bd_addr} failed: '
|
||||
@@ -1437,7 +1447,8 @@ class Host(AbortableEventEmitter):
|
||||
logger.debug('no link key provider')
|
||||
link_key = None
|
||||
else:
|
||||
link_key = await self.abort_on(
|
||||
link_key = await utils.cancel_on_event(
|
||||
self,
|
||||
'flush',
|
||||
# pylint: disable-next=not-callable
|
||||
self.link_key_provider(event.bd_addr),
|
||||
|
||||
@@ -28,11 +28,11 @@ import json
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type
|
||||
from typing_extensions import Self
|
||||
|
||||
from .colors import color
|
||||
from .hci import Address
|
||||
from bumble.colors import color
|
||||
from bumble.hci import Address
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .device import Device
|
||||
from bumble.device import Device
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -23,7 +23,6 @@ import logging
|
||||
import struct
|
||||
|
||||
from collections import deque
|
||||
from pyee import EventEmitter
|
||||
from typing import (
|
||||
Dict,
|
||||
Type,
|
||||
@@ -39,19 +38,19 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from .utils import deprecated
|
||||
from .colors import color
|
||||
from .core import (
|
||||
BT_CENTRAL_ROLE,
|
||||
from bumble import utils
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
InvalidStateError,
|
||||
InvalidArgumentError,
|
||||
InvalidPacketError,
|
||||
OutOfResourcesError,
|
||||
ProtocolError,
|
||||
)
|
||||
from .hci import (
|
||||
from bumble.hci import (
|
||||
HCI_LE_Connection_Update_Command,
|
||||
HCI_Object,
|
||||
Role,
|
||||
key_with_value,
|
||||
name_or_number,
|
||||
)
|
||||
@@ -720,7 +719,7 @@ class L2CAP_LE_Flow_Control_Credit(L2CAP_Control_Frame):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class ClassicChannel(EventEmitter):
|
||||
class ClassicChannel(utils.EventEmitter):
|
||||
class State(enum.IntEnum):
|
||||
# States
|
||||
CLOSED = 0x00
|
||||
@@ -821,8 +820,8 @@ class ClassicChannel(EventEmitter):
|
||||
|
||||
# Wait for the connection to succeed or fail
|
||||
try:
|
||||
return await self.connection.abort_on(
|
||||
'disconnection', self.connection_result
|
||||
return await utils.cancel_on_event(
|
||||
self.connection, 'disconnection', self.connection_result
|
||||
)
|
||||
finally:
|
||||
self.connection_result = None
|
||||
@@ -1026,7 +1025,7 @@ class ClassicChannel(EventEmitter):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class LeCreditBasedChannel(EventEmitter):
|
||||
class LeCreditBasedChannel(utils.EventEmitter):
|
||||
"""
|
||||
LE Credit-based Connection Oriented Channel
|
||||
"""
|
||||
@@ -1381,7 +1380,7 @@ class LeCreditBasedChannel(EventEmitter):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class ClassicChannelServer(EventEmitter):
|
||||
class ClassicChannelServer(utils.EventEmitter):
|
||||
def __init__(
|
||||
self,
|
||||
manager: ChannelManager,
|
||||
@@ -1406,7 +1405,7 @@ class ClassicChannelServer(EventEmitter):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class LeCreditBasedChannelServer(EventEmitter):
|
||||
class LeCreditBasedChannelServer(utils.EventEmitter):
|
||||
def __init__(
|
||||
self,
|
||||
manager: ChannelManager,
|
||||
@@ -1521,6 +1520,9 @@ class ChannelManager:
|
||||
|
||||
def next_identifier(self, connection: Connection) -> int:
|
||||
identifier = (self.identifiers.setdefault(connection.handle, 0) + 1) % 256
|
||||
# 0x00 is an invalid ID (BT Core Spec, Vol 3, Part A, Sect 4
|
||||
if identifier == 0:
|
||||
identifier = 1
|
||||
self.identifiers[connection.handle] = identifier
|
||||
return identifier
|
||||
|
||||
@@ -1533,7 +1535,7 @@ class ChannelManager:
|
||||
if cid in self.fixed_channels:
|
||||
del self.fixed_channels[cid]
|
||||
|
||||
@deprecated("Please use create_classic_server")
|
||||
@utils.deprecated("Please use create_classic_server")
|
||||
def register_server(
|
||||
self,
|
||||
psm: int,
|
||||
@@ -1579,7 +1581,7 @@ class ChannelManager:
|
||||
|
||||
return self.servers[spec.psm]
|
||||
|
||||
@deprecated("Please use create_le_credit_based_server()")
|
||||
@utils.deprecated("Please use create_le_credit_based_server()")
|
||||
def register_le_coc_server(
|
||||
self,
|
||||
psm: int,
|
||||
@@ -1908,7 +1910,7 @@ class ChannelManager:
|
||||
def on_l2cap_connection_parameter_update_request(
|
||||
self, connection: Connection, cid: int, request
|
||||
):
|
||||
if connection.role == BT_CENTRAL_ROLE:
|
||||
if connection.role == Role.CENTRAL:
|
||||
self.send_control_frame(
|
||||
connection,
|
||||
cid,
|
||||
@@ -2123,7 +2125,7 @@ class ChannelManager:
|
||||
if channel.source_cid in connection_channels:
|
||||
del connection_channels[channel.source_cid]
|
||||
|
||||
@deprecated("Please use create_le_credit_based_channel()")
|
||||
@utils.deprecated("Please use create_le_credit_based_channel()")
|
||||
async def open_le_coc(
|
||||
self, connection: Connection, psm: int, max_credits: int, mtu: int, mps: int
|
||||
) -> LeCreditBasedChannel:
|
||||
@@ -2180,7 +2182,7 @@ class ChannelManager:
|
||||
|
||||
return channel
|
||||
|
||||
@deprecated("Please use create_classic_channel()")
|
||||
@utils.deprecated("Please use create_classic_channel()")
|
||||
async def connect(self, connection: Connection, psm: int) -> ClassicChannel:
|
||||
return await self.create_classic_channel(
|
||||
connection=connection, spec=ClassicChannelSpec(psm=psm)
|
||||
@@ -2230,12 +2232,12 @@ class ChannelManager:
|
||||
|
||||
|
||||
class Channel(ClassicChannel):
|
||||
@deprecated("Please use ClassicChannel")
|
||||
@utils.deprecated("Please use ClassicChannel")
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class LeConnectionOrientedChannel(LeCreditBasedChannel):
|
||||
@deprecated("Please use LeCreditBasedChannel")
|
||||
@utils.deprecated("Please use LeCreditBasedChannel")
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -20,14 +20,13 @@ import asyncio
|
||||
from functools import partial
|
||||
|
||||
from bumble.core import (
|
||||
BT_PERIPHERAL_ROLE,
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
BT_LE_TRANSPORT,
|
||||
PhysicalTransport,
|
||||
InvalidStateError,
|
||||
)
|
||||
from bumble.colors import color
|
||||
from bumble.hci import (
|
||||
Address,
|
||||
Role,
|
||||
HCI_SUCCESS,
|
||||
HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR,
|
||||
HCI_CONNECTION_TIMEOUT_ERROR,
|
||||
@@ -116,10 +115,10 @@ class LocalLink:
|
||||
|
||||
def send_acl_data(self, sender_controller, destination_address, transport, data):
|
||||
# Send the data to the first controller with a matching address
|
||||
if transport == BT_LE_TRANSPORT:
|
||||
if transport == PhysicalTransport.LE:
|
||||
destination_controller = self.find_controller(destination_address)
|
||||
source_address = sender_controller.random_address
|
||||
elif transport == BT_BR_EDR_TRANSPORT:
|
||||
elif transport == PhysicalTransport.BR_EDR:
|
||||
destination_controller = self.find_classic_controller(destination_address)
|
||||
source_address = sender_controller.public_address
|
||||
else:
|
||||
@@ -292,7 +291,7 @@ class LocalLink:
|
||||
return
|
||||
|
||||
async def task():
|
||||
if responder_role != BT_PERIPHERAL_ROLE:
|
||||
if responder_role != Role.PERIPHERAL:
|
||||
initiator_controller.on_classic_role_change(
|
||||
responder_controller.public_address, int(not (responder_role))
|
||||
)
|
||||
|
||||
@@ -20,14 +20,14 @@ import enum
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from .hci import (
|
||||
from bumble.hci import (
|
||||
Address,
|
||||
HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
||||
HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||
HCI_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||
)
|
||||
from .smp import (
|
||||
from bumble.smp import (
|
||||
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
SMP_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||
SMP_DISPLAY_ONLY_IO_CAPABILITY,
|
||||
@@ -41,7 +41,7 @@ from .smp import (
|
||||
OobLegacyContext,
|
||||
OobSharedData,
|
||||
)
|
||||
from .core import AdvertisingData, LeRole
|
||||
from bumble.core import AdvertisingData, LeRole
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -22,11 +22,11 @@ __version__ = "0.0.1"
|
||||
import grpc
|
||||
import grpc.aio
|
||||
|
||||
from .config import Config
|
||||
from .device import PandoraDevice
|
||||
from .host import HostService
|
||||
from .l2cap import L2CAPService
|
||||
from .security import SecurityService, SecurityStorageService
|
||||
from bumble.pandora.config import Config
|
||||
from bumble.pandora.device import PandoraDevice
|
||||
from bumble.pandora.host import HostService
|
||||
from bumble.pandora.l2cap import L2CAPService
|
||||
from bumble.pandora.security import SecurityService, SecurityStorageService
|
||||
from pandora.host_grpc_aio import add_HostServicer_to_server
|
||||
from pandora.l2cap_grpc_aio import add_L2CAPServicer_to_server
|
||||
from pandora.security_grpc_aio import (
|
||||
|
||||
@@ -20,12 +20,11 @@ import grpc.aio
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from . import utils
|
||||
from .config import Config
|
||||
import bumble.utils
|
||||
from bumble.pandora import utils
|
||||
from bumble.pandora.config import Config
|
||||
from bumble.core import (
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
BT_LE_TRANSPORT,
|
||||
BT_PERIPHERAL_ROLE,
|
||||
PhysicalTransport,
|
||||
UUID,
|
||||
AdvertisingData,
|
||||
Appearance,
|
||||
@@ -47,6 +46,8 @@ from bumble.hci import (
|
||||
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
||||
Address,
|
||||
Phy,
|
||||
Role,
|
||||
OwnAddressType,
|
||||
)
|
||||
from google.protobuf import any_pb2 # pytype: disable=pyi-error
|
||||
from google.protobuf import empty_pb2 # pytype: disable=pyi-error
|
||||
@@ -114,11 +115,11 @@ SECONDARY_PHY_TO_BUMBLE_PHY_MAP: Dict[SecondaryPhy, Phy] = {
|
||||
SECONDARY_CODED: Phy.LE_CODED,
|
||||
}
|
||||
|
||||
OWN_ADDRESS_MAP: Dict[host_pb2.OwnAddressType, bumble.hci.OwnAddressType] = {
|
||||
host_pb2.PUBLIC: bumble.hci.OwnAddressType.PUBLIC,
|
||||
host_pb2.RANDOM: bumble.hci.OwnAddressType.RANDOM,
|
||||
host_pb2.RESOLVABLE_OR_PUBLIC: bumble.hci.OwnAddressType.RESOLVABLE_OR_PUBLIC,
|
||||
host_pb2.RESOLVABLE_OR_RANDOM: bumble.hci.OwnAddressType.RESOLVABLE_OR_RANDOM,
|
||||
OWN_ADDRESS_MAP: Dict[host_pb2.OwnAddressType, OwnAddressType] = {
|
||||
host_pb2.PUBLIC: OwnAddressType.PUBLIC,
|
||||
host_pb2.RANDOM: OwnAddressType.RANDOM,
|
||||
host_pb2.RESOLVABLE_OR_PUBLIC: OwnAddressType.RESOLVABLE_OR_PUBLIC,
|
||||
host_pb2.RESOLVABLE_OR_RANDOM: OwnAddressType.RESOLVABLE_OR_RANDOM,
|
||||
}
|
||||
|
||||
|
||||
@@ -184,7 +185,7 @@ class HostService(HostServicer):
|
||||
|
||||
try:
|
||||
connection = await self.device.connect(
|
||||
address, transport=BT_BR_EDR_TRANSPORT
|
||||
address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
except ConnectionError as e:
|
||||
if e.error_code == HCI_PAGE_TIMEOUT_ERROR:
|
||||
@@ -217,7 +218,7 @@ class HostService(HostServicer):
|
||||
self.log.debug(f"WaitConnection from {address}...")
|
||||
|
||||
connection = self.device.find_connection_by_bd_addr(
|
||||
address, transport=BT_BR_EDR_TRANSPORT
|
||||
address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
if connection and id(connection) in self.waited_connections:
|
||||
# this connection was already returned: wait for a new one.
|
||||
@@ -249,8 +250,8 @@ class HostService(HostServicer):
|
||||
try:
|
||||
connection = await self.device.connect(
|
||||
address,
|
||||
transport=BT_LE_TRANSPORT,
|
||||
own_address_type=request.own_address_type,
|
||||
transport=PhysicalTransport.LE,
|
||||
own_address_type=OwnAddressType(request.own_address_type),
|
||||
)
|
||||
except ConnectionError as e:
|
||||
if e.error_code == HCI_PAGE_TIMEOUT_ERROR:
|
||||
@@ -377,8 +378,8 @@ class HostService(HostServicer):
|
||||
|
||||
def on_connection(connection: bumble.device.Connection) -> None:
|
||||
if (
|
||||
connection.transport == BT_LE_TRANSPORT
|
||||
and connection.role == BT_PERIPHERAL_ROLE
|
||||
connection.transport == PhysicalTransport.LE
|
||||
and connection.role == Role.PERIPHERAL
|
||||
):
|
||||
connections.put_nowait(connection)
|
||||
|
||||
@@ -495,8 +496,8 @@ class HostService(HostServicer):
|
||||
|
||||
def on_connection(connection: bumble.device.Connection) -> None:
|
||||
if (
|
||||
connection.transport == BT_LE_TRANSPORT
|
||||
and connection.role == BT_PERIPHERAL_ROLE
|
||||
connection.transport == PhysicalTransport.LE
|
||||
and connection.role == Role.PERIPHERAL
|
||||
):
|
||||
connections.put_nowait(connection)
|
||||
|
||||
@@ -509,7 +510,7 @@ class HostService(HostServicer):
|
||||
await self.device.start_advertising(
|
||||
target=target,
|
||||
advertising_type=advertising_type,
|
||||
own_address_type=request.own_address_type,
|
||||
own_address_type=OwnAddressType(request.own_address_type),
|
||||
)
|
||||
|
||||
if not request.connectable:
|
||||
@@ -534,7 +535,9 @@ class HostService(HostServicer):
|
||||
|
||||
try:
|
||||
self.log.debug('Stop advertising')
|
||||
await self.device.abort_on('flush', self.device.stop_advertising())
|
||||
await bumble.utils.cancel_on_event(
|
||||
self.device, 'flush', self.device.stop_advertising()
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -558,7 +561,7 @@ class HostService(HostServicer):
|
||||
await self.device.start_scanning(
|
||||
legacy=request.legacy,
|
||||
active=not request.passive,
|
||||
own_address_type=request.own_address_type,
|
||||
own_address_type=OwnAddressType(request.own_address_type),
|
||||
scan_interval=(
|
||||
int(request.interval)
|
||||
if request.interval
|
||||
@@ -602,7 +605,9 @@ class HostService(HostServicer):
|
||||
self.device.remove_listener('advertisement', handler) # type: ignore
|
||||
try:
|
||||
self.log.debug('Stop scanning')
|
||||
await self.device.abort_on('flush', self.device.stop_scanning())
|
||||
await bumble.utils.cancel_on_event(
|
||||
self.device, 'flush', self.device.stop_scanning()
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -642,7 +647,9 @@ class HostService(HostServicer):
|
||||
self.device.remove_listener('inquiry_result', result_handler) # type: ignore
|
||||
try:
|
||||
self.log.debug('Stop inquiry')
|
||||
await self.device.abort_on('flush', self.device.stop_discovery())
|
||||
await bumble.utils.cancel_on_event(
|
||||
self.device, 'flush', self.device.stop_discovery()
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ import logging
|
||||
|
||||
from asyncio import Queue as AsyncQueue, Future
|
||||
|
||||
from . import utils
|
||||
from .config import Config
|
||||
from bumble.pandora import utils
|
||||
from bumble.pandora.config import Config
|
||||
from bumble.core import OutOfResourcesError, InvalidArgumentError
|
||||
from bumble.device import Device
|
||||
from bumble.l2cap import (
|
||||
|
||||
@@ -18,18 +18,16 @@ import contextlib
|
||||
import grpc
|
||||
import logging
|
||||
|
||||
from . import utils
|
||||
from .config import Config
|
||||
from bumble.pandora import utils
|
||||
from bumble.pandora.config import Config
|
||||
from bumble import hci
|
||||
from bumble.core import (
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
BT_LE_TRANSPORT,
|
||||
BT_PERIPHERAL_ROLE,
|
||||
PhysicalTransport,
|
||||
ProtocolError,
|
||||
)
|
||||
import bumble.utils
|
||||
from bumble.device import Connection as BumbleConnection, Device
|
||||
from bumble.hci import HCI_Error
|
||||
from bumble.utils import EventWatcher
|
||||
from bumble.hci import HCI_Error, Role
|
||||
from bumble.pairing import PairingConfig, PairingDelegate as BasePairingDelegate
|
||||
from google.protobuf import any_pb2 # pytype: disable=pyi-error
|
||||
from google.protobuf import empty_pb2 # pytype: disable=pyi-error
|
||||
@@ -95,7 +93,7 @@ class PairingDelegate(BasePairingDelegate):
|
||||
else:
|
||||
# In BR/EDR, connection may not be complete,
|
||||
# use address instead
|
||||
assert self.connection.transport == BT_BR_EDR_TRANSPORT
|
||||
assert self.connection.transport == PhysicalTransport.BR_EDR
|
||||
ev.address = bytes(reversed(bytes(self.connection.peer_address)))
|
||||
|
||||
return ev
|
||||
@@ -174,7 +172,7 @@ class PairingDelegate(BasePairingDelegate):
|
||||
|
||||
async def display_number(self, number: int, digits: int = 6) -> None:
|
||||
if (
|
||||
self.connection.transport == BT_BR_EDR_TRANSPORT
|
||||
self.connection.transport == PhysicalTransport.BR_EDR
|
||||
and self.io_capability == BasePairingDelegate.DISPLAY_OUTPUT_ONLY
|
||||
):
|
||||
return
|
||||
@@ -287,7 +285,7 @@ class SecurityService(SecurityServicer):
|
||||
|
||||
oneof = request.WhichOneof('level')
|
||||
level = getattr(request, oneof)
|
||||
assert {BT_BR_EDR_TRANSPORT: 'classic', BT_LE_TRANSPORT: 'le'}[
|
||||
assert {PhysicalTransport.BR_EDR: 'classic', PhysicalTransport.LE: 'le'}[
|
||||
connection.transport
|
||||
] == oneof
|
||||
|
||||
@@ -302,7 +300,7 @@ class SecurityService(SecurityServicer):
|
||||
|
||||
security_result = asyncio.get_running_loop().create_future()
|
||||
|
||||
with contextlib.closing(EventWatcher()) as watcher:
|
||||
with contextlib.closing(bumble.utils.EventWatcher()) as watcher:
|
||||
|
||||
@watcher.on(connection, 'pairing')
|
||||
def on_pairing(*_: Any) -> None:
|
||||
@@ -317,8 +315,8 @@ class SecurityService(SecurityServicer):
|
||||
security_result.set_result('connection_died')
|
||||
|
||||
if (
|
||||
connection.transport == BT_LE_TRANSPORT
|
||||
and connection.role == BT_PERIPHERAL_ROLE
|
||||
connection.transport == PhysicalTransport.LE
|
||||
and connection.role == Role.PERIPHERAL
|
||||
):
|
||||
connection.request_pairing()
|
||||
else:
|
||||
@@ -379,7 +377,7 @@ class SecurityService(SecurityServicer):
|
||||
|
||||
assert request.level
|
||||
level = request.level
|
||||
assert {BT_BR_EDR_TRANSPORT: 'classic', BT_LE_TRANSPORT: 'le'}[
|
||||
assert {PhysicalTransport.BR_EDR: 'classic', PhysicalTransport.LE: 'le'}[
|
||||
connection.transport
|
||||
] == request.level_variant()
|
||||
|
||||
@@ -427,7 +425,7 @@ class SecurityService(SecurityServicer):
|
||||
self.log.debug('Wait for security: done')
|
||||
wait_for_security.set_result('success')
|
||||
elif (
|
||||
connection.transport == BT_BR_EDR_TRANSPORT
|
||||
connection.transport == PhysicalTransport.BR_EDR
|
||||
and self.need_authentication(connection, level)
|
||||
):
|
||||
nonlocal authenticate_task
|
||||
@@ -451,7 +449,7 @@ class SecurityService(SecurityServicer):
|
||||
'security_request': pair,
|
||||
}
|
||||
|
||||
with contextlib.closing(EventWatcher()) as watcher:
|
||||
with contextlib.closing(bumble.utils.EventWatcher()) as watcher:
|
||||
# register event handlers
|
||||
for event, listener in listeners.items():
|
||||
watcher.on(connection, event, listener)
|
||||
@@ -505,12 +503,12 @@ class SecurityService(SecurityServicer):
|
||||
return BR_LEVEL_REACHED[level](connection)
|
||||
|
||||
def need_pairing(self, connection: BumbleConnection, level: int) -> bool:
|
||||
if connection.transport == BT_LE_TRANSPORT:
|
||||
if connection.transport == PhysicalTransport.LE:
|
||||
return level >= LE_LEVEL3 and not connection.authenticated
|
||||
return False
|
||||
|
||||
def need_authentication(self, connection: BumbleConnection, level: int) -> bool:
|
||||
if connection.transport == BT_LE_TRANSPORT:
|
||||
if connection.transport == PhysicalTransport.LE:
|
||||
return False
|
||||
if level == LEVEL2 and connection.encryption != 0:
|
||||
return not connection.authenticated
|
||||
@@ -518,7 +516,7 @@ class SecurityService(SecurityServicer):
|
||||
|
||||
def need_encryption(self, connection: BumbleConnection, level: int) -> bool:
|
||||
# TODO(abel): need to support MITM
|
||||
if connection.transport == BT_LE_TRANSPORT:
|
||||
if connection.transport == PhysicalTransport.LE:
|
||||
return level == LE_LEVEL2 and not connection.encryption
|
||||
return level >= LEVEL2 and not connection.encryption
|
||||
|
||||
|
||||
@@ -20,11 +20,11 @@ import inspect
|
||||
import logging
|
||||
|
||||
from bumble.device import Device
|
||||
from bumble.hci import Address
|
||||
from bumble.hci import Address, AddressType
|
||||
from google.protobuf.message import Message # pytype: disable=pyi-error
|
||||
from typing import Any, Dict, Generator, MutableMapping, Optional, Tuple
|
||||
|
||||
ADDRESS_TYPES: Dict[str, int] = {
|
||||
ADDRESS_TYPES: Dict[str, AddressType] = {
|
||||
"public": Address.PUBLIC_DEVICE_ADDRESS,
|
||||
"random": Address.RANDOM_DEVICE_ADDRESS,
|
||||
"public_identity": Address.PUBLIC_IDENTITY_ADDRESS,
|
||||
|
||||
@@ -48,7 +48,7 @@ from bumble.gatt_adapters import (
|
||||
UTF8CharacteristicProxyAdapter,
|
||||
)
|
||||
from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
|
||||
from bumble.utils import OpenIntEnum
|
||||
from bumble import utils
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -64,7 +64,7 @@ GAIN_SETTINGS_MIN_VALUE = 0
|
||||
GAIN_SETTINGS_MAX_VALUE = 255
|
||||
|
||||
|
||||
class ErrorCode(OpenIntEnum):
|
||||
class ErrorCode(utils.OpenIntEnum):
|
||||
'''
|
||||
Cf. 1.6 Application error codes
|
||||
'''
|
||||
@@ -76,7 +76,7 @@ class ErrorCode(OpenIntEnum):
|
||||
GAIN_MODE_CHANGE_NOT_ALLOWED = 0x84
|
||||
|
||||
|
||||
class Mute(OpenIntEnum):
|
||||
class Mute(utils.OpenIntEnum):
|
||||
'''
|
||||
Cf. 2.2.1.2 Mute Field
|
||||
'''
|
||||
@@ -86,7 +86,7 @@ class Mute(OpenIntEnum):
|
||||
DISABLED = 0x02
|
||||
|
||||
|
||||
class GainMode(OpenIntEnum):
|
||||
class GainMode(utils.OpenIntEnum):
|
||||
'''
|
||||
Cf. 2.2.1.3 Gain Mode
|
||||
'''
|
||||
@@ -97,7 +97,7 @@ class GainMode(OpenIntEnum):
|
||||
AUTOMATIC = 0x03
|
||||
|
||||
|
||||
class AudioInputStatus(OpenIntEnum):
|
||||
class AudioInputStatus(utils.OpenIntEnum):
|
||||
'''
|
||||
Cf. 3.4 Audio Input Status
|
||||
'''
|
||||
@@ -106,7 +106,7 @@ class AudioInputStatus(OpenIntEnum):
|
||||
ACTIVE = 0x01
|
||||
|
||||
|
||||
class AudioInputControlPointOpCode(OpenIntEnum):
|
||||
class AudioInputControlPointOpCode(utils.OpenIntEnum):
|
||||
'''
|
||||
Cf. 3.5.1 Audio Input Control Point procedure requirements
|
||||
'''
|
||||
|
||||
513
bumble/profiles/ancs.py
Normal file
513
bumble/profiles/ancs.py
Normal file
@@ -0,0 +1,513 @@
|
||||
# Copyright 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.
|
||||
|
||||
"""
|
||||
Apple Notification Center Service (ANCS).
|
||||
"""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import datetime
|
||||
import enum
|
||||
import logging
|
||||
import struct
|
||||
from typing import Optional, Sequence, Union
|
||||
|
||||
|
||||
from bumble.att import ATT_Error
|
||||
from bumble.device import Peer
|
||||
from bumble.gatt import (
|
||||
Characteristic,
|
||||
GATT_ANCS_SERVICE,
|
||||
GATT_ANCS_NOTIFICATION_SOURCE_CHARACTERISTIC,
|
||||
GATT_ANCS_CONTROL_POINT_CHARACTERISTIC,
|
||||
GATT_ANCS_DATA_SOURCE_CHARACTERISTIC,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
||||
from bumble.gatt_adapters import SerializableCharacteristicProxyAdapter
|
||||
from bumble import utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
_DEFAULT_ATTRIBUTE_MAX_LENGTH = 65535
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Protocol
|
||||
# -----------------------------------------------------------------------------
|
||||
class ActionId(utils.OpenIntEnum):
|
||||
POSITIVE = 0
|
||||
NEGATIVE = 1
|
||||
|
||||
|
||||
class AppAttributeId(utils.OpenIntEnum):
|
||||
DISPLAY_NAME = 0
|
||||
|
||||
|
||||
class CategoryId(utils.OpenIntEnum):
|
||||
OTHER = 0
|
||||
INCOMING_CALL = 1
|
||||
MISSED_CALL = 2
|
||||
VOICEMAIL = 3
|
||||
SOCIAL = 4
|
||||
SCHEDULE = 5
|
||||
EMAIL = 6
|
||||
NEWS = 7
|
||||
HEALTH_AND_FITNESS = 8
|
||||
BUSINESS_AND_FINANCE = 9
|
||||
LOCATION = 10
|
||||
ENTERTAINMENT = 11
|
||||
|
||||
|
||||
class CommandId(utils.OpenIntEnum):
|
||||
GET_NOTIFICATION_ATTRIBUTES = 0
|
||||
GET_APP_ATTRIBUTES = 1
|
||||
PERFORM_NOTIFICATION_ACTION = 2
|
||||
|
||||
|
||||
class EventId(utils.OpenIntEnum):
|
||||
NOTIFICATION_ADDED = 0
|
||||
NOTIFICATION_MODIFIED = 1
|
||||
NOTIFICATION_REMOVED = 2
|
||||
|
||||
|
||||
class EventFlags(enum.IntFlag):
|
||||
SILENT = 1 << 0
|
||||
IMPORTANT = 1 << 1
|
||||
PRE_EXISTING = 1 << 2
|
||||
POSITIVE_ACTION = 1 << 3
|
||||
NEGATIVE_ACTION = 1 << 4
|
||||
|
||||
|
||||
class NotificationAttributeId(utils.OpenIntEnum):
|
||||
APP_IDENTIFIER = 0
|
||||
TITLE = 1
|
||||
SUBTITLE = 2
|
||||
MESSAGE = 3
|
||||
MESSAGE_SIZE = 4
|
||||
DATE = 5
|
||||
POSITIVE_ACTION_LABEL = 6
|
||||
NEGATIVE_ACTION_LABEL = 7
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class NotificationAttribute:
|
||||
attribute_id: NotificationAttributeId
|
||||
value: Union[str, int, datetime.datetime]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class AppAttribute:
|
||||
attribute_id: AppAttributeId
|
||||
value: str
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Notification:
|
||||
event_id: EventId
|
||||
event_flags: EventFlags
|
||||
category_id: CategoryId
|
||||
category_count: int
|
||||
notification_uid: int
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data: bytes) -> Notification:
|
||||
return cls(
|
||||
event_id=EventId(data[0]),
|
||||
event_flags=EventFlags(data[1]),
|
||||
category_id=CategoryId(data[2]),
|
||||
category_count=data[3],
|
||||
notification_uid=int.from_bytes(data[4:8], 'little'),
|
||||
)
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return struct.pack(
|
||||
"<BBBBI",
|
||||
self.event_id,
|
||||
self.event_flags,
|
||||
self.category_id,
|
||||
self.category_count,
|
||||
self.notification_uid,
|
||||
)
|
||||
|
||||
|
||||
class ErrorCode(utils.OpenIntEnum):
|
||||
UNKNOWN_COMMAND = 0xA0
|
||||
INVALID_COMMAND = 0xA1
|
||||
INVALID_PARAMETER = 0xA2
|
||||
ACTION_FAILED = 0xA3
|
||||
|
||||
|
||||
class ProtocolError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class CommandError(Exception):
|
||||
def __init__(self, error_code: ErrorCode) -> None:
|
||||
self.error_code = error_code
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"CommandError(error_code={self.error_code.name})"
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# GATT Server-side
|
||||
# -----------------------------------------------------------------------------
|
||||
class Ancs(TemplateService):
|
||||
UUID = GATT_ANCS_SERVICE
|
||||
|
||||
notification_source_characteristic: Characteristic
|
||||
data_source_characteristic: Characteristic
|
||||
control_point_characteristic: Characteristic
|
||||
|
||||
def __init__(self) -> None:
|
||||
# TODO not the final implementation
|
||||
self.notification_source_characteristic = Characteristic(
|
||||
GATT_ANCS_NOTIFICATION_SOURCE_CHARACTERISTIC,
|
||||
Characteristic.Properties.NOTIFY,
|
||||
Characteristic.Permissions.READABLE,
|
||||
)
|
||||
|
||||
# TODO not the final implementation
|
||||
self.data_source_characteristic = Characteristic(
|
||||
GATT_ANCS_DATA_SOURCE_CHARACTERISTIC,
|
||||
Characteristic.Properties.NOTIFY,
|
||||
Characteristic.Permissions.READABLE,
|
||||
)
|
||||
|
||||
# TODO not the final implementation
|
||||
self.control_point_characteristic = Characteristic(
|
||||
GATT_ANCS_CONTROL_POINT_CHARACTERISTIC,
|
||||
Characteristic.Properties.WRITE,
|
||||
Characteristic.Permissions.WRITEABLE,
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
[
|
||||
self.notification_source_characteristic,
|
||||
self.data_source_characteristic,
|
||||
self.control_point_characteristic,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# GATT Client-side
|
||||
# -----------------------------------------------------------------------------
|
||||
class AncsProxy(ProfileServiceProxy):
|
||||
SERVICE_CLASS = Ancs
|
||||
|
||||
notification_source: CharacteristicProxy[Notification]
|
||||
data_source: CharacteristicProxy
|
||||
control_point: CharacteristicProxy[bytes]
|
||||
|
||||
def __init__(self, service_proxy: ServiceProxy):
|
||||
self.notification_source = SerializableCharacteristicProxyAdapter(
|
||||
service_proxy.get_required_characteristic_by_uuid(
|
||||
GATT_ANCS_NOTIFICATION_SOURCE_CHARACTERISTIC
|
||||
),
|
||||
Notification,
|
||||
)
|
||||
|
||||
self.data_source = service_proxy.get_required_characteristic_by_uuid(
|
||||
GATT_ANCS_DATA_SOURCE_CHARACTERISTIC
|
||||
)
|
||||
|
||||
self.control_point = service_proxy.get_required_characteristic_by_uuid(
|
||||
GATT_ANCS_CONTROL_POINT_CHARACTERISTIC
|
||||
)
|
||||
|
||||
|
||||
class AncsClient(utils.EventEmitter):
|
||||
_expected_response_command_id: Optional[CommandId]
|
||||
_expected_response_notification_uid: Optional[int]
|
||||
_expected_response_app_identifier: Optional[str]
|
||||
_expected_app_identifier: Optional[str]
|
||||
_expected_response_tuples: int
|
||||
_response_accumulator: bytes
|
||||
|
||||
def __init__(self, ancs_proxy: AncsProxy) -> None:
|
||||
super().__init__()
|
||||
self._ancs_proxy = ancs_proxy
|
||||
self._command_semaphore = asyncio.Semaphore()
|
||||
self._response: Optional[asyncio.Future] = None
|
||||
self._reset_response()
|
||||
self._started = False
|
||||
|
||||
@classmethod
|
||||
async def for_peer(cls, peer: Peer) -> Optional[AncsClient]:
|
||||
ancs_proxy = await peer.discover_service_and_create_proxy(AncsProxy)
|
||||
if ancs_proxy is None:
|
||||
return None
|
||||
return cls(ancs_proxy)
|
||||
|
||||
async def start(self) -> None:
|
||||
await self._ancs_proxy.notification_source.subscribe(self._on_notification)
|
||||
await self._ancs_proxy.data_source.subscribe(self._on_data)
|
||||
self._started = True
|
||||
|
||||
async def stop(self) -> None:
|
||||
await self._ancs_proxy.notification_source.unsubscribe(self._on_notification)
|
||||
await self._ancs_proxy.data_source.unsubscribe(self._on_data)
|
||||
self._started = False
|
||||
|
||||
def _reset_response(self) -> None:
|
||||
self._expected_response_command_id = None
|
||||
self._expected_response_notification_uid = None
|
||||
self._expected_app_identifier = None
|
||||
self._expected_response_tuples = 0
|
||||
self._response_accumulator = b""
|
||||
|
||||
def _on_notification(self, notification: Notification) -> None:
|
||||
logger.debug(f"ANCS NOTIFICATION: {notification}")
|
||||
self.emit("notification", notification)
|
||||
|
||||
def _on_data(self, data: bytes) -> None:
|
||||
logger.debug(f"ANCS DATA: {data.hex()}")
|
||||
|
||||
if not self._response:
|
||||
logger.warning("received unexpected data, discarding")
|
||||
return
|
||||
|
||||
self._response_accumulator += data
|
||||
|
||||
# Try to parse the accumulated data until we have all we need.
|
||||
if not self._response_accumulator:
|
||||
logger.warning("empty data from data source")
|
||||
return
|
||||
|
||||
command_id = self._response_accumulator[0]
|
||||
if command_id != self._expected_response_command_id:
|
||||
logger.warning(
|
||||
"unexpected response command id: "
|
||||
f"expected {self._expected_response_command_id} "
|
||||
f"but got {command_id}"
|
||||
)
|
||||
self._reset_response()
|
||||
if not self._response.done():
|
||||
self._response.set_exception(ProtocolError())
|
||||
|
||||
if len(self._response_accumulator) < 5:
|
||||
# Not enough data yet.
|
||||
return
|
||||
|
||||
attributes: list[Union[NotificationAttribute, AppAttribute]] = []
|
||||
|
||||
if command_id == CommandId.GET_NOTIFICATION_ATTRIBUTES:
|
||||
(notification_uid,) = struct.unpack_from(
|
||||
"<I", self._response_accumulator, 1
|
||||
)
|
||||
if notification_uid != self._expected_response_notification_uid:
|
||||
logger.warning(
|
||||
"unexpected response notification uid: "
|
||||
f"expected {self._expected_response_notification_uid} "
|
||||
f"but got {notification_uid}"
|
||||
)
|
||||
self._reset_response()
|
||||
if not self._response.done():
|
||||
self._response.set_exception(ProtocolError())
|
||||
|
||||
attribute_data = self._response_accumulator[5:]
|
||||
while len(attribute_data) >= 3:
|
||||
attribute_id, attribute_data_length = struct.unpack_from(
|
||||
"<BH", attribute_data, 0
|
||||
)
|
||||
if len(attribute_data) < 3 + attribute_data_length:
|
||||
return
|
||||
str_value = attribute_data[3 : 3 + attribute_data_length].decode(
|
||||
"utf-8"
|
||||
)
|
||||
value: Union[str, int, datetime.datetime]
|
||||
if attribute_id == NotificationAttributeId.MESSAGE_SIZE:
|
||||
value = int(str_value)
|
||||
elif attribute_id == NotificationAttributeId.DATE:
|
||||
year = int(str_value[:4])
|
||||
month = int(str_value[4:6])
|
||||
day = int(str_value[6:8])
|
||||
hour = int(str_value[9:11])
|
||||
minute = int(str_value[11:13])
|
||||
second = int(str_value[13:15])
|
||||
value = datetime.datetime(year, month, day, hour, minute, second)
|
||||
else:
|
||||
value = str_value
|
||||
attributes.append(
|
||||
NotificationAttribute(NotificationAttributeId(attribute_id), value)
|
||||
)
|
||||
attribute_data = attribute_data[3 + attribute_data_length :]
|
||||
elif command_id == CommandId.GET_APP_ATTRIBUTES:
|
||||
if 0 not in self._response_accumulator[1:]:
|
||||
# No null-terminated string yet.
|
||||
return
|
||||
|
||||
app_identifier_length = self._response_accumulator.find(0, 1) - 1
|
||||
app_identifier = self._response_accumulator[
|
||||
1 : 1 + app_identifier_length
|
||||
].decode("utf-8")
|
||||
if app_identifier != self._expected_response_app_identifier:
|
||||
logger.warning(
|
||||
"unexpected response app identifier: "
|
||||
f"expected {self._expected_response_app_identifier} "
|
||||
f"but got {app_identifier}"
|
||||
)
|
||||
self._reset_response()
|
||||
if not self._response.done():
|
||||
self._response.set_exception(ProtocolError())
|
||||
|
||||
attribute_data = self._response_accumulator[1 + app_identifier_length + 1 :]
|
||||
while len(attribute_data) >= 3:
|
||||
attribute_id, attribute_data_length = struct.unpack_from(
|
||||
"<BH", attribute_data, 0
|
||||
)
|
||||
if len(attribute_data) < 3 + attribute_data_length:
|
||||
return
|
||||
attributes.append(
|
||||
AppAttribute(
|
||||
AppAttributeId(attribute_id),
|
||||
attribute_data[3 : 3 + attribute_data_length].decode("utf-8"),
|
||||
)
|
||||
)
|
||||
attribute_data = attribute_data[3 + attribute_data_length :]
|
||||
else:
|
||||
logger.warning(f"unexpected response command id {command_id}")
|
||||
return
|
||||
|
||||
if len(attributes) < self._expected_response_tuples:
|
||||
# We have not received all the tuples yet.
|
||||
return
|
||||
|
||||
if not self._response.done():
|
||||
self._response.set_result(attributes)
|
||||
|
||||
async def _send_command(self, command: bytes) -> None:
|
||||
try:
|
||||
await self._ancs_proxy.control_point.write_value(
|
||||
command, with_response=True
|
||||
)
|
||||
except ATT_Error as error:
|
||||
raise CommandError(error_code=ErrorCode(error.error_code)) from error
|
||||
|
||||
async def get_notification_attributes(
|
||||
self,
|
||||
notification_uid: int,
|
||||
attributes: Sequence[
|
||||
Union[NotificationAttributeId, tuple[NotificationAttributeId, int]]
|
||||
],
|
||||
) -> list[NotificationAttribute]:
|
||||
if not self._started:
|
||||
raise RuntimeError("client not started")
|
||||
|
||||
command = struct.pack(
|
||||
"<BI", CommandId.GET_NOTIFICATION_ATTRIBUTES, notification_uid
|
||||
)
|
||||
for attribute in attributes:
|
||||
attribute_max_length = 0
|
||||
if isinstance(attribute, tuple):
|
||||
attribute_id, attribute_max_length = attribute
|
||||
if attribute_id not in (
|
||||
NotificationAttributeId.TITLE,
|
||||
NotificationAttributeId.SUBTITLE,
|
||||
NotificationAttributeId.MESSAGE,
|
||||
):
|
||||
raise ValueError(
|
||||
"this attribute does not allow specifying a max length"
|
||||
)
|
||||
else:
|
||||
attribute_id = attribute
|
||||
if attribute_id in (
|
||||
NotificationAttributeId.TITLE,
|
||||
NotificationAttributeId.SUBTITLE,
|
||||
NotificationAttributeId.MESSAGE,
|
||||
):
|
||||
attribute_max_length = _DEFAULT_ATTRIBUTE_MAX_LENGTH
|
||||
|
||||
if attribute_max_length:
|
||||
command += struct.pack("<BH", attribute_id, attribute_max_length)
|
||||
else:
|
||||
command += struct.pack("B", attribute_id)
|
||||
|
||||
try:
|
||||
async with self._command_semaphore:
|
||||
self._expected_response_notification_uid = notification_uid
|
||||
self._expected_response_tuples = len(attributes)
|
||||
self._expected_response_command_id = (
|
||||
CommandId.GET_NOTIFICATION_ATTRIBUTES
|
||||
)
|
||||
self._response = asyncio.Future()
|
||||
|
||||
# Send the command.
|
||||
await self._send_command(command)
|
||||
|
||||
# Wait for the response.
|
||||
return await self._response
|
||||
finally:
|
||||
self._reset_response()
|
||||
|
||||
async def get_app_attributes(
|
||||
self, app_identifier: str, attributes: Sequence[AppAttributeId]
|
||||
) -> list[AppAttribute]:
|
||||
if not self._started:
|
||||
raise RuntimeError("client not started")
|
||||
|
||||
command = (
|
||||
bytes([CommandId.GET_APP_ATTRIBUTES])
|
||||
+ app_identifier.encode("utf-8")
|
||||
+ b"\0"
|
||||
)
|
||||
for attribute_id in attributes:
|
||||
command += struct.pack("B", attribute_id)
|
||||
|
||||
try:
|
||||
async with self._command_semaphore:
|
||||
self._expected_response_app_identifier = app_identifier
|
||||
self._expected_response_tuples = len(attributes)
|
||||
self._expected_response_command_id = CommandId.GET_APP_ATTRIBUTES
|
||||
self._response = asyncio.Future()
|
||||
|
||||
# Send the command.
|
||||
await self._send_command(command)
|
||||
|
||||
# Wait for the response.
|
||||
return await self._response
|
||||
finally:
|
||||
self._reset_response()
|
||||
|
||||
async def perform_action(self, notification_uid: int, action: ActionId) -> None:
|
||||
if not self._started:
|
||||
raise RuntimeError("client not started")
|
||||
|
||||
command = struct.pack(
|
||||
"<BIB", CommandId.PERFORM_NOTIFICATION_ACTION, notification_uid, action
|
||||
)
|
||||
|
||||
async with self._command_semaphore:
|
||||
await self._send_command(command)
|
||||
|
||||
async def perform_positive_action(self, notification_uid: int) -> None:
|
||||
return await self.perform_action(notification_uid, ActionId.POSITIVE)
|
||||
|
||||
async def perform_negative_action(self, notification_uid: int) -> None:
|
||||
return await self.perform_action(notification_uid, ActionId.NEGATIVE)
|
||||
@@ -23,6 +23,7 @@ import logging
|
||||
import struct
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union
|
||||
|
||||
from bumble import utils
|
||||
from bumble import colors
|
||||
from bumble.profiles.bap import CodecSpecificConfiguration
|
||||
from bumble.profiles import le_audio
|
||||
@@ -343,8 +344,10 @@ class AseStateMachine(gatt.Characteristic):
|
||||
and cis_id == self.cis_id
|
||||
and self.state == self.State.ENABLING
|
||||
):
|
||||
acl_connection.abort_on(
|
||||
'flush', self.service.device.accept_cis_request(cis_handle)
|
||||
utils.cancel_on_event(
|
||||
acl_connection,
|
||||
'flush',
|
||||
self.service.device.accept_cis_request(cis_handle),
|
||||
)
|
||||
|
||||
def on_cis_establishment(self, cis_link: device.CisLink) -> None:
|
||||
@@ -361,7 +364,9 @@ class AseStateMachine(gatt.Characteristic):
|
||||
self.state = self.State.STREAMING
|
||||
await self.service.device.notify_subscribers(self, self.value)
|
||||
|
||||
cis_link.acl_connection.abort_on('flush', post_cis_established())
|
||||
utils.cancel_on_event(
|
||||
cis_link.acl_connection, 'flush', post_cis_established()
|
||||
)
|
||||
self.cis_link = cis_link
|
||||
|
||||
def on_cis_disconnection(self, _reason) -> None:
|
||||
@@ -509,7 +514,7 @@ class AseStateMachine(gatt.Characteristic):
|
||||
self.state = self.State.IDLE
|
||||
await self.service.device.notify_subscribers(self, self.value)
|
||||
|
||||
self.service.device.abort_on('flush', remove_cis_async())
|
||||
utils.cancel_on_event(self.service.device, 'flush', remove_cis_async())
|
||||
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
||||
|
||||
@property
|
||||
@@ -594,7 +599,7 @@ class AudioStreamControlService(gatt.TemplateService):
|
||||
UUID = gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE
|
||||
|
||||
ase_state_machines: Dict[int, AseStateMachine]
|
||||
ase_control_point: gatt.Characteristic
|
||||
ase_control_point: gatt.Characteristic[bytes]
|
||||
_active_client: Optional[device.Connection] = None
|
||||
|
||||
def __init__(
|
||||
@@ -691,7 +696,8 @@ class AudioStreamControlService(gatt.TemplateService):
|
||||
control_point_notification = bytes(
|
||||
[operation.op_code, len(responses)]
|
||||
) + b''.join(map(bytes, responses))
|
||||
self.device.abort_on(
|
||||
utils.cancel_on_event(
|
||||
self.device,
|
||||
'flush',
|
||||
self.device.notify_subscribers(
|
||||
self.ase_control_point, control_point_notification
|
||||
@@ -700,7 +706,8 @@ class AudioStreamControlService(gatt.TemplateService):
|
||||
|
||||
for ase_id, *_ in responses:
|
||||
if ase := self.ase_state_machines.get(ase_id):
|
||||
self.device.abort_on(
|
||||
utils.cancel_on_event(
|
||||
self.device,
|
||||
'flush',
|
||||
self.device.notify_subscribers(ase, ase.value),
|
||||
)
|
||||
@@ -710,9 +717,9 @@ class AudioStreamControlService(gatt.TemplateService):
|
||||
class AudioStreamControlServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
SERVICE_CLASS = AudioStreamControlService
|
||||
|
||||
sink_ase: List[gatt_client.CharacteristicProxy]
|
||||
source_ase: List[gatt_client.CharacteristicProxy]
|
||||
ase_control_point: gatt_client.CharacteristicProxy
|
||||
sink_ase: List[gatt_client.CharacteristicProxy[bytes]]
|
||||
source_ase: List[gatt_client.CharacteristicProxy[bytes]]
|
||||
ase_control_point: gatt_client.CharacteristicProxy[bytes]
|
||||
|
||||
def __init__(self, service_proxy: gatt_client.ServiceProxy):
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
@@ -259,11 +259,11 @@ class AshaService(gatt.TemplateService):
|
||||
# -----------------------------------------------------------------------------
|
||||
class AshaServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
SERVICE_CLASS = AshaService
|
||||
read_only_properties_characteristic: gatt_client.CharacteristicProxy
|
||||
audio_control_point_characteristic: gatt_client.CharacteristicProxy
|
||||
audio_status_point_characteristic: gatt_client.CharacteristicProxy
|
||||
volume_characteristic: gatt_client.CharacteristicProxy
|
||||
psm_characteristic: gatt_client.CharacteristicProxy
|
||||
read_only_properties_characteristic: gatt_client.CharacteristicProxy[bytes]
|
||||
audio_control_point_characteristic: gatt_client.CharacteristicProxy[bytes]
|
||||
audio_status_point_characteristic: gatt_client.CharacteristicProxy[bytes]
|
||||
volume_characteristic: gatt_client.CharacteristicProxy[bytes]
|
||||
psm_characteristic: gatt_client.CharacteristicProxy[bytes]
|
||||
|
||||
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
@@ -354,7 +354,7 @@ class BroadcastAudioScanService(gatt.TemplateService):
|
||||
class BroadcastAudioScanServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
SERVICE_CLASS = BroadcastAudioScanService
|
||||
|
||||
broadcast_audio_scan_control_point: gatt_client.CharacteristicProxy
|
||||
broadcast_audio_scan_control_point: gatt_client.CharacteristicProxy[bytes]
|
||||
broadcast_receive_states: list[
|
||||
gatt_client.CharacteristicProxy[Optional[BroadcastReceiveState]]
|
||||
]
|
||||
|
||||
@@ -99,10 +99,10 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
|
||||
UUID = gatt.GATT_COORDINATED_SET_IDENTIFICATION_SERVICE
|
||||
|
||||
set_identity_resolving_key: bytes
|
||||
set_identity_resolving_key_characteristic: gatt.Characteristic
|
||||
coordinated_set_size_characteristic: Optional[gatt.Characteristic] = None
|
||||
set_member_lock_characteristic: Optional[gatt.Characteristic] = None
|
||||
set_member_rank_characteristic: Optional[gatt.Characteristic] = None
|
||||
set_identity_resolving_key_characteristic: gatt.Characteristic[bytes]
|
||||
coordinated_set_size_characteristic: Optional[gatt.Characteristic[bytes]] = None
|
||||
set_member_lock_characteristic: Optional[gatt.Characteristic[bytes]] = None
|
||||
set_member_rank_characteristic: Optional[gatt.Characteristic[bytes]] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -170,7 +170,7 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
|
||||
else:
|
||||
assert connection
|
||||
|
||||
if connection.transport == core.BT_LE_TRANSPORT:
|
||||
if connection.transport == core.PhysicalTransport.LE:
|
||||
key = await connection.device.get_long_term_key(
|
||||
connection_handle=connection.handle, rand=b'', ediv=0
|
||||
)
|
||||
@@ -203,10 +203,10 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
|
||||
class CoordinatedSetIdentificationProxy(gatt_client.ProfileServiceProxy):
|
||||
SERVICE_CLASS = CoordinatedSetIdentificationService
|
||||
|
||||
set_identity_resolving_key: gatt_client.CharacteristicProxy
|
||||
coordinated_set_size: Optional[gatt_client.CharacteristicProxy] = None
|
||||
set_member_lock: Optional[gatt_client.CharacteristicProxy] = None
|
||||
set_member_rank: Optional[gatt_client.CharacteristicProxy] = None
|
||||
set_identity_resolving_key: gatt_client.CharacteristicProxy[bytes]
|
||||
coordinated_set_size: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
set_member_lock: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
set_member_rank: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
|
||||
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
||||
self.service_proxy = service_proxy
|
||||
@@ -242,7 +242,7 @@ class CoordinatedSetIdentificationProxy(gatt_client.ProfileServiceProxy):
|
||||
else:
|
||||
connection = self.service_proxy.client.connection
|
||||
device = connection.device
|
||||
if connection.transport == core.BT_LE_TRANSPORT:
|
||||
if connection.transport == core.PhysicalTransport.LE:
|
||||
key = await device.get_long_term_key(
|
||||
connection_handle=connection.handle, rand=b'', ediv=0
|
||||
)
|
||||
|
||||
@@ -32,10 +32,10 @@ class GenericAttributeProfileService(gatt.TemplateService):
|
||||
|
||||
UUID = gatt.GATT_GENERIC_ATTRIBUTE_SERVICE
|
||||
|
||||
client_supported_features_characteristic: gatt.Characteristic | None = None
|
||||
server_supported_features_characteristic: gatt.Characteristic | None = None
|
||||
database_hash_characteristic: gatt.Characteristic | None = None
|
||||
service_changed_characteristic: gatt.Characteristic | None = None
|
||||
client_supported_features_characteristic: gatt.Characteristic[bytes] | None = None
|
||||
server_supported_features_characteristic: gatt.Characteristic[bytes] | None = None
|
||||
database_hash_characteristic: gatt.Characteristic[bytes] | None = None
|
||||
service_changed_characteristic: gatt.Characteristic[bytes] | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -143,14 +143,14 @@ class GenericAttributeProfileService(gatt.TemplateService):
|
||||
class GenericAttributeProfileServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
SERVICE_CLASS = GenericAttributeProfileService
|
||||
|
||||
client_supported_features_characteristic: gatt_client.CharacteristicProxy | None = (
|
||||
None
|
||||
)
|
||||
server_supported_features_characteristic: gatt_client.CharacteristicProxy | None = (
|
||||
None
|
||||
)
|
||||
database_hash_characteristic: gatt_client.CharacteristicProxy | None = None
|
||||
service_changed_characteristic: gatt_client.CharacteristicProxy | None = None
|
||||
client_supported_features_characteristic: (
|
||||
gatt_client.CharacteristicProxy[bytes] | None
|
||||
) = None
|
||||
server_supported_features_characteristic: (
|
||||
gatt_client.CharacteristicProxy[bytes] | None
|
||||
) = None
|
||||
database_hash_characteristic: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
service_changed_characteristic: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
|
||||
_CHARACTERISTICS = {
|
||||
gatt.GATT_CLIENT_SUPPORTED_FEATURES_CHARACTERISTIC: 'client_supported_features_characteristic',
|
||||
|
||||
@@ -25,14 +25,14 @@ from typing import Any, Dict, List, Optional, Set, Union
|
||||
from bumble import att, gatt, gatt_adapters, gatt_client
|
||||
from bumble.core import InvalidArgumentError, InvalidStateError
|
||||
from bumble.device import Device, Connection
|
||||
from bumble.utils import AsyncRunner, OpenIntEnum
|
||||
from bumble import utils
|
||||
from bumble.hci import Address
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
class ErrorCode(OpenIntEnum):
|
||||
class ErrorCode(utils.OpenIntEnum):
|
||||
'''See Hearing Access Service 2.4. Attribute Profile error codes.'''
|
||||
|
||||
INVALID_OPCODE = 0x80
|
||||
@@ -42,7 +42,7 @@ class ErrorCode(OpenIntEnum):
|
||||
INVALID_PARAMETERS_LENGTH = 0x84
|
||||
|
||||
|
||||
class HearingAidType(OpenIntEnum):
|
||||
class HearingAidType(utils.OpenIntEnum):
|
||||
'''See Hearing Access Service 3.1. Hearing Aid Features.'''
|
||||
|
||||
BINAURAL_HEARING_AID = 0b00
|
||||
@@ -50,35 +50,35 @@ class HearingAidType(OpenIntEnum):
|
||||
BANDED_HEARING_AID = 0b10
|
||||
|
||||
|
||||
class PresetSynchronizationSupport(OpenIntEnum):
|
||||
class PresetSynchronizationSupport(utils.OpenIntEnum):
|
||||
'''See Hearing Access Service 3.1. Hearing Aid Features.'''
|
||||
|
||||
PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED = 0b0
|
||||
PRESET_SYNCHRONIZATION_IS_SUPPORTED = 0b1
|
||||
|
||||
|
||||
class IndependentPresets(OpenIntEnum):
|
||||
class IndependentPresets(utils.OpenIntEnum):
|
||||
'''See Hearing Access Service 3.1. Hearing Aid Features.'''
|
||||
|
||||
IDENTICAL_PRESET_RECORD = 0b0
|
||||
DIFFERENT_PRESET_RECORD = 0b1
|
||||
|
||||
|
||||
class DynamicPresets(OpenIntEnum):
|
||||
class DynamicPresets(utils.OpenIntEnum):
|
||||
'''See Hearing Access Service 3.1. Hearing Aid Features.'''
|
||||
|
||||
PRESET_RECORDS_DOES_NOT_CHANGE = 0b0
|
||||
PRESET_RECORDS_MAY_CHANGE = 0b1
|
||||
|
||||
|
||||
class WritablePresetsSupport(OpenIntEnum):
|
||||
class WritablePresetsSupport(utils.OpenIntEnum):
|
||||
'''See Hearing Access Service 3.1. Hearing Aid Features.'''
|
||||
|
||||
WRITABLE_PRESET_RECORDS_NOT_SUPPORTED = 0b0
|
||||
WRITABLE_PRESET_RECORDS_SUPPORTED = 0b1
|
||||
|
||||
|
||||
class HearingAidPresetControlPointOpcode(OpenIntEnum):
|
||||
class HearingAidPresetControlPointOpcode(utils.OpenIntEnum):
|
||||
'''See Hearing Access Service 3.3.1 Hearing Aid Preset Control Point operation requirements.'''
|
||||
|
||||
# fmt: off
|
||||
@@ -130,7 +130,7 @@ def HearingAidFeatures_from_bytes(data: int) -> HearingAidFeatures:
|
||||
class PresetChangedOperation:
|
||||
'''See Hearing Access Service 3.2.2.2. Preset Changed operation.'''
|
||||
|
||||
class ChangeId(OpenIntEnum):
|
||||
class ChangeId(utils.OpenIntEnum):
|
||||
# fmt: off
|
||||
GENERIC_UPDATE = 0x00
|
||||
PRESET_RECORD_DELETED = 0x01
|
||||
@@ -190,11 +190,11 @@ class PresetRecord:
|
||||
|
||||
@dataclass
|
||||
class Property:
|
||||
class Writable(OpenIntEnum):
|
||||
class Writable(utils.OpenIntEnum):
|
||||
CANNOT_BE_WRITTEN = 0b0
|
||||
CAN_BE_WRITTEN = 0b1
|
||||
|
||||
class IsAvailable(OpenIntEnum):
|
||||
class IsAvailable(utils.OpenIntEnum):
|
||||
IS_UNAVAILABLE = 0b0
|
||||
IS_AVAILABLE = 0b1
|
||||
|
||||
@@ -224,9 +224,9 @@ class PresetRecord:
|
||||
class HearingAccessService(gatt.TemplateService):
|
||||
UUID = gatt.GATT_HEARING_ACCESS_SERVICE
|
||||
|
||||
hearing_aid_features_characteristic: gatt.Characteristic
|
||||
hearing_aid_preset_control_point: gatt.Characteristic
|
||||
active_preset_index_characteristic: gatt.Characteristic
|
||||
hearing_aid_features_characteristic: gatt.Characteristic[bytes]
|
||||
hearing_aid_preset_control_point: gatt.Characteristic[bytes]
|
||||
active_preset_index_characteristic: gatt.Characteristic[bytes]
|
||||
active_preset_index: int
|
||||
active_preset_index_per_device: Dict[Address, int]
|
||||
|
||||
@@ -333,7 +333,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
# Update the active preset index if needed
|
||||
await self.notify_active_preset_for_connection(connection)
|
||||
|
||||
connection.abort_on('disconnection', on_connection_async())
|
||||
utils.cancel_on_event(connection, 'disconnection', on_connection_async())
|
||||
|
||||
def _on_read_active_preset_index(
|
||||
self, __connection__: Optional[Connection]
|
||||
@@ -382,7 +382,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
if len(presets) == 0:
|
||||
raise att.ATT_Error(att.ErrorCode.OUT_OF_RANGE)
|
||||
|
||||
AsyncRunner.spawn(self._read_preset_response(connection, presets))
|
||||
utils.AsyncRunner.spawn(self._read_preset_response(connection, presets))
|
||||
|
||||
async def _read_preset_response(
|
||||
self, connection: Connection, presets: List[PresetRecord]
|
||||
|
||||
@@ -338,30 +338,32 @@ class MediaControlServiceProxy(
|
||||
'content_control_id': gatt.GATT_CONTENT_CONTROL_ID_CHARACTERISTIC,
|
||||
}
|
||||
|
||||
media_player_name: Optional[gatt_client.CharacteristicProxy] = None
|
||||
media_player_icon_object_id: Optional[gatt_client.CharacteristicProxy] = None
|
||||
media_player_icon_url: Optional[gatt_client.CharacteristicProxy] = None
|
||||
track_changed: Optional[gatt_client.CharacteristicProxy] = None
|
||||
track_title: Optional[gatt_client.CharacteristicProxy] = None
|
||||
track_duration: Optional[gatt_client.CharacteristicProxy] = None
|
||||
track_position: Optional[gatt_client.CharacteristicProxy] = None
|
||||
playback_speed: Optional[gatt_client.CharacteristicProxy] = None
|
||||
seeking_speed: Optional[gatt_client.CharacteristicProxy] = None
|
||||
current_track_segments_object_id: Optional[gatt_client.CharacteristicProxy] = None
|
||||
current_track_object_id: Optional[gatt_client.CharacteristicProxy] = None
|
||||
next_track_object_id: Optional[gatt_client.CharacteristicProxy] = None
|
||||
parent_group_object_id: Optional[gatt_client.CharacteristicProxy] = None
|
||||
current_group_object_id: Optional[gatt_client.CharacteristicProxy] = None
|
||||
playing_order: Optional[gatt_client.CharacteristicProxy] = None
|
||||
playing_orders_supported: Optional[gatt_client.CharacteristicProxy] = None
|
||||
media_state: Optional[gatt_client.CharacteristicProxy] = None
|
||||
media_control_point: Optional[gatt_client.CharacteristicProxy] = None
|
||||
media_control_point_opcodes_supported: Optional[gatt_client.CharacteristicProxy] = (
|
||||
None
|
||||
)
|
||||
search_control_point: Optional[gatt_client.CharacteristicProxy] = None
|
||||
search_results_object_id: Optional[gatt_client.CharacteristicProxy] = None
|
||||
content_control_id: Optional[gatt_client.CharacteristicProxy] = None
|
||||
media_player_name: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
media_player_icon_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
media_player_icon_url: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
track_changed: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
track_title: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
track_duration: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
track_position: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
playback_speed: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
seeking_speed: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
current_track_segments_object_id: Optional[
|
||||
gatt_client.CharacteristicProxy[bytes]
|
||||
] = None
|
||||
current_track_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
next_track_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
parent_group_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
current_group_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
playing_order: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
playing_orders_supported: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
media_state: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
media_control_point: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
media_control_point_opcodes_supported: Optional[
|
||||
gatt_client.CharacteristicProxy[bytes]
|
||||
] = None
|
||||
search_control_point: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
search_results_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
content_control_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
|
||||
if TYPE_CHECKING:
|
||||
media_control_point_notifications: asyncio.Queue[bytes]
|
||||
|
||||
@@ -104,12 +104,12 @@ class PacRecord:
|
||||
class PublishedAudioCapabilitiesService(gatt.TemplateService):
|
||||
UUID = gatt.GATT_PUBLISHED_AUDIO_CAPABILITIES_SERVICE
|
||||
|
||||
sink_pac: Optional[gatt.Characteristic]
|
||||
sink_audio_locations: Optional[gatt.Characteristic]
|
||||
source_pac: Optional[gatt.Characteristic]
|
||||
source_audio_locations: Optional[gatt.Characteristic]
|
||||
available_audio_contexts: gatt.Characteristic
|
||||
supported_audio_contexts: gatt.Characteristic
|
||||
sink_pac: Optional[gatt.Characteristic[bytes]]
|
||||
sink_audio_locations: Optional[gatt.Characteristic[bytes]]
|
||||
source_pac: Optional[gatt.Characteristic[bytes]]
|
||||
source_audio_locations: Optional[gatt.Characteristic[bytes]]
|
||||
available_audio_contexts: gatt.Characteristic[bytes]
|
||||
supported_audio_contexts: gatt.Characteristic[bytes]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -40,7 +40,7 @@ class PublicBroadcastAnnouncement:
|
||||
def from_bytes(cls, data: bytes) -> Self:
|
||||
features = cls.Features(data[0])
|
||||
metadata_length = data[1]
|
||||
metadata_ltv = data[1 : 1 + metadata_length]
|
||||
metadata_ltv = data[2 : 2 + metadata_length]
|
||||
return cls(
|
||||
features=features, metadata=le_audio.Metadata.from_bytes(metadata_ltv)
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ import enum
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from bumble import att
|
||||
from bumble import utils
|
||||
from bumble import device
|
||||
from bumble import gatt
|
||||
from bumble import gatt_adapters
|
||||
@@ -90,9 +91,9 @@ class VolumeState:
|
||||
class VolumeControlService(gatt.TemplateService):
|
||||
UUID = gatt.GATT_VOLUME_CONTROL_SERVICE
|
||||
|
||||
volume_state: gatt.Characteristic
|
||||
volume_control_point: gatt.Characteristic
|
||||
volume_flags: gatt.Characteristic
|
||||
volume_state: gatt.Characteristic[bytes]
|
||||
volume_control_point: gatt.Characteristic[bytes]
|
||||
volume_flags: gatt.Characteristic[bytes]
|
||||
|
||||
volume_setting: int
|
||||
muted: int
|
||||
@@ -160,7 +161,8 @@ class VolumeControlService(gatt.TemplateService):
|
||||
handler = getattr(self, '_on_' + opcode.name.lower())
|
||||
if handler(*value[2:]):
|
||||
self.change_counter = (self.change_counter + 1) % 256
|
||||
connection.abort_on(
|
||||
utils.cancel_on_event(
|
||||
connection,
|
||||
'disconnection',
|
||||
connection.device.notify_subscribers(attribute=self.volume_state),
|
||||
)
|
||||
|
||||
@@ -38,7 +38,7 @@ from bumble.gatt_adapters import (
|
||||
UTF8CharacteristicProxyAdapter,
|
||||
)
|
||||
from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
|
||||
from bumble.utils import OpenIntEnum
|
||||
from bumble import utils
|
||||
from bumble.profiles.bap import AudioLocation
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -50,11 +50,11 @@ MAX_VOLUME_OFFSET = 255
|
||||
CHANGE_COUNTER_MAX_VALUE = 0xFF
|
||||
|
||||
|
||||
class SetVolumeOffsetOpCode(OpenIntEnum):
|
||||
class SetVolumeOffsetOpCode(utils.OpenIntEnum):
|
||||
SET_VOLUME_OFFSET = 0x01
|
||||
|
||||
|
||||
class ErrorCode(OpenIntEnum):
|
||||
class ErrorCode(utils.OpenIntEnum):
|
||||
"""
|
||||
See Volume Offset Control Service 1.6. Application error codes.
|
||||
"""
|
||||
|
||||
@@ -25,16 +25,16 @@ import enum
|
||||
from typing import Callable, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
|
||||
from typing_extensions import Self
|
||||
|
||||
from pyee import EventEmitter
|
||||
|
||||
from bumble import core
|
||||
from bumble import l2cap
|
||||
from bumble import sdp
|
||||
from .colors import color
|
||||
from .core import (
|
||||
from bumble import utils
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
UUID,
|
||||
BT_RFCOMM_PROTOCOL_ID,
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
PhysicalTransport,
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
InvalidArgumentError,
|
||||
InvalidStateError,
|
||||
@@ -441,7 +441,7 @@ class RFCOMM_MCC_MSC:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class DLC(EventEmitter):
|
||||
class DLC(utils.EventEmitter):
|
||||
class State(enum.IntEnum):
|
||||
INIT = 0x00
|
||||
CONNECTING = 0x01
|
||||
@@ -749,7 +749,7 @@ class DLC(EventEmitter):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Multiplexer(EventEmitter):
|
||||
class Multiplexer(utils.EventEmitter):
|
||||
class Role(enum.IntEnum):
|
||||
INITIATOR = 0x00
|
||||
RESPONDER = 0x01
|
||||
@@ -845,7 +845,7 @@ class Multiplexer(EventEmitter):
|
||||
self.open_result.set_exception(
|
||||
core.ConnectionError(
|
||||
core.ConnectionError.CONNECTION_REFUSED,
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
PhysicalTransport.BR_EDR,
|
||||
self.l2cap_channel.connection.peer_address,
|
||||
'rfcomm',
|
||||
)
|
||||
@@ -1075,7 +1075,7 @@ class Client:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Server(EventEmitter):
|
||||
class Server(utils.EventEmitter):
|
||||
def __init__(
|
||||
self, device: Device, l2cap_mtu: int = RFCOMM_DEFAULT_L2CAP_MTU
|
||||
) -> None:
|
||||
|
||||
@@ -33,7 +33,7 @@ from bumble.core import (
|
||||
from bumble.hci import HCI_Object, name_or_number, key_with_value
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .device import Device, Connection
|
||||
from bumble.device import Device, Connection
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
@@ -41,26 +41,25 @@ from typing import (
|
||||
cast,
|
||||
)
|
||||
|
||||
from pyee import EventEmitter
|
||||
|
||||
from .colors import color
|
||||
from .hci import (
|
||||
from bumble.colors import color
|
||||
from bumble.hci import (
|
||||
Address,
|
||||
Role,
|
||||
HCI_LE_Enable_Encryption_Command,
|
||||
HCI_Object,
|
||||
key_with_value,
|
||||
)
|
||||
from .core import (
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
BT_CENTRAL_ROLE,
|
||||
BT_LE_TRANSPORT,
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
AdvertisingData,
|
||||
InvalidArgumentError,
|
||||
ProtocolError,
|
||||
name_or_number,
|
||||
)
|
||||
from .keys import PairingKeys
|
||||
from . import crypto
|
||||
from bumble.keys import PairingKeys
|
||||
from bumble import crypto
|
||||
from bumble import utils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Connection, Device
|
||||
@@ -857,7 +856,7 @@ class Session:
|
||||
initiator_io_capability: int,
|
||||
responder_io_capability: int,
|
||||
) -> None:
|
||||
if self.connection.transport == BT_BR_EDR_TRANSPORT:
|
||||
if self.connection.transport == PhysicalTransport.BR_EDR:
|
||||
self.pairing_method = PairingMethod.CTKD_OVER_CLASSIC
|
||||
return
|
||||
if (not self.mitm) and (auth_req & SMP_MITM_AUTHREQ == 0):
|
||||
@@ -900,7 +899,7 @@ class Session:
|
||||
|
||||
self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
|
||||
|
||||
self.connection.abort_on('disconnection', prompt())
|
||||
utils.cancel_on_event(self.connection, 'disconnection', prompt())
|
||||
|
||||
def prompt_user_for_numeric_comparison(
|
||||
self, code: int, next_steps: Callable[[], None]
|
||||
@@ -919,7 +918,7 @@ class Session:
|
||||
|
||||
self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
|
||||
|
||||
self.connection.abort_on('disconnection', prompt())
|
||||
utils.cancel_on_event(self.connection, 'disconnection', prompt())
|
||||
|
||||
def prompt_user_for_number(self, next_steps: Callable[[int], None]) -> None:
|
||||
async def prompt() -> None:
|
||||
@@ -936,7 +935,7 @@ class Session:
|
||||
logger.warning(f'exception while prompting: {error}')
|
||||
self.send_pairing_failed(SMP_PASSKEY_ENTRY_FAILED_ERROR)
|
||||
|
||||
self.connection.abort_on('disconnection', prompt())
|
||||
utils.cancel_on_event(self.connection, 'disconnection', prompt())
|
||||
|
||||
def display_passkey(self) -> None:
|
||||
# Generate random Passkey/PIN code
|
||||
@@ -951,7 +950,8 @@ class Session:
|
||||
logger.debug(f'TK from passkey = {self.tk.hex()}')
|
||||
|
||||
try:
|
||||
self.connection.abort_on(
|
||||
utils.cancel_on_event(
|
||||
self.connection,
|
||||
'disconnection',
|
||||
self.pairing_config.delegate.display_number(self.passkey, digits=6),
|
||||
)
|
||||
@@ -1050,7 +1050,7 @@ class Session:
|
||||
)
|
||||
|
||||
# Perform the next steps asynchronously in case we need to wait for input
|
||||
self.connection.abort_on('disconnection', next_steps())
|
||||
utils.cancel_on_event(self.connection, 'disconnection', next_steps())
|
||||
else:
|
||||
confirm_value = crypto.c1(
|
||||
self.tk,
|
||||
@@ -1170,11 +1170,11 @@ class Session:
|
||||
if self.is_initiator:
|
||||
# CTKD: Derive LTK from LinkKey
|
||||
if (
|
||||
self.connection.transport == BT_BR_EDR_TRANSPORT
|
||||
self.connection.transport == PhysicalTransport.BR_EDR
|
||||
and self.initiator_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG
|
||||
):
|
||||
self.ctkd_task = self.connection.abort_on(
|
||||
'disconnection', self.get_link_key_and_derive_ltk()
|
||||
self.ctkd_task = utils.cancel_on_event(
|
||||
self.connection, 'disconnection', self.get_link_key_and_derive_ltk()
|
||||
)
|
||||
elif not self.sc:
|
||||
# Distribute the LTK, EDIV and RAND
|
||||
@@ -1209,11 +1209,11 @@ class Session:
|
||||
else:
|
||||
# CTKD: Derive LTK from LinkKey
|
||||
if (
|
||||
self.connection.transport == BT_BR_EDR_TRANSPORT
|
||||
self.connection.transport == PhysicalTransport.BR_EDR
|
||||
and self.responder_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG
|
||||
):
|
||||
self.ctkd_task = self.connection.abort_on(
|
||||
'disconnection', self.get_link_key_and_derive_ltk()
|
||||
self.ctkd_task = utils.cancel_on_event(
|
||||
self.connection, 'disconnection', self.get_link_key_and_derive_ltk()
|
||||
)
|
||||
# Distribute the LTK, EDIV and RAND
|
||||
elif not self.sc:
|
||||
@@ -1248,7 +1248,7 @@ class Session:
|
||||
def compute_peer_expected_distributions(self, key_distribution_flags: int) -> None:
|
||||
# Set our expectations for what to wait for in the key distribution phase
|
||||
self.peer_expected_distributions = []
|
||||
if not self.sc and self.connection.transport == BT_LE_TRANSPORT:
|
||||
if not self.sc and self.connection.transport == PhysicalTransport.LE:
|
||||
if key_distribution_flags & SMP_ENC_KEY_DISTRIBUTION_FLAG != 0:
|
||||
self.peer_expected_distributions.append(
|
||||
SMP_Encryption_Information_Command
|
||||
@@ -1305,7 +1305,9 @@ class Session:
|
||||
|
||||
# Wait for the pairing process to finish
|
||||
assert self.pairing_result
|
||||
await self.connection.abort_on('disconnection', self.pairing_result)
|
||||
await utils.cancel_on_event(
|
||||
self.connection, 'disconnection', self.pairing_result
|
||||
)
|
||||
|
||||
def on_disconnection(self, _: int) -> None:
|
||||
self.connection.remove_listener('disconnection', self.on_disconnection)
|
||||
@@ -1323,7 +1325,7 @@ class Session:
|
||||
if self.is_initiator:
|
||||
self.distribute_keys()
|
||||
|
||||
self.connection.abort_on('disconnection', self.on_pairing())
|
||||
utils.cancel_on_event(self.connection, 'disconnection', self.on_pairing())
|
||||
|
||||
def on_connection_encryption_change(self) -> None:
|
||||
if self.connection.is_encrypted and not self.completed:
|
||||
@@ -1365,7 +1367,7 @@ class Session:
|
||||
keys = PairingKeys()
|
||||
keys.address_type = peer_address.address_type
|
||||
authenticated = self.pairing_method != PairingMethod.JUST_WORKS
|
||||
if self.sc or self.connection.transport == BT_BR_EDR_TRANSPORT:
|
||||
if self.sc or self.connection.transport == PhysicalTransport.BR_EDR:
|
||||
keys.ltk = PairingKeys.Key(value=self.ltk, authenticated=authenticated)
|
||||
else:
|
||||
our_ltk_key = PairingKeys.Key(
|
||||
@@ -1432,8 +1434,10 @@ class Session:
|
||||
def on_smp_pairing_request_command(
|
||||
self, command: SMP_Pairing_Request_Command
|
||||
) -> None:
|
||||
self.connection.abort_on(
|
||||
'disconnection', self.on_smp_pairing_request_command_async(command)
|
||||
utils.cancel_on_event(
|
||||
self.connection,
|
||||
'disconnection',
|
||||
self.on_smp_pairing_request_command_async(command),
|
||||
)
|
||||
|
||||
async def on_smp_pairing_request_command_async(
|
||||
@@ -1506,7 +1510,7 @@ class Session:
|
||||
# CTKD over BR/EDR should happen after the connection has been encrypted,
|
||||
# so when receiving pairing requests, responder should start distributing keys
|
||||
if (
|
||||
self.connection.transport == BT_BR_EDR_TRANSPORT
|
||||
self.connection.transport == PhysicalTransport.BR_EDR
|
||||
and self.connection.is_encrypted
|
||||
and self.is_responder
|
||||
and accepted
|
||||
@@ -1878,7 +1882,7 @@ class Session:
|
||||
self.wait_before_continuing = None
|
||||
self.send_pairing_dhkey_check_command()
|
||||
|
||||
self.connection.abort_on('disconnection', next_steps())
|
||||
utils.cancel_on_event(self.connection, 'disconnection', next_steps())
|
||||
else:
|
||||
self.send_pairing_dhkey_check_command()
|
||||
else:
|
||||
@@ -1922,7 +1926,7 @@ class Session:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Manager(EventEmitter):
|
||||
class Manager(utils.EventEmitter):
|
||||
'''
|
||||
Implements the Initiator and Responder roles of the Security Manager Protocol
|
||||
'''
|
||||
@@ -1950,7 +1954,9 @@ class Manager(EventEmitter):
|
||||
f'>>> Sending SMP Command on connection [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address}: {command}'
|
||||
)
|
||||
cid = SMP_BR_CID if connection.transport == BT_BR_EDR_TRANSPORT else SMP_CID
|
||||
cid = (
|
||||
SMP_BR_CID if connection.transport == PhysicalTransport.BR_EDR else SMP_CID
|
||||
)
|
||||
connection.send_l2cap_pdu(cid, bytes(command))
|
||||
|
||||
def on_smp_security_request_command(
|
||||
@@ -1975,7 +1981,7 @@ class Manager(EventEmitter):
|
||||
|
||||
# Look for a session with this connection, and create one if none exists
|
||||
if not (session := self.sessions.get(connection.handle)):
|
||||
if connection.role == BT_CENTRAL_ROLE:
|
||||
if connection.role == Role.CENTRAL:
|
||||
logger.warning('Remote starts pairing as Peripheral!')
|
||||
pairing_config = self.pairing_config_factory(connection)
|
||||
session = self.session_proxy(
|
||||
@@ -1995,7 +2001,7 @@ class Manager(EventEmitter):
|
||||
|
||||
async def pair(self, connection: Connection) -> None:
|
||||
# TODO: check if there's already a session for this connection
|
||||
if connection.role != BT_CENTRAL_ROLE:
|
||||
if connection.role != Role.CENTRAL:
|
||||
logger.warning('Start pairing as Peripheral!')
|
||||
pairing_config = self.pairing_config_factory(connection)
|
||||
session = self.session_proxy(
|
||||
|
||||
@@ -20,8 +20,13 @@ import logging
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from .common import Transport, AsyncPipeSink, SnoopingTransport, TransportSpecError
|
||||
from ..snoop import create_snooper
|
||||
from bumble.transport.common import (
|
||||
Transport,
|
||||
AsyncPipeSink,
|
||||
SnoopingTransport,
|
||||
TransportSpecError,
|
||||
)
|
||||
from bumble.snoop import create_snooper
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -108,80 +113,80 @@ async def _open_transport(scheme: str, spec: Optional[str]) -> Transport:
|
||||
# pylint: disable=too-many-return-statements
|
||||
|
||||
if scheme == 'serial' and spec:
|
||||
from .serial import open_serial_transport
|
||||
from bumble.transport.serial import open_serial_transport
|
||||
|
||||
return await open_serial_transport(spec)
|
||||
|
||||
if scheme == 'udp' and spec:
|
||||
from .udp import open_udp_transport
|
||||
from bumble.transport.udp import open_udp_transport
|
||||
|
||||
return await open_udp_transport(spec)
|
||||
|
||||
if scheme == 'tcp-client' and spec:
|
||||
from .tcp_client import open_tcp_client_transport
|
||||
from bumble.transport.tcp_client import open_tcp_client_transport
|
||||
|
||||
return await open_tcp_client_transport(spec)
|
||||
|
||||
if scheme == 'tcp-server' and spec:
|
||||
from .tcp_server import open_tcp_server_transport
|
||||
from bumble.transport.tcp_server import open_tcp_server_transport
|
||||
|
||||
return await open_tcp_server_transport(spec)
|
||||
|
||||
if scheme == 'ws-client' and spec:
|
||||
from .ws_client import open_ws_client_transport
|
||||
from bumble.transport.ws_client import open_ws_client_transport
|
||||
|
||||
return await open_ws_client_transport(spec)
|
||||
|
||||
if scheme == 'ws-server' and spec:
|
||||
from .ws_server import open_ws_server_transport
|
||||
from bumble.transport.ws_server import open_ws_server_transport
|
||||
|
||||
return await open_ws_server_transport(spec)
|
||||
|
||||
if scheme == 'pty':
|
||||
from .pty import open_pty_transport
|
||||
from bumble.transport.pty import open_pty_transport
|
||||
|
||||
return await open_pty_transport(spec)
|
||||
|
||||
if scheme == 'file':
|
||||
from .file import open_file_transport
|
||||
from bumble.transport.file import open_file_transport
|
||||
|
||||
assert spec is not None
|
||||
return await open_file_transport(spec)
|
||||
|
||||
if scheme == 'vhci':
|
||||
from .vhci import open_vhci_transport
|
||||
from bumble.transport.vhci import open_vhci_transport
|
||||
|
||||
return await open_vhci_transport(spec)
|
||||
|
||||
if scheme == 'hci-socket':
|
||||
from .hci_socket import open_hci_socket_transport
|
||||
from bumble.transport.hci_socket import open_hci_socket_transport
|
||||
|
||||
return await open_hci_socket_transport(spec)
|
||||
|
||||
if scheme == 'usb':
|
||||
from .usb import open_usb_transport
|
||||
from bumble.transport.usb import open_usb_transport
|
||||
|
||||
assert spec
|
||||
return await open_usb_transport(spec)
|
||||
|
||||
if scheme == 'pyusb':
|
||||
from .pyusb import open_pyusb_transport
|
||||
from bumble.transport.pyusb import open_pyusb_transport
|
||||
|
||||
assert spec
|
||||
return await open_pyusb_transport(spec)
|
||||
|
||||
if scheme == 'android-emulator':
|
||||
from .android_emulator import open_android_emulator_transport
|
||||
from bumble.transport.android_emulator import open_android_emulator_transport
|
||||
|
||||
return await open_android_emulator_transport(spec)
|
||||
|
||||
if scheme == 'android-netsim':
|
||||
from .android_netsim import open_android_netsim_transport
|
||||
from bumble.transport.android_netsim import open_android_netsim_transport
|
||||
|
||||
return await open_android_netsim_transport(spec)
|
||||
|
||||
if scheme == 'unix':
|
||||
from .unix import open_unix_client_transport
|
||||
from bumble.transport.unix import open_unix_client_transport
|
||||
|
||||
assert spec
|
||||
return await open_unix_client_transport(spec)
|
||||
@@ -204,8 +209,8 @@ async def open_transport_or_link(name: str) -> Transport:
|
||||
"""
|
||||
if name.startswith('link-relay:'):
|
||||
logger.warning('Link Relay has been deprecated.')
|
||||
from ..controller import Controller
|
||||
from ..link import RemoteLink # lazy import
|
||||
from bumble.controller import Controller
|
||||
from bumble.link import RemoteLink # lazy import
|
||||
|
||||
link = RemoteLink(name[11:])
|
||||
await link.wait_until_connected()
|
||||
|
||||
@@ -20,7 +20,7 @@ import grpc.aio
|
||||
|
||||
from typing import Optional, Union
|
||||
|
||||
from .common import (
|
||||
from bumble.transport.common import (
|
||||
PumpedTransport,
|
||||
PumpedPacketSource,
|
||||
PumpedPacketSink,
|
||||
@@ -29,9 +29,13 @@ from .common import (
|
||||
)
|
||||
|
||||
# pylint: disable=no-name-in-module
|
||||
from .grpc_protobuf.emulated_bluetooth_pb2_grpc import EmulatedBluetoothServiceStub
|
||||
from .grpc_protobuf.emulated_bluetooth_packets_pb2 import HCIPacket
|
||||
from .grpc_protobuf.emulated_bluetooth_vhci_pb2_grpc import VhciForwardingServiceStub
|
||||
from bumble.transport.grpc_protobuf.emulated_bluetooth_pb2_grpc import (
|
||||
EmulatedBluetoothServiceStub,
|
||||
)
|
||||
from bumble.transport.grpc_protobuf.emulated_bluetooth_packets_pb2 import HCIPacket
|
||||
from bumble.transport.grpc_protobuf.emulated_bluetooth_vhci_pb2_grpc import (
|
||||
VhciForwardingServiceStub,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -38,15 +38,18 @@ from bumble.transport.common import (
|
||||
)
|
||||
|
||||
# pylint: disable=no-name-in-module
|
||||
from .grpc_protobuf.netsim.packet_streamer_pb2_grpc import (
|
||||
from bumble.transport.grpc_protobuf.netsim.packet_streamer_pb2_grpc import (
|
||||
PacketStreamerStub,
|
||||
PacketStreamerServicer,
|
||||
add_PacketStreamerServicer_to_server,
|
||||
)
|
||||
from .grpc_protobuf.netsim.packet_streamer_pb2 import PacketRequest, PacketResponse
|
||||
from .grpc_protobuf.netsim.hci_packet_pb2 import HCIPacket
|
||||
from .grpc_protobuf.netsim.startup_pb2 import Chip, ChipInfo, DeviceInfo
|
||||
from .grpc_protobuf.netsim.common_pb2 import ChipKind
|
||||
from bumble.transport.grpc_protobuf.netsim.packet_streamer_pb2 import (
|
||||
PacketRequest,
|
||||
PacketResponse,
|
||||
)
|
||||
from bumble.transport.grpc_protobuf.netsim.hci_packet_pb2 import HCIPacket
|
||||
from bumble.transport.grpc_protobuf.netsim.startup_pb2 import Chip, ChipInfo, DeviceInfo
|
||||
from bumble.transport.grpc_protobuf.netsim.common_pb2 import ChipKind
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -139,6 +139,7 @@ class PacketParser:
|
||||
packet_type
|
||||
) or self.extended_packet_info.get(packet_type)
|
||||
if self.packet_info is None:
|
||||
self.reset()
|
||||
raise core.InvalidPacketError(
|
||||
f'invalid packet type {packet_type}'
|
||||
)
|
||||
@@ -302,7 +303,10 @@ class ParserSource(BaseSource):
|
||||
# -----------------------------------------------------------------------------
|
||||
class StreamPacketSource(asyncio.Protocol, ParserSource):
|
||||
def data_received(self, data: bytes) -> None:
|
||||
self.parser.feed_data(data)
|
||||
try:
|
||||
self.parser.feed_data(data)
|
||||
except core.InvalidPacketError:
|
||||
logger.warning("invalid packet, ignoring data")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -19,7 +19,7 @@ import asyncio
|
||||
import io
|
||||
import logging
|
||||
|
||||
from .common import Transport, StreamPacketSource, StreamPacketSink
|
||||
from bumble.transport.common import Transport, StreamPacketSource, StreamPacketSink
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
@@ -25,7 +25,7 @@ import collections
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from .common import Transport, ParserSource
|
||||
from bumble.transport.common import Transport, ParserSource
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -25,7 +25,7 @@ import logging
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from .common import Transport, StreamPacketSource, StreamPacketSink
|
||||
from bumble.transport.common import Transport, StreamPacketSource, StreamPacketSink
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
@@ -29,9 +29,9 @@ from usb.core import USBError
|
||||
from usb.util import CTRL_TYPE_CLASS, CTRL_RECIPIENT_OTHER
|
||||
from usb.legacy import REQ_SET_FEATURE, REQ_CLEAR_FEATURE, CLASS_HUB
|
||||
|
||||
from .common import Transport, ParserSource, TransportInitError
|
||||
from .. import hci
|
||||
from ..colors import color
|
||||
from bumble.transport.common import Transport, ParserSource, TransportInitError
|
||||
from bumble import hci
|
||||
from bumble.colors import color
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -19,7 +19,7 @@ import asyncio
|
||||
import logging
|
||||
import serial_asyncio
|
||||
|
||||
from .common import Transport, StreamPacketSource, StreamPacketSink
|
||||
from bumble.transport.common import Transport, StreamPacketSource, StreamPacketSink
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from .common import Transport, StreamPacketSource, StreamPacketSink
|
||||
from bumble.transport.common import Transport, StreamPacketSource, StreamPacketSink
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
@@ -20,7 +20,7 @@ import asyncio
|
||||
import logging
|
||||
import socket
|
||||
|
||||
from .common import Transport, StreamPacketSource
|
||||
from bumble.transport.common import Transport, StreamPacketSource
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from .common import Transport, ParserSource
|
||||
from bumble.transport.common import Transport, ParserSource
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from .common import Transport, StreamPacketSource, StreamPacketSink
|
||||
from bumble.transport.common import Transport, StreamPacketSource, StreamPacketSink
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
@@ -115,9 +115,7 @@ async def open_usb_transport(spec: str) -> Transport:
|
||||
self.acl_out = acl_out
|
||||
self.acl_out_transfer = device.getTransfer()
|
||||
self.acl_out_transfer_ready = asyncio.Semaphore(1)
|
||||
self.packets: asyncio.Queue[bytes] = (
|
||||
asyncio.Queue()
|
||||
) # Queue of packets waiting to be sent
|
||||
self.packets = asyncio.Queue[bytes]() # Queue of packets waiting to be sent
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.queue_task = None
|
||||
self.cancel_done = self.loop.create_future()
|
||||
|
||||
@@ -19,8 +19,8 @@ import logging
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from .common import Transport
|
||||
from .file import open_file_transport
|
||||
from bumble.transport.common import Transport
|
||||
from bumble.transport.file import open_file_transport
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
@@ -18,7 +18,12 @@
|
||||
import logging
|
||||
import websockets.client
|
||||
|
||||
from .common import PumpedPacketSource, PumpedPacketSink, PumpedTransport, Transport
|
||||
from bumble.transport.common import (
|
||||
PumpedPacketSource,
|
||||
PumpedPacketSink,
|
||||
PumpedTransport,
|
||||
Transport,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
import logging
|
||||
import websockets
|
||||
|
||||
from .common import Transport, ParserSource, PumpedPacketSink
|
||||
from bumble.transport.common import Transport, ParserSource, PumpedPacketSink
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
171
bumble/utils.py
171
bumble/utils.py
@@ -38,9 +38,10 @@ from typing import (
|
||||
)
|
||||
from typing_extensions import Self
|
||||
|
||||
from pyee import EventEmitter
|
||||
import pyee
|
||||
import pyee.asyncio
|
||||
|
||||
from .colors import color
|
||||
from bumble.colors import color
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -56,6 +57,48 @@ def setup_event_forwarding(emitter, forwarder, event_name):
|
||||
emitter.on(event_name, emit)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def wrap_async(function):
|
||||
"""
|
||||
Wraps the provided function in an async function.
|
||||
"""
|
||||
return functools.partial(async_call, function)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def deprecated(msg: str):
|
||||
"""
|
||||
Throw deprecation warning before execution.
|
||||
"""
|
||||
|
||||
def wrapper(function):
|
||||
@functools.wraps(function)
|
||||
def inner(*args, **kwargs):
|
||||
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
||||
return function(*args, **kwargs)
|
||||
|
||||
return inner
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def experimental(msg: str):
|
||||
"""
|
||||
Throws a future warning before execution.
|
||||
"""
|
||||
|
||||
def wrapper(function):
|
||||
@functools.wraps(function)
|
||||
def inner(*args, **kwargs):
|
||||
warnings.warn(msg, FutureWarning, stacklevel=2)
|
||||
return function(*args, **kwargs)
|
||||
|
||||
return inner
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def composite_listener(cls):
|
||||
"""
|
||||
@@ -113,21 +156,23 @@ class EventWatcher:
|
||||
```
|
||||
'''
|
||||
|
||||
handlers: List[Tuple[EventEmitter, str, Callable[..., Any]]]
|
||||
handlers: List[Tuple[pyee.EventEmitter, str, Callable[..., Any]]]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.handlers = []
|
||||
|
||||
@overload
|
||||
def on(
|
||||
self, emitter: EventEmitter, event: str
|
||||
self, emitter: pyee.EventEmitter, event: str
|
||||
) -> Callable[[_Handler], _Handler]: ...
|
||||
|
||||
@overload
|
||||
def on(self, emitter: EventEmitter, event: str, handler: _Handler) -> _Handler: ...
|
||||
def on(
|
||||
self, emitter: pyee.EventEmitter, event: str, handler: _Handler
|
||||
) -> _Handler: ...
|
||||
|
||||
def on(
|
||||
self, emitter: EventEmitter, event: str, handler: Optional[_Handler] = None
|
||||
self, emitter: pyee.EventEmitter, event: str, handler: Optional[_Handler] = None
|
||||
) -> Union[_Handler, Callable[[_Handler], _Handler]]:
|
||||
'''Watch an event until the context is closed.
|
||||
|
||||
@@ -147,16 +192,16 @@ class EventWatcher:
|
||||
|
||||
@overload
|
||||
def once(
|
||||
self, emitter: EventEmitter, event: str
|
||||
self, emitter: pyee.EventEmitter, event: str
|
||||
) -> Callable[[_Handler], _Handler]: ...
|
||||
|
||||
@overload
|
||||
def once(
|
||||
self, emitter: EventEmitter, event: str, handler: _Handler
|
||||
self, emitter: pyee.EventEmitter, event: str, handler: _Handler
|
||||
) -> _Handler: ...
|
||||
|
||||
def once(
|
||||
self, emitter: EventEmitter, event: str, handler: Optional[_Handler] = None
|
||||
self, emitter: pyee.EventEmitter, event: str, handler: Optional[_Handler] = None
|
||||
) -> Union[_Handler, Callable[[_Handler], _Handler]]:
|
||||
'''Watch an event for once.
|
||||
|
||||
@@ -184,38 +229,48 @@ class EventWatcher:
|
||||
_T = TypeVar('_T')
|
||||
|
||||
|
||||
class AbortableEventEmitter(EventEmitter):
|
||||
def abort_on(self, event: str, awaitable: Awaitable[_T]) -> Awaitable[_T]:
|
||||
"""
|
||||
Set a coroutine or future to abort when an event occur.
|
||||
"""
|
||||
future = asyncio.ensure_future(awaitable)
|
||||
if future.done():
|
||||
return future
|
||||
|
||||
def on_event(*_):
|
||||
if future.done():
|
||||
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)
|
||||
else:
|
||||
future.set_exception(asyncio.CancelledError(msg))
|
||||
|
||||
def on_done(_):
|
||||
self.remove_listener(event, on_event)
|
||||
|
||||
self.on(event, on_event)
|
||||
future.add_done_callback(on_done)
|
||||
def cancel_on_event(
|
||||
emitter: pyee.EventEmitter, event: str, awaitable: Awaitable[_T]
|
||||
) -> Awaitable[_T]:
|
||||
"""Set a coroutine or future to cancel when an event occur."""
|
||||
future = asyncio.ensure_future(awaitable)
|
||||
if future.done():
|
||||
return future
|
||||
|
||||
def on_event(*args, **kwargs) -> None:
|
||||
del args, kwargs
|
||||
if future.done():
|
||||
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)
|
||||
else:
|
||||
future.set_exception(asyncio.CancelledError(msg))
|
||||
|
||||
def on_done(_):
|
||||
emitter.remove_listener(event, on_event)
|
||||
|
||||
emitter.on(event, on_event)
|
||||
future.add_done_callback(on_done)
|
||||
return future
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class CompositeEventEmitter(AbortableEventEmitter):
|
||||
class EventEmitter(pyee.asyncio.AsyncIOEventEmitter):
|
||||
"""A Base EventEmitter for Bumble."""
|
||||
|
||||
@deprecated("Use `cancel_on_event` instead.")
|
||||
def abort_on(self, event: str, awaitable: Awaitable[_T]) -> Awaitable[_T]:
|
||||
"""Set a coroutine or future to abort when an event occur."""
|
||||
return cancel_on_event(self, event, awaitable)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class CompositeEventEmitter(EventEmitter):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._listener = None
|
||||
@@ -430,48 +485,6 @@ async def async_call(function, *args, **kwargs):
|
||||
return function(*args, **kwargs)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def wrap_async(function):
|
||||
"""
|
||||
Wraps the provided function in an async function.
|
||||
"""
|
||||
return functools.partial(async_call, function)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def deprecated(msg: str):
|
||||
"""
|
||||
Throw deprecation warning before execution.
|
||||
"""
|
||||
|
||||
def wrapper(function):
|
||||
@functools.wraps(function)
|
||||
def inner(*args, **kwargs):
|
||||
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
||||
return function(*args, **kwargs)
|
||||
|
||||
return inner
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def experimental(msg: str):
|
||||
"""
|
||||
Throws a future warning before execution.
|
||||
"""
|
||||
|
||||
def wrapper(function):
|
||||
@functools.wraps(function)
|
||||
def inner(*args, **kwargs):
|
||||
warnings.warn(msg, FutureWarning, stacklevel=2)
|
||||
return function(*args, **kwargs)
|
||||
|
||||
return inner
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class OpenIntEnum(enum.IntEnum):
|
||||
"""
|
||||
|
||||
202
docs/mkdocs/src/apps_and_tools/auracast.md
Normal file
202
docs/mkdocs/src/apps_and_tools/auracast.md
Normal file
@@ -0,0 +1,202 @@
|
||||
AURACAST TOOL
|
||||
=============
|
||||
|
||||
The "auracast" tool implements commands that implement broadcasting, receiving
|
||||
and controlling LE Audio broadcasts.
|
||||
|
||||
=== "Running as an installed package"
|
||||
```
|
||||
$ bumble-auracast
|
||||
```
|
||||
|
||||
=== "Running from source"
|
||||
```
|
||||
$ python3 apps/auracast.py <args>
|
||||
```
|
||||
|
||||
# Python Dependencies
|
||||
Try installing the optional `[auracast]` dependencies:
|
||||
|
||||
=== "From source"
|
||||
```bash
|
||||
$ python3 -m pip install ".[auracast]"
|
||||
```
|
||||
|
||||
=== "From PyPI"
|
||||
```bash
|
||||
$ python3 -m pip install "bumble[auracast]"
|
||||
```
|
||||
|
||||
## LC3
|
||||
The `auracast` app depends on the `lc3` python module, which is available
|
||||
either as PyPI module (currently only available for Linux x86_64).
|
||||
When installing Bumble with the optional `auracast` dependency, the `lc3`
|
||||
module will be installed from the `lc3py` PyPI package if available.
|
||||
If not, you will need to install it separately. This can be done with:
|
||||
```bash
|
||||
$ python3 -m pip install "git+https://github.com/google/liblc3.git"
|
||||
```
|
||||
|
||||
## SoundDevice
|
||||
The `sounddevice` module is required for audio output to the host's sound
|
||||
output device(s) and/or input from the host's input device(s).
|
||||
If not installed, the `auracast` app is still functional, but will be limited
|
||||
to non-device inputs and output (files, external processes, ...)
|
||||
|
||||
On macOS and Windows, the `sounddevice` module gets installed with the
|
||||
native PortAudio libraries included.
|
||||
|
||||
For Linux, however, PortAudio must be installed separately.
|
||||
This is typically done with a command like:
|
||||
```bash
|
||||
$ sudo apt install libportaudio2
|
||||
```
|
||||
|
||||
Visit the [sounddevice documentation](https://python-sounddevice.readthedocs.io/)
|
||||
for details.
|
||||
|
||||
|
||||
# General Usage
|
||||
```
|
||||
Usage: bumble-auracast [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
Options:
|
||||
--help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
assist Scan for broadcasts on behalf of an audio server
|
||||
pair Pair with an audio server
|
||||
receive Receive a broadcast source
|
||||
scan Scan for public broadcasts
|
||||
transmit Transmit a broadcast source
|
||||
```
|
||||
|
||||
Use `bumble-auracast <command> --help` to get more detailed usage information
|
||||
for a specific `<command>`.
|
||||
|
||||
## `assist`
|
||||
Act as a broadcast assistant.
|
||||
|
||||
Use `bumble-auracast assist --help` for details on the commands and options.
|
||||
|
||||
The assistant commands are:
|
||||
|
||||
### `monitor-state`
|
||||
Subscribe to the state characteristic and monitor changes.
|
||||
|
||||
### `add-source`
|
||||
Add a broadcast source. This will instruct the device to start
|
||||
receiving a broadcast.
|
||||
|
||||
### `modify-source`
|
||||
Modify a broadcast source.
|
||||
|
||||
### `remove-source`
|
||||
Remote a broadcast source.
|
||||
|
||||
## `pair`
|
||||
Pair with a device.
|
||||
|
||||
## `receive`
|
||||
Receive a broadcast source.
|
||||
|
||||
The `--output` option specifies where to send the decoded audio samples.
|
||||
The following outputs are supported:
|
||||
|
||||
### Sound Device
|
||||
The `--output` argument is either `device`, to send the audio to the hosts's default sound device, or `device:<DEVICE_ID>` where `<DEVICE_ID>`
|
||||
is the integer ID of one of the available sound devices.
|
||||
When invoked with `--output "device:?"`, a list of available devices and
|
||||
their IDs is printed out.
|
||||
|
||||
### Standard Output
|
||||
With `--output stdout`, the decoded audio samples are written to the
|
||||
standard output (currently always as float32 PCM samples)
|
||||
|
||||
### FFPlay
|
||||
With `--output ffplay`, the decoded audio samples are piped to `ffplay`
|
||||
in a child process. This option is only available if `ffplay` is a command that is available on the host.
|
||||
|
||||
### File
|
||||
With `--output <filename>` or `--output file:<filename>`, the decoded audio
|
||||
samples are written to a file (currently always as float32 PCM)
|
||||
|
||||
## `transmit`
|
||||
Broadcast an audio source as a transmitter.
|
||||
|
||||
The `--input` and `--input-format` options specify what audio input
|
||||
source to transmit.
|
||||
The following inputs are supported:
|
||||
|
||||
### Sound Device
|
||||
The `--input` argument is either `device`, to use the host's default sound
|
||||
device (typically a builtin microphone), or `device:<DEVICE_ID>` where
|
||||
`<DEVICE_ID>` is the integer ID of one of the available sound devices.
|
||||
When invoked with `--input "device:?"`, a list of available devices and their
|
||||
IDs is printed out.
|
||||
|
||||
### Standard Input
|
||||
With `--input stdout`, the audio samples are read from the standard input.
|
||||
(currently always as int16 PCM).
|
||||
|
||||
### File
|
||||
With `--input <filename>` or `--input file:<filename>`, the audio samples
|
||||
are read from a .wav or raw PCM file.
|
||||
|
||||
Use the `--input-format <FORMAT>` option to specify the format of the audio
|
||||
samples in raw PCM files. `<FORMAT>` is expressed as:
|
||||
`<sample-type>,<sample-rate>,<channels>`
|
||||
(the only supported <sample-type> currently is 'int16le' for 16 bit signed integers with little-endian byte order)
|
||||
|
||||
## `scan`
|
||||
Scan for public broadcasts.
|
||||
|
||||
A live display of the available broadcasts is displayed continuously.
|
||||
|
||||
# Compatibility With Some Products
|
||||
The `auracast` app has been tested for compatibility with a few products.
|
||||
The list is still very limited. Please let us know if there are products
|
||||
that are not working well, or if there are specific instructions that should
|
||||
be shared to allow better compatibiity with certain products.
|
||||
|
||||
## Transmitters
|
||||
|
||||
The `receive` command has been tested to successfully receive broadcasts from
|
||||
the following transmitters:
|
||||
|
||||
* JBL GO 4
|
||||
* Flairmesh FlooGoo FMA120
|
||||
* Eppfun AK3040Pro Max
|
||||
* HIGHGAZE BA-25T
|
||||
* Nexum Audio VOCE and USB dongle
|
||||
|
||||
## Receivers
|
||||
|
||||
### Pixel Buds Pro 2
|
||||
|
||||
The Pixel Buds Pro 2 can be used as a broadcast receiver, controlled by the
|
||||
`auracast assist` command, instructing the buds to receive a broadcast.
|
||||
|
||||
Use the `assist --command add-source` command to tell the buds to receive a
|
||||
broadcast.
|
||||
|
||||
Use the `assist --command monitor-state` command to monitor the current sync/receive
|
||||
state of the buds.
|
||||
|
||||
### JBL
|
||||
The JBL GO 4 and other JBL products that support the Auracast feature can be used
|
||||
as transmitters or receivers.
|
||||
|
||||
When running in receiver mode (pressing the Auracast button while not already playing),
|
||||
the JBL speaker will scan for broadcast advertisements with a specific manufacturer data.
|
||||
Use the `--manufacturer-data` option of the `transmit` command in order to include data
|
||||
that will let the speaker recognize the broadcast as a compatible source.
|
||||
|
||||
The manufacturer ID for JBL is 87.
|
||||
Using an option like `--manufacturer-data 87:00000000000000000000000000000000dffd` should work (tested on the
|
||||
JBL GO 4. The `dffd` value at the end of the payload may be different on other models?).
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* Nexum Audio VOCE and USB dongle
|
||||
@@ -24,7 +24,7 @@ from bumble.colors import color
|
||||
from bumble.device import Device
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.core import (
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
PhysicalTransport,
|
||||
BT_AVDTP_PROTOCOL_ID,
|
||||
BT_AUDIO_SINK_SERVICE,
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
@@ -165,7 +165,9 @@ async def main() -> None:
|
||||
# Connect to a peer
|
||||
target_address = sys.argv[3]
|
||||
print(f'=== Connecting to {target_address}...')
|
||||
connection = await device.connect(target_address, transport=BT_BR_EDR_TRANSPORT)
|
||||
connection = await device.connect(
|
||||
target_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
print(f'=== Connected to {connection.peer_address}!')
|
||||
|
||||
# Request authentication
|
||||
|
||||
@@ -23,7 +23,7 @@ from typing import Any, Dict
|
||||
|
||||
from bumble.device import Device
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.core import BT_BR_EDR_TRANSPORT
|
||||
from bumble.core import PhysicalTransport
|
||||
from bumble.avdtp import (
|
||||
AVDTP_AUDIO_MEDIA_TYPE,
|
||||
Protocol,
|
||||
@@ -145,7 +145,7 @@ async def main() -> None:
|
||||
target_address = sys.argv[4]
|
||||
print(f'=== Connecting to {target_address}...')
|
||||
connection = await device.connect(
|
||||
target_address, transport=BT_BR_EDR_TRANSPORT
|
||||
target_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
print(f'=== Connected to {connection.peer_address}!')
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import logging
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.core import BT_BR_EDR_TRANSPORT
|
||||
from bumble.core import PhysicalTransport
|
||||
from bumble.avdtp import (
|
||||
find_avdtp_service_with_connection,
|
||||
AVDTP_AUDIO_MEDIA_TYPE,
|
||||
@@ -146,7 +146,7 @@ async def main() -> None:
|
||||
target_address = sys.argv[4]
|
||||
print(f'=== Connecting to {target_address}...')
|
||||
connection = await device.connect(
|
||||
target_address, transport=BT_BR_EDR_TRANSPORT
|
||||
target_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
print(f'=== Connected to {connection.peer_address}!')
|
||||
|
||||
|
||||
215
examples/run_ancs_client.py
Normal file
215
examples/run_ancs_client.py
Normal file
@@ -0,0 +1,215 @@
|
||||
# Copyright 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
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
from bumble.colors import color
|
||||
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.transport import open_transport
|
||||
from bumble.profiles.ancs import (
|
||||
AncsClient,
|
||||
AppAttribute,
|
||||
AppAttributeId,
|
||||
EventFlags,
|
||||
EventId,
|
||||
Notification,
|
||||
NotificationAttributeId,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
_cached_app_names: dict[str, str] = {}
|
||||
_notification_queue = asyncio.Queue[Notification]()
|
||||
|
||||
|
||||
async def process_notifications(ancs_client: AncsClient):
|
||||
while True:
|
||||
notification = await _notification_queue.get()
|
||||
|
||||
prefix = " "
|
||||
if notification.event_id == EventId.NOTIFICATION_ADDED:
|
||||
print_color = "green"
|
||||
if notification.event_flags & EventFlags.PRE_EXISTING:
|
||||
prefix = " Existing "
|
||||
else:
|
||||
prefix = " New "
|
||||
elif notification.event_id == EventId.NOTIFICATION_REMOVED:
|
||||
print_color = "red"
|
||||
elif notification.event_id == EventId.NOTIFICATION_MODIFIED:
|
||||
print_color = "yellow"
|
||||
else:
|
||||
print_color = "white"
|
||||
|
||||
print(
|
||||
color(
|
||||
(
|
||||
f"[{notification.event_id.name}]{prefix}Notification "
|
||||
f"({notification.notification_uid}):"
|
||||
),
|
||||
print_color,
|
||||
)
|
||||
)
|
||||
print(color(" Event ID: ", "yellow"), notification.event_id.name)
|
||||
print(color(" Event Flags: ", "yellow"), notification.event_flags.name)
|
||||
print(color(" Category ID: ", "yellow"), notification.category_id.name)
|
||||
print(color(" Category Count:", "yellow"), notification.category_count)
|
||||
|
||||
if notification.event_id not in (
|
||||
EventId.NOTIFICATION_ADDED,
|
||||
EventId.NOTIFICATION_MODIFIED,
|
||||
):
|
||||
continue
|
||||
|
||||
requested_attributes = [
|
||||
NotificationAttributeId.APP_IDENTIFIER,
|
||||
NotificationAttributeId.TITLE,
|
||||
NotificationAttributeId.SUBTITLE,
|
||||
NotificationAttributeId.MESSAGE,
|
||||
NotificationAttributeId.DATE,
|
||||
]
|
||||
if notification.event_flags & EventFlags.NEGATIVE_ACTION:
|
||||
requested_attributes.append(NotificationAttributeId.NEGATIVE_ACTION_LABEL)
|
||||
if notification.event_flags & EventFlags.POSITIVE_ACTION:
|
||||
requested_attributes.append(NotificationAttributeId.POSITIVE_ACTION_LABEL)
|
||||
|
||||
attributes = await ancs_client.get_notification_attributes(
|
||||
notification.notification_uid, requested_attributes
|
||||
)
|
||||
max_attribute_name_width = max(
|
||||
(len(attribute.attribute_id.name) for attribute in attributes)
|
||||
)
|
||||
app_identifier = str(
|
||||
next(
|
||||
(
|
||||
attribute.value
|
||||
for attribute in attributes
|
||||
if attribute.attribute_id == NotificationAttributeId.APP_IDENTIFIER
|
||||
)
|
||||
)
|
||||
)
|
||||
if app_identifier not in _cached_app_names:
|
||||
app_attributes = await ancs_client.get_app_attributes(
|
||||
app_identifier, [AppAttributeId.DISPLAY_NAME]
|
||||
)
|
||||
_cached_app_names[app_identifier] = app_attributes[0].value
|
||||
app_name = _cached_app_names[app_identifier]
|
||||
|
||||
for attribute in attributes:
|
||||
padding = ' ' * (
|
||||
max_attribute_name_width - len(attribute.attribute_id.name)
|
||||
)
|
||||
suffix = (
|
||||
f" ({app_name})"
|
||||
if attribute.attribute_id == NotificationAttributeId.APP_IDENTIFIER
|
||||
else ""
|
||||
)
|
||||
print(
|
||||
color(f" {attribute.attribute_id.name}:{padding}", "blue"),
|
||||
f"{attribute.value}{suffix}",
|
||||
)
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def on_ancs_notification(notification: Notification) -> None:
|
||||
_notification_queue.put_nowait(notification)
|
||||
|
||||
|
||||
async def handle_command_client(
|
||||
ancs_client: AncsClient, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
||||
) -> None:
|
||||
while True:
|
||||
command = (await reader.readline()).decode("utf-8").strip()
|
||||
|
||||
try:
|
||||
command_name, command_args = command.split(" ", 1)
|
||||
if command_name == "+":
|
||||
notification_uid = int(command_args)
|
||||
await ancs_client.perform_positive_action(notification_uid)
|
||||
elif command_name == "-":
|
||||
notification_uid = int(command_args)
|
||||
await ancs_client.perform_negative_action(notification_uid)
|
||||
else:
|
||||
writer.write(f"unknown command {command_name}".encode("utf-8"))
|
||||
except Exception as error:
|
||||
writer.write(f"ERROR: {error}\n".encode("utf-8"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def main() -> None:
|
||||
if len(sys.argv) < 3:
|
||||
print(
|
||||
'Usage: run_ancs_client.py <device-config> <transport-spec> '
|
||||
'<bluetooth-address> <mtu>'
|
||||
)
|
||||
print('example: run_ancs_client.py device1.json usb:0 E1:CA:72:48:C4:E8 512')
|
||||
return
|
||||
device_config, transport_spec, bluetooth_address, mtu = sys.argv[1:]
|
||||
|
||||
print('<<< connecting to HCI...')
|
||||
async with await open_transport(transport_spec) as hci_transport:
|
||||
print('<<< connected')
|
||||
|
||||
# Create a device to manage the host, with a custom listener
|
||||
device = Device.from_config_file_with_hci(
|
||||
device_config, hci_transport.source, hci_transport.sink
|
||||
)
|
||||
await device.power_on()
|
||||
|
||||
# Connect to the peer
|
||||
print(f'=== Connecting to {bluetooth_address}...')
|
||||
connection = await device.connect(bluetooth_address)
|
||||
print(f'=== Connected: {connection}')
|
||||
|
||||
await connection.encrypt()
|
||||
|
||||
peer = Peer(connection)
|
||||
mtu_int = int(mtu)
|
||||
if mtu_int:
|
||||
new_mtu = await peer.request_mtu(mtu_int)
|
||||
print(f'ATT MTU = {new_mtu}')
|
||||
ancs_client = await AncsClient.for_peer(peer)
|
||||
if ancs_client is None:
|
||||
print("!!! no ANCS service found")
|
||||
return
|
||||
await ancs_client.start()
|
||||
|
||||
print('Subscribing to updates')
|
||||
ancs_client.on("notification", on_ancs_notification)
|
||||
|
||||
# Process all notifications in a task.
|
||||
notification_processing_task = asyncio.create_task(
|
||||
process_notifications(ancs_client)
|
||||
)
|
||||
|
||||
# Accept a TCP connection to handle commands.
|
||||
tcp_server = await asyncio.start_server(
|
||||
lambda reader, writer: handle_command_client(ancs_client, reader, writer),
|
||||
'127.0.0.1',
|
||||
9000,
|
||||
)
|
||||
print("Accepting command client on port 9000")
|
||||
async with tcp_server:
|
||||
await tcp_server.serve_forever()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
asyncio.run(main())
|
||||
@@ -25,7 +25,7 @@ import websockets
|
||||
|
||||
from bumble.device import Device
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.core import BT_BR_EDR_TRANSPORT
|
||||
from bumble.core import PhysicalTransport
|
||||
from bumble import avc
|
||||
from bumble import avrcp
|
||||
from bumble import avdtp
|
||||
@@ -379,7 +379,7 @@ async def main() -> None:
|
||||
target_address = sys.argv[4]
|
||||
print(f'=== Connecting to {target_address}...')
|
||||
connection = await device.connect(
|
||||
target_address, transport=BT_BR_EDR_TRANSPORT
|
||||
target_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
print(f'=== Connected to {connection.peer_address}!')
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ async def main() -> None:
|
||||
|
||||
print(f'<<< Connecting to {target_address}')
|
||||
connection = await device.connect(
|
||||
target_address, transport=core.BT_LE_TRANSPORT
|
||||
target_address, transport=core.PhysicalTransport.LE
|
||||
)
|
||||
print('<<< ACL Connected')
|
||||
if not (await device.get_long_term_key(connection.handle, b'', 0)):
|
||||
|
||||
@@ -19,12 +19,8 @@ import asyncio
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
from bumble.device import (
|
||||
Device,
|
||||
Connection,
|
||||
AdvertisingParameters,
|
||||
AdvertisingEventProperties,
|
||||
)
|
||||
from bumble import utils
|
||||
from bumble.device import Device, Connection
|
||||
from bumble.hci import (
|
||||
OwnAddressType,
|
||||
)
|
||||
@@ -79,7 +75,9 @@ async def main() -> None:
|
||||
def on_cis_request(
|
||||
connection: Connection, cis_handle: int, _cig_id: int, _cis_id: int
|
||||
):
|
||||
connection.abort_on('disconnection', devices[0].accept_cis_request(cis_handle))
|
||||
utils.cancel_on_event(
|
||||
connection, 'disconnection', devices[0].accept_cis_request(cis_handle)
|
||||
)
|
||||
|
||||
devices[0].on('cis_request', on_cis_request)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ from bumble.colors import color
|
||||
|
||||
from bumble.device import Device
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.core import BT_BR_EDR_TRANSPORT, BT_L2CAP_PROTOCOL_ID, CommandTimeoutError
|
||||
from bumble.core import PhysicalTransport, BT_L2CAP_PROTOCOL_ID, CommandTimeoutError
|
||||
from bumble.sdp import (
|
||||
Client as SDP_Client,
|
||||
SDP_PUBLIC_BROWSE_ROOT,
|
||||
@@ -57,7 +57,7 @@ async def main() -> None:
|
||||
print(f'=== Connecting to {target_address}...')
|
||||
try:
|
||||
connection = await device.connect(
|
||||
target_address, transport=BT_BR_EDR_TRANSPORT
|
||||
target_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
except CommandTimeoutError:
|
||||
print('!!! Connection timed out')
|
||||
|
||||
@@ -16,11 +16,10 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
from bumble.core import BT_BR_EDR_TRANSPORT
|
||||
from bumble.core import PhysicalTransport
|
||||
from bumble.device import Device, ScoLink
|
||||
from bumble.hci import HCI_Enhanced_Setup_Synchronous_Connection_Command
|
||||
from bumble.hfp import DefaultCodecParameters, ESCO_PARAMETERS
|
||||
@@ -61,7 +60,9 @@ async def main() -> None:
|
||||
|
||||
connections = await asyncio.gather(
|
||||
devices[0].accept(devices[1].public_address),
|
||||
devices[1].connect(devices[0].public_address, transport=BT_BR_EDR_TRANSPORT),
|
||||
devices[1].connect(
|
||||
devices[0].public_address, transport=PhysicalTransport.BR_EDR
|
||||
),
|
||||
)
|
||||
|
||||
def on_sco(sco_link: ScoLink):
|
||||
|
||||
@@ -28,9 +28,7 @@ import websockets
|
||||
import bumble.core
|
||||
from bumble.device import Device, ScoLink
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.core import (
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
)
|
||||
from bumble.core import PhysicalTransport
|
||||
from bumble import hci, rfcomm, hfp
|
||||
|
||||
|
||||
@@ -234,7 +232,7 @@ async def main() -> None:
|
||||
target_address = sys.argv[3]
|
||||
print(f'=== Connecting to {target_address}...')
|
||||
connection = await device.connect(
|
||||
target_address, transport=BT_BR_EDR_TRANSPORT
|
||||
target_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
print(f'=== Connected to {connection.peer_address}!')
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import websockets
|
||||
import functools
|
||||
from typing import Optional
|
||||
|
||||
from bumble import utils
|
||||
from bumble import rfcomm
|
||||
from bumble import hci
|
||||
from bumble.device import Device, Connection
|
||||
@@ -60,7 +61,8 @@ def on_dlc(dlc: rfcomm.DLC, configuration: hfp.HfConfiguration):
|
||||
else:
|
||||
raise RuntimeError("unknown active codec")
|
||||
|
||||
connection.abort_on(
|
||||
utils.cancel_on_event(
|
||||
connection,
|
||||
'disconnection',
|
||||
connection.device.send_command(
|
||||
hci.HCI_Enhanced_Accept_Synchronous_Connection_Request_Command(
|
||||
|
||||
@@ -26,7 +26,7 @@ import struct
|
||||
from bumble.device import Device
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.core import (
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
PhysicalTransport,
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
BT_HUMAN_INTERFACE_DEVICE_SERVICE,
|
||||
BT_HIDP_PROTOCOL_ID,
|
||||
@@ -721,7 +721,7 @@ async def main() -> None:
|
||||
elif choice == '9':
|
||||
hid_host_bd_addr = str(hid_device.remote_device_bd_address)
|
||||
connection = await device.connect(
|
||||
hid_host_bd_addr, transport=BT_BR_EDR_TRANSPORT
|
||||
hid_host_bd_addr, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
await connection.authenticate()
|
||||
await connection.encrypt()
|
||||
|
||||
@@ -26,7 +26,7 @@ from bumble.device import Device
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.core import (
|
||||
BT_HUMAN_INTERFACE_DEVICE_SERVICE,
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
PhysicalTransport,
|
||||
)
|
||||
from bumble.hci import Address
|
||||
from bumble.hid import Host, Message
|
||||
@@ -349,7 +349,9 @@ async def main() -> None:
|
||||
# Connect to a peer
|
||||
target_address = sys.argv[3]
|
||||
print(f'=== Connecting to {target_address}...')
|
||||
connection = await device.connect(target_address, transport=BT_BR_EDR_TRANSPORT)
|
||||
connection = await device.connect(
|
||||
target_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
print(f'=== Connected to {connection.peer_address}!')
|
||||
|
||||
# Request authentication
|
||||
@@ -519,10 +521,10 @@ async def main() -> None:
|
||||
|
||||
elif choice == '13':
|
||||
peer_address = Address.from_string_for_transport(
|
||||
target_address, transport=BT_BR_EDR_TRANSPORT
|
||||
target_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
connection = device.find_connection_by_bd_addr(
|
||||
peer_address, transport=BT_BR_EDR_TRANSPORT
|
||||
peer_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
if connection is not None:
|
||||
await connection.disconnect()
|
||||
@@ -538,7 +540,7 @@ async def main() -> None:
|
||||
|
||||
elif choice == '15':
|
||||
connection = await device.connect(
|
||||
target_address, transport=BT_BR_EDR_TRANSPORT
|
||||
target_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
await connection.authenticate()
|
||||
await connection.encrypt()
|
||||
|
||||
@@ -22,6 +22,7 @@ import os
|
||||
import websockets
|
||||
import json
|
||||
|
||||
from bumble import utils
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import (
|
||||
Device,
|
||||
@@ -169,7 +170,7 @@ async def main() -> None:
|
||||
mcp.on('track_position', on_track_position)
|
||||
await mcp.subscribe_characteristics()
|
||||
|
||||
connection.abort_on('disconnection', on_connection_async())
|
||||
utils.cancel_on_event(connection, 'disconnection', on_connection_async())
|
||||
|
||||
device.on('connection', on_connection)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ from bumble.transport import open_transport_or_link
|
||||
from bumble.core import (
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
BT_RFCOMM_PROTOCOL_ID,
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
PhysicalTransport,
|
||||
)
|
||||
from bumble.rfcomm import Client
|
||||
from bumble.sdp import (
|
||||
@@ -191,7 +191,9 @@ async def main() -> None:
|
||||
# Connect to a peer
|
||||
target_address = sys.argv[3]
|
||||
print(f'=== Connecting to {target_address}...')
|
||||
connection = await device.connect(target_address, transport=BT_BR_EDR_TRANSPORT)
|
||||
connection = await device.connect(
|
||||
target_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
print(f'=== Connected to {connection.peer_address}!')
|
||||
|
||||
channel_str = sys.argv[4]
|
||||
|
||||
@@ -21,7 +21,7 @@ import os
|
||||
import pytest
|
||||
|
||||
from bumble.controller import Controller
|
||||
from bumble.core import BT_BR_EDR_TRANSPORT
|
||||
from bumble.core import PhysicalTransport
|
||||
from bumble.link import LocalLink
|
||||
from bumble.device import Device
|
||||
from bumble.host import Host
|
||||
@@ -106,7 +106,7 @@ async def test_self_connection():
|
||||
# Connect the two devices
|
||||
await asyncio.gather(
|
||||
two_devices.devices[0].connect(
|
||||
two_devices.devices[1].public_address, transport=BT_BR_EDR_TRANSPORT
|
||||
two_devices.devices[1].public_address, transport=PhysicalTransport.BR_EDR
|
||||
),
|
||||
two_devices.devices[1].accept(two_devices.devices[0].public_address),
|
||||
)
|
||||
@@ -190,7 +190,7 @@ async def test_source_sink_1():
|
||||
async def make_connection():
|
||||
connections = await asyncio.gather(
|
||||
two_devices.devices[0].connect(
|
||||
two_devices.devices[1].public_address, BT_BR_EDR_TRANSPORT
|
||||
two_devices.devices[1].public_address, PhysicalTransport.BR_EDR
|
||||
),
|
||||
two_devices.devices[1].accept(two_devices.devices[0].public_address),
|
||||
)
|
||||
|
||||
@@ -70,7 +70,7 @@ class TwoDevices:
|
||||
|
||||
self.connections = await asyncio.gather(
|
||||
self.devices[0].connect(
|
||||
self.devices[1].public_address, core.BT_BR_EDR_TRANSPORT
|
||||
self.devices[1].public_address, core.PhysicalTransport.BR_EDR
|
||||
),
|
||||
self.devices[1].accept(self.devices[0].public_address),
|
||||
)
|
||||
|
||||
@@ -22,9 +22,7 @@ import os
|
||||
import pytest
|
||||
|
||||
from bumble.core import (
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
BT_LE_TRANSPORT,
|
||||
BT_PERIPHERAL_ROLE,
|
||||
PhysicalTransport,
|
||||
ConnectionParameters,
|
||||
)
|
||||
from bumble.device import (
|
||||
@@ -43,6 +41,7 @@ from bumble.hci import (
|
||||
HCI_CONNECTION_FAILED_TO_BE_ESTABLISHED_ERROR,
|
||||
Address,
|
||||
OwnAddressType,
|
||||
Role,
|
||||
HCI_Command_Complete_Event,
|
||||
HCI_Command_Status_Event,
|
||||
HCI_Connection_Complete_Event,
|
||||
@@ -50,6 +49,7 @@ from bumble.hci import (
|
||||
HCI_Error,
|
||||
HCI_Packet,
|
||||
)
|
||||
from bumble import utils
|
||||
from bumble import gatt
|
||||
|
||||
from .test_utils import TwoDevices, async_barrier
|
||||
@@ -229,10 +229,10 @@ async def test_device_connect_parallel():
|
||||
[c01, c02, a10, a20] = await asyncio.gather(
|
||||
*[
|
||||
asyncio.create_task(
|
||||
d0.connect(d1.public_address, transport=BT_BR_EDR_TRANSPORT)
|
||||
d0.connect(d1.public_address, transport=PhysicalTransport.BR_EDR)
|
||||
),
|
||||
asyncio.create_task(
|
||||
d0.connect(d2.public_address, transport=BT_BR_EDR_TRANSPORT)
|
||||
d0.connect(d2.public_address, transport=PhysicalTransport.BR_EDR)
|
||||
),
|
||||
d1_accept_task,
|
||||
d2_accept_task,
|
||||
@@ -252,7 +252,7 @@ async def test_device_connect_parallel():
|
||||
@pytest.mark.asyncio
|
||||
async def test_flush():
|
||||
d0 = Device(host=Host(None, None))
|
||||
task = d0.abort_on('flush', asyncio.sleep(10000))
|
||||
task = utils.cancel_on_event(d0, 'flush', asyncio.sleep(10000))
|
||||
await d0.host.flush()
|
||||
try:
|
||||
await task
|
||||
@@ -291,11 +291,11 @@ async def test_legacy_advertising_disconnection(auto_restart):
|
||||
await device.start_advertising(auto_restart=auto_restart)
|
||||
device.on_connection(
|
||||
0x0001,
|
||||
BT_LE_TRANSPORT,
|
||||
PhysicalTransport.LE,
|
||||
peer_address,
|
||||
None,
|
||||
None,
|
||||
BT_PERIPHERAL_ROLE,
|
||||
Role.PERIPHERAL,
|
||||
ConnectionParameters(0, 0, 0),
|
||||
)
|
||||
|
||||
@@ -349,11 +349,11 @@ async def test_extended_advertising_connection(own_address_type):
|
||||
)
|
||||
device.on_connection(
|
||||
0x0001,
|
||||
BT_LE_TRANSPORT,
|
||||
PhysicalTransport.LE,
|
||||
peer_address,
|
||||
None,
|
||||
None,
|
||||
BT_PERIPHERAL_ROLE,
|
||||
Role.PERIPHERAL,
|
||||
ConnectionParameters(0, 0, 0),
|
||||
)
|
||||
device.on_advertising_set_termination(
|
||||
@@ -393,11 +393,11 @@ async def test_extended_advertising_connection_out_of_order(own_address_type):
|
||||
)
|
||||
device.on_connection(
|
||||
0x0001,
|
||||
BT_LE_TRANSPORT,
|
||||
PhysicalTransport.LE,
|
||||
Address('F0:F1:F2:F3:F4:F5'),
|
||||
None,
|
||||
None,
|
||||
BT_PERIPHERAL_ROLE,
|
||||
Role.PERIPHERAL,
|
||||
ConnectionParameters(0, 0, 0),
|
||||
)
|
||||
|
||||
@@ -483,8 +483,8 @@ async def test_cis():
|
||||
_cig_id: int,
|
||||
_cis_id: int,
|
||||
):
|
||||
acl_connection.abort_on(
|
||||
'disconnection', devices[1].accept_cis_request(cis_handle)
|
||||
utils.cancel_on_event(
|
||||
acl_connection, 'disconnection', devices[1].accept_cis_request(cis_handle)
|
||||
)
|
||||
peripheral_cis_futures[cis_handle] = asyncio.get_running_loop().create_future()
|
||||
|
||||
|
||||
@@ -522,7 +522,7 @@ async def test_sco_setup():
|
||||
|
||||
connections = await asyncio.gather(
|
||||
devices[0].connect(
|
||||
devices[1].public_address, transport=core.BT_BR_EDR_TRANSPORT
|
||||
devices[1].public_address, transport=core.PhysicalTransport.BR_EDR
|
||||
),
|
||||
devices[1].accept(devices[0].public_address),
|
||||
)
|
||||
|
||||
@@ -24,7 +24,7 @@ import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from bumble.controller import Controller
|
||||
from bumble.core import BT_BR_EDR_TRANSPORT, BT_PERIPHERAL_ROLE, BT_CENTRAL_ROLE
|
||||
from bumble.core import PhysicalTransport
|
||||
from bumble.link import LocalLink
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.host import Host
|
||||
@@ -39,6 +39,7 @@ from bumble.smp import (
|
||||
)
|
||||
from bumble.core import ProtocolError
|
||||
from bumble.keys import PairingKeys
|
||||
from bumble.hci import Role
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -111,7 +112,7 @@ async def test_self_connection():
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
'responder_role,',
|
||||
(BT_CENTRAL_ROLE, BT_PERIPHERAL_ROLE),
|
||||
(Role.CENTRAL, Role.PERIPHERAL),
|
||||
)
|
||||
async def test_self_classic_connection(responder_role):
|
||||
# Create two devices, each with a controller, attached to the same link
|
||||
@@ -136,7 +137,7 @@ async def test_self_classic_connection(responder_role):
|
||||
# Connect the two devices
|
||||
await asyncio.gather(
|
||||
two_devices.devices[0].connect(
|
||||
two_devices.devices[1].public_address, transport=BT_BR_EDR_TRANSPORT
|
||||
two_devices.devices[1].public_address, transport=PhysicalTransport.BR_EDR
|
||||
),
|
||||
two_devices.devices[1].accept(
|
||||
two_devices.devices[0].public_address, responder_role
|
||||
@@ -506,7 +507,7 @@ async def test_self_smp_over_classic():
|
||||
# Connect the two devices
|
||||
await asyncio.gather(
|
||||
two_devices.devices[0].connect(
|
||||
two_devices.devices[1].public_address, transport=BT_BR_EDR_TRANSPORT
|
||||
two_devices.devices[1].public_address, transport=PhysicalTransport.BR_EDR
|
||||
),
|
||||
two_devices.devices[1].accept(two_devices.devices[0].public_address),
|
||||
)
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import pyee
|
||||
|
||||
from bumble import utils
|
||||
from bumble.device import Device
|
||||
from bumble.hci import HCI_Reset_Command
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Scanner(pyee.EventEmitter):
|
||||
class Scanner(utils.EventEmitter):
|
||||
"""
|
||||
Scanner web app
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import enum
|
||||
import logging
|
||||
from typing import Dict, List
|
||||
|
||||
from bumble.core import BT_BR_EDR_TRANSPORT, CommandTimeoutError
|
||||
from bumble.core import PhysicalTransport, CommandTimeoutError
|
||||
from bumble.device import Device, DeviceConfiguration
|
||||
from bumble.pairing import PairingConfig
|
||||
from bumble.sdp import ServiceAttribute
|
||||
@@ -229,7 +229,9 @@ class Speaker:
|
||||
async def connect(self, address):
|
||||
# Connect to the source
|
||||
print(f'=== Connecting to {address}...')
|
||||
connection = await self.device.connect(address, transport=BT_BR_EDR_TRANSPORT)
|
||||
connection = await self.device.connect(
|
||||
address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
print(f'=== Connected to {connection.peer_address}')
|
||||
|
||||
# Request authentication
|
||||
|
||||
Reference in New Issue
Block a user