forked from auracaster/bumble_mirror
Compare commits
30 Commits
gbg/auraca
...
gbg/bt-ben
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab60b42b85 | ||
|
|
febed8179b | ||
|
|
1bd83273e8 | ||
|
|
5e9fc89f80 | ||
|
|
2686663eb2 | ||
|
|
55801bc2ca | ||
|
|
6cecc16519 | ||
|
|
a57cf13e2e | ||
|
|
58f153afc4 | ||
|
|
7569da37e4 | ||
|
|
a8019a70da | ||
|
|
685f1dc43e | ||
|
|
220b3b0236 | ||
|
|
3495eb52ba | ||
|
|
1f7a1401eb | ||
|
|
ce2b02b62a | ||
|
|
5e55c0e358 | ||
|
|
ebeb0dc9f1 | ||
|
|
776bdae519 | ||
|
|
b2d9541f8f | ||
|
|
637224d5bc | ||
|
|
92ab171013 | ||
|
|
592475e2ed | ||
|
|
12bcdb7770 | ||
|
|
7a58f36020 | ||
|
|
ed0eb912c5 | ||
|
|
752ce6c830 | ||
|
|
82d825071c | ||
|
|
4befc5bbae | ||
|
|
da029a1749 |
@@ -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:
|
||||
@@ -514,14 +523,19 @@ async def run_assist(
|
||||
return
|
||||
|
||||
# Subscribe to and read the broadcast receive state characteristics
|
||||
def on_broadcast_receive_state_update(
|
||||
value: bass.BroadcastReceiveState, index: int
|
||||
) -> None:
|
||||
print(
|
||||
f"{color(f'Broadcast Receive State Update [{index}]:', 'green')} {value}"
|
||||
)
|
||||
|
||||
for i, broadcast_receive_state in enumerate(
|
||||
bass_client.broadcast_receive_states
|
||||
):
|
||||
try:
|
||||
await broadcast_receive_state.subscribe(
|
||||
lambda value, i=i: print(
|
||||
f"{color(f'Broadcast Receive State Update [{i}]:', 'green')} {value}"
|
||||
)
|
||||
functools.partial(on_broadcast_receive_state_update, index=i)
|
||||
)
|
||||
except core.ProtocolError as error:
|
||||
print(
|
||||
@@ -697,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()
|
||||
@@ -725,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
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -939,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
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -1083,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'))
|
||||
@@ -1289,8 +1291,10 @@ class Central(Connection.Listener):
|
||||
logging.info(color('### Connected', 'cyan'))
|
||||
self.connection.listener = self
|
||||
print_connection(self.connection)
|
||||
phy = await self.connection.get_phy()
|
||||
print_connection_phy(phy)
|
||||
|
||||
if not self.classic:
|
||||
phy = await self.connection.get_phy()
|
||||
print_connection_phy(phy)
|
||||
|
||||
# Switch roles if needed.
|
||||
if self.role_switch:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -234,7 +234,7 @@ class GattlinkNodeBridge(GattlinkL2capEndpoint, Device.Listener):
|
||||
Characteristic.WRITEABLE,
|
||||
CharacteristicValue(write=self.on_rx_write),
|
||||
)
|
||||
self.tx_characteristic = Characteristic(
|
||||
self.tx_characteristic: Characteristic[bytes] = Characteristic(
|
||||
GG_GATTLINK_TX_CHARACTERISTIC_UUID,
|
||||
Characteristic.Properties.NOTIFY,
|
||||
Characteristic.READABLE,
|
||||
|
||||
@@ -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 [
|
||||
|
||||
103
bumble/att.py
103
bumble/att.py
@@ -29,27 +29,32 @@ import functools
|
||||
import inspect
|
||||
import struct
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Generic,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from pyee import EventEmitter
|
||||
|
||||
from bumble import utils
|
||||
from bumble.core import UUID, name_or_number, ProtocolError
|
||||
from bumble.core import UUID, name_or_number, InvalidOperationError, ProtocolError
|
||||
from bumble.hci import HCI_Object, key_with_value
|
||||
from bumble.colors import color
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Typing
|
||||
# -----------------------------------------------------------------------------
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Connection
|
||||
|
||||
_T = TypeVar('_T')
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -217,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',
|
||||
@@ -227,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}'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -748,7 +761,7 @@ class ATT_Handle_Value_Confirmation(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class AttributeValue:
|
||||
class AttributeValue(Generic[_T]):
|
||||
'''
|
||||
Attribute value where reading and/or writing is delegated to functions
|
||||
passed as arguments to the constructor.
|
||||
@@ -757,33 +770,34 @@ class AttributeValue:
|
||||
def __init__(
|
||||
self,
|
||||
read: Union[
|
||||
Callable[[Optional[Connection]], Any],
|
||||
Callable[[Optional[Connection]], Awaitable[Any]],
|
||||
Callable[[Optional[Connection]], _T],
|
||||
Callable[[Optional[Connection]], Awaitable[_T]],
|
||||
None,
|
||||
] = None,
|
||||
write: Union[
|
||||
Callable[[Optional[Connection], Any], None],
|
||||
Callable[[Optional[Connection], Any], Awaitable[None]],
|
||||
Callable[[Optional[Connection], _T], None],
|
||||
Callable[[Optional[Connection], _T], Awaitable[None]],
|
||||
None,
|
||||
] = None,
|
||||
):
|
||||
self._read = read
|
||||
self._write = write
|
||||
|
||||
def read(self, connection: Optional[Connection]) -> Union[bytes, Awaitable[bytes]]:
|
||||
return self._read(connection) if self._read else b''
|
||||
def read(self, connection: Optional[Connection]) -> Union[_T, Awaitable[_T]]:
|
||||
if self._read is None:
|
||||
raise InvalidOperationError('AttributeValue has no read function')
|
||||
return self._read(connection)
|
||||
|
||||
def write(
|
||||
self, connection: Optional[Connection], value: bytes
|
||||
self, connection: Optional[Connection], value: _T
|
||||
) -> Union[Awaitable[None], None]:
|
||||
if self._write:
|
||||
return self._write(connection, value)
|
||||
|
||||
return None
|
||||
if self._write is None:
|
||||
raise InvalidOperationError('AttributeValue has no write function')
|
||||
return self._write(connection, value)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Attribute(EventEmitter):
|
||||
class Attribute(utils.EventEmitter, Generic[_T]):
|
||||
class Permissions(enum.IntFlag):
|
||||
READABLE = 0x01
|
||||
WRITEABLE = 0x02
|
||||
@@ -822,15 +836,15 @@ class Attribute(EventEmitter):
|
||||
READ_REQUIRES_AUTHORIZATION = Permissions.READ_REQUIRES_AUTHORIZATION
|
||||
WRITE_REQUIRES_AUTHORIZATION = Permissions.WRITE_REQUIRES_AUTHORIZATION
|
||||
|
||||
value: Any
|
||||
value: Union[AttributeValue[_T], _T, None]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
attribute_type: Union[str, bytes, UUID],
|
||||
permissions: Union[str, Attribute.Permissions],
|
||||
value: Any = b'',
|
||||
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):
|
||||
@@ -848,11 +862,11 @@ class Attribute(EventEmitter):
|
||||
|
||||
self.value = value
|
||||
|
||||
def encode_value(self, value: Any) -> bytes:
|
||||
return value
|
||||
def encode_value(self, value: _T) -> bytes:
|
||||
return value # type: ignore
|
||||
|
||||
def decode_value(self, value_bytes: bytes) -> Any:
|
||||
return value_bytes
|
||||
def decode_value(self, value: bytes) -> _T:
|
||||
return value # type: ignore
|
||||
|
||||
async def read_value(self, connection: Optional[Connection]) -> bytes:
|
||||
if (
|
||||
@@ -877,11 +891,14 @@ class Attribute(EventEmitter):
|
||||
error_code=ATT_INSUFFICIENT_AUTHORIZATION_ERROR, att_handle=self.handle
|
||||
)
|
||||
|
||||
if hasattr(self.value, 'read'):
|
||||
value: Union[_T, None]
|
||||
if isinstance(self.value, AttributeValue):
|
||||
try:
|
||||
value = self.value.read(connection)
|
||||
if inspect.isawaitable(value):
|
||||
value = await value
|
||||
read_value = self.value.read(connection)
|
||||
if inspect.isawaitable(read_value):
|
||||
value = await read_value
|
||||
else:
|
||||
value = read_value
|
||||
except ATT_Error as error:
|
||||
raise ATT_Error(
|
||||
error_code=error.error_code, att_handle=self.handle
|
||||
@@ -889,20 +906,24 @@ class Attribute(EventEmitter):
|
||||
else:
|
||||
value = self.value
|
||||
|
||||
self.emit('read', connection, value)
|
||||
self.emit('read', connection, b'' if value is None else value)
|
||||
|
||||
return self.encode_value(value)
|
||||
return b'' if value is None else self.encode_value(value)
|
||||
|
||||
async def write_value(self, connection: Connection, value_bytes: bytes) -> None:
|
||||
async def write_value(self, connection: Optional[Connection], value: bytes) -> None:
|
||||
if (
|
||||
self.permissions & self.WRITE_REQUIRES_ENCRYPTION
|
||||
) and not connection.encryption:
|
||||
(self.permissions & self.WRITE_REQUIRES_ENCRYPTION)
|
||||
and connection is not None
|
||||
and not connection.encryption
|
||||
):
|
||||
raise ATT_Error(
|
||||
error_code=ATT_INSUFFICIENT_ENCRYPTION_ERROR, att_handle=self.handle
|
||||
)
|
||||
if (
|
||||
self.permissions & self.WRITE_REQUIRES_AUTHENTICATION
|
||||
) and not connection.authenticated:
|
||||
(self.permissions & self.WRITE_REQUIRES_AUTHENTICATION)
|
||||
and connection is not None
|
||||
and not connection.authenticated
|
||||
):
|
||||
raise ATT_Error(
|
||||
error_code=ATT_INSUFFICIENT_AUTHENTICATION_ERROR, att_handle=self.handle
|
||||
)
|
||||
@@ -912,11 +933,11 @@ class Attribute(EventEmitter):
|
||||
error_code=ATT_INSUFFICIENT_AUTHORIZATION_ERROR, att_handle=self.handle
|
||||
)
|
||||
|
||||
value = self.decode_value(value_bytes)
|
||||
decoded_value = self.decode_value(value)
|
||||
|
||||
if hasattr(self.value, 'write'):
|
||||
if isinstance(self.value, AttributeValue):
|
||||
try:
|
||||
result = self.value.write(connection, value)
|
||||
result = self.value.write(connection, decoded_value)
|
||||
if inspect.isawaitable(result):
|
||||
await result
|
||||
except ATT_Error as error:
|
||||
@@ -924,9 +945,9 @@ class Attribute(EventEmitter):
|
||||
error_code=error.error_code, att_handle=self.handle
|
||||
) from error
|
||||
else:
|
||||
self.value = value
|
||||
self.value = decoded_value
|
||||
|
||||
self.emit('write', connection, value)
|
||||
self.emit('write', connection, decoded_value)
|
||||
|
||||
def __repr__(self):
|
||||
if isinstance(self.value, bytes):
|
||||
|
||||
@@ -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
|
||||
|
||||
434
bumble/device.py
434
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,
|
||||
|
||||
253
bumble/gatt.py
253
bumble/gatt.py
@@ -27,28 +27,16 @@ import enum
|
||||
import functools
|
||||
import logging
|
||||
import struct
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
SupportsBytes,
|
||||
Type,
|
||||
Union,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
from typing import Iterable, List, Optional, Sequence, TypeVar, Union
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.core import BaseBumbleError, InvalidOperationError, UUID
|
||||
from bumble.core import BaseBumbleError, UUID
|
||||
from bumble.att import Attribute, AttributeValue
|
||||
from bumble.utils import ByteSerializable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.gatt_client import AttributeProxy
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Typing
|
||||
# -----------------------------------------------------------------------------
|
||||
_T = TypeVar('_T')
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -298,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')
|
||||
@@ -436,7 +440,7 @@ class IncludedServiceDeclaration(Attribute):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Characteristic(Attribute):
|
||||
class Characteristic(Attribute[_T]):
|
||||
'''
|
||||
See Vol 3, Part G - 3.3 CHARACTERISTIC DEFINITION
|
||||
'''
|
||||
@@ -499,7 +503,7 @@ class Characteristic(Attribute):
|
||||
uuid: Union[str, bytes, UUID],
|
||||
properties: Characteristic.Properties,
|
||||
permissions: Union[str, Attribute.Permissions],
|
||||
value: Any = b'',
|
||||
value: Union[AttributeValue[_T], _T, None] = None,
|
||||
descriptors: Sequence[Descriptor] = (),
|
||||
):
|
||||
super().__init__(uuid, permissions, value)
|
||||
@@ -559,217 +563,10 @@ class CharacteristicDeclaration(Attribute):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class CharacteristicValue(AttributeValue):
|
||||
class CharacteristicValue(AttributeValue[_T]):
|
||||
"""Same as AttributeValue, for backward compatibility"""
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class CharacteristicAdapter:
|
||||
'''
|
||||
An adapter that can adapt Characteristic and AttributeProxy objects
|
||||
by wrapping their `read_value()` and `write_value()` methods with ones that
|
||||
return/accept encoded/decoded values.
|
||||
|
||||
For proxies (i.e used by a GATT client), the adaptation is one where the return
|
||||
value of `read_value()` is decoded and the value passed to `write_value()` is
|
||||
encoded. The `subscribe()` method, is wrapped with one where the values are decoded
|
||||
before being passed to the subscriber.
|
||||
|
||||
For local values (i.e hosted by a GATT server) the adaptation is one where the
|
||||
return value of `read_value()` is encoded and the value passed to `write_value()`
|
||||
is decoded.
|
||||
'''
|
||||
|
||||
read_value: Callable
|
||||
write_value: Callable
|
||||
|
||||
def __init__(self, characteristic: Union[Characteristic, AttributeProxy]):
|
||||
self.wrapped_characteristic = characteristic
|
||||
self.subscribers: Dict[Callable, Callable] = (
|
||||
{}
|
||||
) # Map from subscriber to proxy subscriber
|
||||
|
||||
if isinstance(characteristic, Characteristic):
|
||||
self.read_value = self.read_encoded_value
|
||||
self.write_value = self.write_encoded_value
|
||||
else:
|
||||
self.read_value = self.read_decoded_value
|
||||
self.write_value = self.write_decoded_value
|
||||
self.subscribe = self.wrapped_subscribe
|
||||
self.unsubscribe = self.wrapped_unsubscribe
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.wrapped_characteristic, name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name in (
|
||||
'wrapped_characteristic',
|
||||
'subscribers',
|
||||
'read_value',
|
||||
'write_value',
|
||||
'subscribe',
|
||||
'unsubscribe',
|
||||
):
|
||||
super().__setattr__(name, value)
|
||||
else:
|
||||
setattr(self.wrapped_characteristic, name, value)
|
||||
|
||||
async def read_encoded_value(self, connection):
|
||||
return self.encode_value(
|
||||
await self.wrapped_characteristic.read_value(connection)
|
||||
)
|
||||
|
||||
async def write_encoded_value(self, connection, value):
|
||||
return await self.wrapped_characteristic.write_value(
|
||||
connection, self.decode_value(value)
|
||||
)
|
||||
|
||||
async def read_decoded_value(self):
|
||||
return self.decode_value(await self.wrapped_characteristic.read_value())
|
||||
|
||||
async def write_decoded_value(self, value, with_response=False):
|
||||
return await self.wrapped_characteristic.write_value(
|
||||
self.encode_value(value), with_response
|
||||
)
|
||||
|
||||
def encode_value(self, value):
|
||||
return value
|
||||
|
||||
def decode_value(self, value):
|
||||
return value
|
||||
|
||||
def wrapped_subscribe(self, subscriber=None):
|
||||
if subscriber is not None:
|
||||
if subscriber in self.subscribers:
|
||||
# We already have a proxy subscriber
|
||||
subscriber = self.subscribers[subscriber]
|
||||
else:
|
||||
# Create and register a proxy that will decode the value
|
||||
original_subscriber = subscriber
|
||||
|
||||
def on_change(value):
|
||||
original_subscriber(self.decode_value(value))
|
||||
|
||||
self.subscribers[subscriber] = on_change
|
||||
subscriber = on_change
|
||||
|
||||
return self.wrapped_characteristic.subscribe(subscriber)
|
||||
|
||||
def wrapped_unsubscribe(self, subscriber=None):
|
||||
if subscriber in self.subscribers:
|
||||
subscriber = self.subscribers.pop(subscriber)
|
||||
|
||||
return self.wrapped_characteristic.unsubscribe(subscriber)
|
||||
|
||||
def __str__(self) -> str:
|
||||
wrapped = str(self.wrapped_characteristic)
|
||||
return f'{self.__class__.__name__}({wrapped})'
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class DelegatedCharacteristicAdapter(CharacteristicAdapter):
|
||||
'''
|
||||
Adapter that converts bytes values using an encode and a decode function.
|
||||
'''
|
||||
|
||||
def __init__(self, characteristic, encode=None, decode=None):
|
||||
super().__init__(characteristic)
|
||||
self.encode = encode
|
||||
self.decode = decode
|
||||
|
||||
def encode_value(self, value):
|
||||
if self.encode is None:
|
||||
raise InvalidOperationError('delegated adapter does not have an encoder')
|
||||
return self.encode(value)
|
||||
|
||||
def decode_value(self, value):
|
||||
if self.decode is None:
|
||||
raise InvalidOperationError('delegate adapter does not have a decoder')
|
||||
return self.decode(value)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PackedCharacteristicAdapter(CharacteristicAdapter):
|
||||
'''
|
||||
Adapter that packs/unpacks characteristic values according to a standard
|
||||
Python `struct` format.
|
||||
For formats with a single value, the adapted `read_value` and `write_value`
|
||||
methods return/accept single values. For formats with multiple values,
|
||||
they return/accept a tuple with the same number of elements as is required for
|
||||
the format.
|
||||
'''
|
||||
|
||||
def __init__(self, characteristic, pack_format):
|
||||
super().__init__(characteristic)
|
||||
self.struct = struct.Struct(pack_format)
|
||||
|
||||
def pack(self, *values):
|
||||
return self.struct.pack(*values)
|
||||
|
||||
def unpack(self, buffer):
|
||||
return self.struct.unpack(buffer)
|
||||
|
||||
def encode_value(self, value):
|
||||
return self.pack(*value if isinstance(value, tuple) else (value,))
|
||||
|
||||
def decode_value(self, value):
|
||||
unpacked = self.unpack(value)
|
||||
return unpacked[0] if len(unpacked) == 1 else unpacked
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class MappedCharacteristicAdapter(PackedCharacteristicAdapter):
|
||||
'''
|
||||
Adapter that packs/unpacks characteristic values according to a standard
|
||||
Python `struct` format.
|
||||
The adapted `read_value` and `write_value` methods return/accept a dictionary which
|
||||
is packed/unpacked according to format, with the arguments extracted from the
|
||||
dictionary by key, in the same order as they occur in the `keys` parameter.
|
||||
'''
|
||||
|
||||
def __init__(self, characteristic, pack_format, keys):
|
||||
super().__init__(characteristic, pack_format)
|
||||
self.keys = keys
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
def pack(self, values):
|
||||
return super().pack(*(values[key] for key in self.keys))
|
||||
|
||||
def unpack(self, buffer):
|
||||
return dict(zip(self.keys, super().unpack(buffer)))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class UTF8CharacteristicAdapter(CharacteristicAdapter):
|
||||
'''
|
||||
Adapter that converts strings to/from bytes using UTF-8 encoding
|
||||
'''
|
||||
|
||||
def encode_value(self, value: str) -> bytes:
|
||||
return value.encode('utf-8')
|
||||
|
||||
def decode_value(self, value: bytes) -> str:
|
||||
return value.decode('utf-8')
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class SerializableCharacteristicAdapter(CharacteristicAdapter):
|
||||
'''
|
||||
Adapter that converts any class to/from bytes using the class'
|
||||
`to_bytes` and `__bytes__` methods, respectively.
|
||||
'''
|
||||
|
||||
def __init__(self, characteristic, cls: Type[ByteSerializable]):
|
||||
super().__init__(characteristic)
|
||||
self.cls = cls
|
||||
|
||||
def encode_value(self, value: SupportsBytes) -> bytes:
|
||||
return bytes(value)
|
||||
|
||||
def decode_value(self, value: bytes) -> Any:
|
||||
return self.cls.from_bytes(value)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Descriptor(Attribute):
|
||||
'''
|
||||
|
||||
374
bumble/gatt_adapters.py
Normal file
374
bumble/gatt_adapters.py
Normal file
@@ -0,0 +1,374 @@
|
||||
# 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.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# GATT - Type Adapters
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import struct
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Generic,
|
||||
Iterable,
|
||||
Literal,
|
||||
Optional,
|
||||
Type,
|
||||
TypeVar,
|
||||
)
|
||||
|
||||
from bumble.core import InvalidOperationError
|
||||
from bumble.gatt import Characteristic
|
||||
from bumble.gatt_client import CharacteristicProxy
|
||||
from bumble import utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Typing
|
||||
# -----------------------------------------------------------------------------
|
||||
_T = TypeVar('_T')
|
||||
_T2 = TypeVar('_T2', bound=utils.ByteSerializable)
|
||||
_T3 = TypeVar('_T3', bound=utils.IntConvertible)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class CharacteristicAdapter(Characteristic, Generic[_T]):
|
||||
'''Base class for GATT Characteristic adapters.'''
|
||||
|
||||
def __init__(self, characteristic: Characteristic) -> None:
|
||||
super().__init__(
|
||||
characteristic.uuid,
|
||||
characteristic.properties,
|
||||
characteristic.permissions,
|
||||
characteristic.value,
|
||||
characteristic.descriptors,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class CharacteristicProxyAdapter(CharacteristicProxy[_T]):
|
||||
'''Base class for GATT CharacteristicProxy adapters.'''
|
||||
|
||||
def __init__(self, characteristic_proxy: CharacteristicProxy):
|
||||
super().__init__(
|
||||
characteristic_proxy.client,
|
||||
characteristic_proxy.handle,
|
||||
characteristic_proxy.end_group_handle,
|
||||
characteristic_proxy.uuid,
|
||||
characteristic_proxy.properties,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class DelegatedCharacteristicAdapter(CharacteristicAdapter[_T]):
|
||||
'''
|
||||
Adapter that converts bytes values using an encode and/or a decode function.
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
characteristic: Characteristic,
|
||||
encode: Optional[Callable[[_T], bytes]] = None,
|
||||
decode: Optional[Callable[[bytes], _T]] = None,
|
||||
):
|
||||
super().__init__(characteristic)
|
||||
self.encode = encode
|
||||
self.decode = decode
|
||||
|
||||
def encode_value(self, value: _T) -> bytes:
|
||||
if self.encode is None:
|
||||
raise InvalidOperationError('delegated adapter does not have an encoder')
|
||||
return self.encode(value)
|
||||
|
||||
def decode_value(self, value: bytes) -> _T:
|
||||
if self.decode is None:
|
||||
raise InvalidOperationError('delegate adapter does not have a decoder')
|
||||
return self.decode(value)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class DelegatedCharacteristicProxyAdapter(CharacteristicProxyAdapter[_T]):
|
||||
'''
|
||||
Adapter that converts bytes values using an encode and a decode function.
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
characteristic_proxy: CharacteristicProxy,
|
||||
encode: Optional[Callable[[_T], bytes]] = None,
|
||||
decode: Optional[Callable[[bytes], _T]] = None,
|
||||
):
|
||||
super().__init__(characteristic_proxy)
|
||||
self.encode = encode
|
||||
self.decode = decode
|
||||
|
||||
def encode_value(self, value: _T) -> bytes:
|
||||
if self.encode is None:
|
||||
raise InvalidOperationError('delegated adapter does not have an encoder')
|
||||
return self.encode(value)
|
||||
|
||||
def decode_value(self, value: bytes) -> _T:
|
||||
if self.decode is None:
|
||||
raise InvalidOperationError('delegate adapter does not have a decoder')
|
||||
return self.decode(value)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PackedCharacteristicAdapter(CharacteristicAdapter):
|
||||
'''
|
||||
Adapter that packs/unpacks characteristic values according to a standard
|
||||
Python `struct` format.
|
||||
For formats with a single value, the adapted `read_value` and `write_value`
|
||||
methods return/accept single values. For formats with multiple values,
|
||||
they return/accept a tuple with the same number of elements as is required for
|
||||
the format.
|
||||
'''
|
||||
|
||||
def __init__(self, characteristic: Characteristic, pack_format: str) -> None:
|
||||
super().__init__(characteristic)
|
||||
self.struct = struct.Struct(pack_format)
|
||||
|
||||
def pack(self, *values) -> bytes:
|
||||
return self.struct.pack(*values)
|
||||
|
||||
def unpack(self, buffer: bytes) -> tuple:
|
||||
return self.struct.unpack(buffer)
|
||||
|
||||
def encode_value(self, value: Any) -> bytes:
|
||||
return self.pack(*value if isinstance(value, tuple) else (value,))
|
||||
|
||||
def decode_value(self, value: bytes) -> Any:
|
||||
unpacked = self.unpack(value)
|
||||
return unpacked[0] if len(unpacked) == 1 else unpacked
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PackedCharacteristicProxyAdapter(CharacteristicProxyAdapter):
|
||||
'''
|
||||
Adapter that packs/unpacks characteristic values according to a standard
|
||||
Python `struct` format.
|
||||
For formats with a single value, the adapted `read_value` and `write_value`
|
||||
methods return/accept single values. For formats with multiple values,
|
||||
they return/accept a tuple with the same number of elements as is required for
|
||||
the format.
|
||||
'''
|
||||
|
||||
def __init__(self, characteristic_proxy, pack_format):
|
||||
super().__init__(characteristic_proxy)
|
||||
self.struct = struct.Struct(pack_format)
|
||||
|
||||
def pack(self, *values) -> bytes:
|
||||
return self.struct.pack(*values)
|
||||
|
||||
def unpack(self, buffer: bytes) -> tuple:
|
||||
return self.struct.unpack(buffer)
|
||||
|
||||
def encode_value(self, value: Any) -> bytes:
|
||||
return self.pack(*value if isinstance(value, tuple) else (value,))
|
||||
|
||||
def decode_value(self, value: bytes) -> Any:
|
||||
unpacked = self.unpack(value)
|
||||
return unpacked[0] if len(unpacked) == 1 else unpacked
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class MappedCharacteristicAdapter(PackedCharacteristicAdapter):
|
||||
'''
|
||||
Adapter that packs/unpacks characteristic values according to a standard
|
||||
Python `struct` format.
|
||||
The adapted `read_value` and `write_value` methods return/accept a dictionary which
|
||||
is packed/unpacked according to format, with the arguments extracted from the
|
||||
dictionary by key, in the same order as they occur in the `keys` parameter.
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self, characteristic: Characteristic, pack_format: str, keys: Iterable[str]
|
||||
) -> None:
|
||||
super().__init__(characteristic, pack_format)
|
||||
self.keys = keys
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
def pack(self, values) -> bytes:
|
||||
return super().pack(*(values[key] for key in self.keys))
|
||||
|
||||
def unpack(self, buffer: bytes) -> Any:
|
||||
return dict(zip(self.keys, super().unpack(buffer)))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class MappedCharacteristicProxyAdapter(PackedCharacteristicProxyAdapter):
|
||||
'''
|
||||
Adapter that packs/unpacks characteristic values according to a standard
|
||||
Python `struct` format.
|
||||
The adapted `read_value` and `write_value` methods return/accept a dictionary which
|
||||
is packed/unpacked according to format, with the arguments extracted from the
|
||||
dictionary by key, in the same order as they occur in the `keys` parameter.
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
characteristic_proxy: CharacteristicProxy,
|
||||
pack_format: str,
|
||||
keys: Iterable[str],
|
||||
) -> None:
|
||||
super().__init__(characteristic_proxy, pack_format)
|
||||
self.keys = keys
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
def pack(self, values) -> bytes:
|
||||
return super().pack(*(values[key] for key in self.keys))
|
||||
|
||||
def unpack(self, buffer: bytes) -> Any:
|
||||
return dict(zip(self.keys, super().unpack(buffer)))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class UTF8CharacteristicAdapter(CharacteristicAdapter[str]):
|
||||
'''
|
||||
Adapter that converts strings to/from bytes using UTF-8 encoding
|
||||
'''
|
||||
|
||||
def encode_value(self, value: str) -> bytes:
|
||||
return value.encode('utf-8')
|
||||
|
||||
def decode_value(self, value: bytes) -> str:
|
||||
return value.decode('utf-8')
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class UTF8CharacteristicProxyAdapter(CharacteristicProxyAdapter[str]):
|
||||
'''
|
||||
Adapter that converts strings to/from bytes using UTF-8 encoding
|
||||
'''
|
||||
|
||||
def encode_value(self, value: str) -> bytes:
|
||||
return value.encode('utf-8')
|
||||
|
||||
def decode_value(self, value: bytes) -> str:
|
||||
return value.decode('utf-8')
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class SerializableCharacteristicAdapter(CharacteristicAdapter[_T2]):
|
||||
'''
|
||||
Adapter that converts any class to/from bytes using the class'
|
||||
`to_bytes` and `__bytes__` methods, respectively.
|
||||
'''
|
||||
|
||||
def __init__(self, characteristic: Characteristic, cls: Type[_T2]) -> None:
|
||||
super().__init__(characteristic)
|
||||
self.cls = cls
|
||||
|
||||
def encode_value(self, value: _T2) -> bytes:
|
||||
return bytes(value)
|
||||
|
||||
def decode_value(self, value: bytes) -> _T2:
|
||||
return self.cls.from_bytes(value)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class SerializableCharacteristicProxyAdapter(CharacteristicProxyAdapter[_T2]):
|
||||
'''
|
||||
Adapter that converts any class to/from bytes using the class'
|
||||
`to_bytes` and `__bytes__` methods, respectively.
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self, characteristic_proxy: CharacteristicProxy, cls: Type[_T2]
|
||||
) -> None:
|
||||
super().__init__(characteristic_proxy)
|
||||
self.cls = cls
|
||||
|
||||
def encode_value(self, value: _T2) -> bytes:
|
||||
return bytes(value)
|
||||
|
||||
def decode_value(self, value: bytes) -> _T2:
|
||||
return self.cls.from_bytes(value)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class EnumCharacteristicAdapter(CharacteristicAdapter[_T3]):
|
||||
'''
|
||||
Adapter that converts int-enum-like classes to/from bytes using the class'
|
||||
`int().to_bytes()` and `from_bytes()` methods, respectively.
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
characteristic: Characteristic,
|
||||
cls: Type[_T3],
|
||||
length: int,
|
||||
byteorder: Literal['little', 'big'] = 'little',
|
||||
):
|
||||
"""
|
||||
Initialize an instance.
|
||||
|
||||
Params:
|
||||
characteristic: the Characteristic to adapt to/from
|
||||
cls: the class to/from which to convert integer values
|
||||
length: number of bytes used to represent integer values
|
||||
byteorder: byte order of the byte representation of integers.
|
||||
"""
|
||||
super().__init__(characteristic)
|
||||
self.cls = cls
|
||||
self.length = length
|
||||
self.byteorder = byteorder
|
||||
|
||||
def encode_value(self, value: _T3) -> bytes:
|
||||
return int(value).to_bytes(self.length, self.byteorder)
|
||||
|
||||
def decode_value(self, value: bytes) -> _T3:
|
||||
int_value = int.from_bytes(value, self.byteorder)
|
||||
return self.cls(int_value)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class EnumCharacteristicProxyAdapter(CharacteristicProxyAdapter[_T3]):
|
||||
'''
|
||||
Adapter that converts int-enum-like classes to/from bytes using the class'
|
||||
`int().to_bytes()` and `from_bytes()` methods, respectively.
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
characteristic_proxy: CharacteristicProxy,
|
||||
cls: Type[_T3],
|
||||
length: int,
|
||||
byteorder: Literal['little', 'big'] = 'little',
|
||||
):
|
||||
"""
|
||||
Initialize an instance.
|
||||
|
||||
Params:
|
||||
characteristic_proxy: the CharacteristicProxy to adapt to/from
|
||||
cls: the class to/from which to convert integer values
|
||||
length: number of bytes used to represent integer values
|
||||
byteorder: byte order of the byte representation of integers.
|
||||
"""
|
||||
super().__init__(characteristic_proxy)
|
||||
self.cls = cls
|
||||
self.length = length
|
||||
self.byteorder = byteorder
|
||||
|
||||
def encode_value(self, value: _T3) -> bytes:
|
||||
return int(value).to_bytes(self.length, self.byteorder)
|
||||
|
||||
def decode_value(self, value: bytes) -> _T3:
|
||||
int_value = int.from_bytes(value, self.byteorder)
|
||||
a = self.cls(int_value)
|
||||
return self.cls(int_value)
|
||||
@@ -29,24 +29,25 @@ import logging
|
||||
import struct
|
||||
from datetime import datetime
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Generic,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Dict,
|
||||
Tuple,
|
||||
Callable,
|
||||
Union,
|
||||
Any,
|
||||
Iterable,
|
||||
Type,
|
||||
Set,
|
||||
Tuple,
|
||||
Union,
|
||||
Type,
|
||||
TypeVar,
|
||||
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,
|
||||
@@ -67,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,
|
||||
@@ -82,9 +84,14 @@ from .gatt import (
|
||||
TemplateService,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Typing
|
||||
# -----------------------------------------------------------------------------
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Connection
|
||||
|
||||
_T = TypeVar('_T')
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -110,31 +117,31 @@ def show_services(services: Iterable[ServiceProxy]) -> None:
|
||||
# -----------------------------------------------------------------------------
|
||||
# Proxies
|
||||
# -----------------------------------------------------------------------------
|
||||
class AttributeProxy(EventEmitter):
|
||||
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
|
||||
self.type = attribute_type
|
||||
|
||||
async def read_value(self, no_long_read: bool = False) -> bytes:
|
||||
async def read_value(self, no_long_read: bool = False) -> _T:
|
||||
return self.decode_value(
|
||||
await self.client.read_value(self.handle, no_long_read)
|
||||
)
|
||||
|
||||
async def write_value(self, value, with_response=False):
|
||||
async def write_value(self, value: _T, with_response=False):
|
||||
return await self.client.write_value(
|
||||
self.handle, self.encode_value(value), with_response
|
||||
)
|
||||
|
||||
def encode_value(self, value: Any) -> bytes:
|
||||
return value
|
||||
def encode_value(self, value: _T) -> bytes:
|
||||
return value # type: ignore
|
||||
|
||||
def decode_value(self, value_bytes: bytes) -> Any:
|
||||
return value_bytes
|
||||
def decode_value(self, value: bytes) -> _T:
|
||||
return value # type: ignore
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'Attribute(handle=0x{self.handle:04X}, type={self.type})'
|
||||
@@ -142,7 +149,7 @@ class AttributeProxy(EventEmitter):
|
||||
|
||||
class ServiceProxy(AttributeProxy):
|
||||
uuid: UUID
|
||||
characteristics: List[CharacteristicProxy]
|
||||
characteristics: List[CharacteristicProxy[bytes]]
|
||||
included_services: List[ServiceProxy]
|
||||
|
||||
@staticmethod
|
||||
@@ -163,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.
|
||||
|
||||
@@ -184,19 +197,19 @@ class ServiceProxy(AttributeProxy):
|
||||
return f'Service(handle=0x{self.handle:04X}, uuid={self.uuid})'
|
||||
|
||||
|
||||
class CharacteristicProxy(AttributeProxy):
|
||||
class CharacteristicProxy(AttributeProxy[_T]):
|
||||
properties: Characteristic.Properties
|
||||
descriptors: List[DescriptorProxy]
|
||||
subscribers: Dict[Any, Callable[[bytes], Any]]
|
||||
subscribers: Dict[Any, Callable[[_T], Any]]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client,
|
||||
handle,
|
||||
end_group_handle,
|
||||
uuid,
|
||||
client: Client,
|
||||
handle: int,
|
||||
end_group_handle: int,
|
||||
uuid: UUID,
|
||||
properties: int,
|
||||
):
|
||||
) -> None:
|
||||
super().__init__(client, handle, end_group_handle, uuid)
|
||||
self.uuid = uuid
|
||||
self.properties = Characteristic.Properties(properties)
|
||||
@@ -204,21 +217,21 @@ class CharacteristicProxy(AttributeProxy):
|
||||
self.descriptors_discovered = False
|
||||
self.subscribers = {} # Map from subscriber to proxy subscriber
|
||||
|
||||
def get_descriptor(self, descriptor_type):
|
||||
def get_descriptor(self, descriptor_type: UUID) -> Optional[DescriptorProxy]:
|
||||
for descriptor in self.descriptors:
|
||||
if descriptor.type == descriptor_type:
|
||||
return descriptor
|
||||
|
||||
return None
|
||||
|
||||
async def discover_descriptors(self):
|
||||
async def discover_descriptors(self) -> list[DescriptorProxy]:
|
||||
return await self.client.discover_descriptors(self)
|
||||
|
||||
async def subscribe(
|
||||
self,
|
||||
subscriber: Optional[Callable[[bytes], Any]] = None,
|
||||
subscriber: Optional[Callable[[_T], Any]] = None,
|
||||
prefer_notify: bool = True,
|
||||
):
|
||||
) -> None:
|
||||
if subscriber is not None:
|
||||
if subscriber in self.subscribers:
|
||||
# We already have a proxy subscriber
|
||||
@@ -233,13 +246,13 @@ class CharacteristicProxy(AttributeProxy):
|
||||
self.subscribers[subscriber] = on_change
|
||||
subscriber = on_change
|
||||
|
||||
return await self.client.subscribe(self, subscriber, prefer_notify)
|
||||
await self.client.subscribe(self, subscriber, prefer_notify)
|
||||
|
||||
async def unsubscribe(self, subscriber=None, force=False):
|
||||
async def unsubscribe(self, subscriber=None, force=False) -> None:
|
||||
if subscriber in self.subscribers:
|
||||
subscriber = self.subscribers.pop(subscriber)
|
||||
|
||||
return await self.client.unsubscribe(self, subscriber, force)
|
||||
await self.client.unsubscribe(self, subscriber, force)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
@@ -249,8 +262,8 @@ class CharacteristicProxy(AttributeProxy):
|
||||
)
|
||||
|
||||
|
||||
class DescriptorProxy(AttributeProxy):
|
||||
def __init__(self, client, handle, descriptor_type):
|
||||
class DescriptorProxy(AttributeProxy[bytes]):
|
||||
def __init__(self, client: Client, handle: int, descriptor_type: UUID) -> None:
|
||||
super().__init__(client, handle, 0, descriptor_type)
|
||||
|
||||
def __str__(self) -> str:
|
||||
@@ -369,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
|
||||
@@ -621,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
|
||||
@@ -634,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(
|
||||
@@ -679,7 +692,7 @@ class Client:
|
||||
|
||||
properties, handle = struct.unpack_from('<BH', attribute_value)
|
||||
characteristic_uuid = UUID.from_bytes(attribute_value[3:])
|
||||
characteristic = CharacteristicProxy(
|
||||
characteristic = CharacteristicProxy[bytes](
|
||||
self, handle, 0, characteristic_uuid, properties
|
||||
)
|
||||
|
||||
@@ -772,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
|
||||
'''
|
||||
@@ -805,7 +818,7 @@ class Client:
|
||||
logger.warning(f'bogus handle value: {attribute_handle}')
|
||||
return []
|
||||
|
||||
attribute = AttributeProxy(
|
||||
attribute = AttributeProxy[bytes](
|
||||
self, attribute_handle, 0, UUID.from_bytes(attribute_uuid)
|
||||
)
|
||||
attributes.append(attribute)
|
||||
@@ -818,7 +831,7 @@ class Client:
|
||||
async def subscribe(
|
||||
self,
|
||||
characteristic: CharacteristicProxy,
|
||||
subscriber: Optional[Callable[[bytes], Any]] = None,
|
||||
subscriber: Optional[Callable[[Any], Any]] = None,
|
||||
prefer_notify: bool = True,
|
||||
) -> None:
|
||||
# If we haven't already discovered the descriptors for this characteristic,
|
||||
@@ -868,7 +881,7 @@ class Client:
|
||||
async def unsubscribe(
|
||||
self,
|
||||
characteristic: CharacteristicProxy,
|
||||
subscriber: Optional[Callable[[bytes], Any]] = None,
|
||||
subscriber: Optional[Callable[[Any], Any]] = None,
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
'''
|
||||
|
||||
@@ -36,10 +36,8 @@ from typing import (
|
||||
Tuple,
|
||||
TypeVar,
|
||||
Type,
|
||||
Union,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
from pyee import EventEmitter
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.core import UUID
|
||||
@@ -78,14 +76,13 @@ from bumble.gatt import (
|
||||
GATT_REQUEST_TIMEOUT,
|
||||
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
Characteristic,
|
||||
CharacteristicAdapter,
|
||||
CharacteristicDeclaration,
|
||||
CharacteristicValue,
|
||||
IncludedServiceDeclaration,
|
||||
Descriptor,
|
||||
Service,
|
||||
)
|
||||
from bumble.utils import AsyncRunner
|
||||
from bumble import utils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Device, Connection
|
||||
@@ -105,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]
|
||||
@@ -469,7 +466,7 @@ class Server(EventEmitter):
|
||||
finally:
|
||||
self.pending_confirmations[connection.handle] = None
|
||||
|
||||
async def notify_or_indicate_subscribers(
|
||||
async def _notify_or_indicate_subscribers(
|
||||
self,
|
||||
indicate: bool,
|
||||
attribute: Attribute,
|
||||
@@ -503,7 +500,9 @@ class Server(EventEmitter):
|
||||
value: Optional[bytes] = None,
|
||||
force: bool = False,
|
||||
):
|
||||
return await self.notify_or_indicate_subscribers(False, attribute, value, force)
|
||||
return await self._notify_or_indicate_subscribers(
|
||||
False, attribute, value, force
|
||||
)
|
||||
|
||||
async def indicate_subscribers(
|
||||
self,
|
||||
@@ -511,7 +510,7 @@ class Server(EventEmitter):
|
||||
value: Optional[bytes] = None,
|
||||
force: bool = False,
|
||||
):
|
||||
return await self.notify_or_indicate_subscribers(True, attribute, value, force)
|
||||
return await self._notify_or_indicate_subscribers(True, attribute, value, force)
|
||||
|
||||
def on_disconnection(self, connection: Connection) -> None:
|
||||
if connection.handle in self.subscribers:
|
||||
@@ -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
|
||||
|
||||
215
bumble/hci.py
215
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
|
||||
|
||||
@@ -5806,12 +5825,18 @@ class HCI_LE_Advertising_Report_Event(HCI_LE_Meta_Event):
|
||||
return HCI_LE_Advertising_Report_Event.event_type_name(self.event_type)
|
||||
|
||||
def to_string(self, indentation='', _=None):
|
||||
def data_to_str(data):
|
||||
try:
|
||||
return data.hex() + ': ' + str(AdvertisingData.from_bytes(data))
|
||||
except Exception:
|
||||
return data.hex()
|
||||
|
||||
return super().to_string(
|
||||
indentation,
|
||||
{
|
||||
'event_type': HCI_LE_Advertising_Report_Event.event_type_name,
|
||||
'address_type': Address.address_type_name,
|
||||
'data': lambda x: str(AdvertisingData.from_bytes(x)),
|
||||
'data': data_to_str,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -6036,12 +6061,18 @@ class HCI_LE_Extended_Advertising_Report_Event(HCI_LE_Meta_Event):
|
||||
|
||||
def to_string(self, indentation='', _=None):
|
||||
# pylint: disable=line-too-long
|
||||
def data_to_str(data):
|
||||
try:
|
||||
return data.hex() + ': ' + str(AdvertisingData.from_bytes(data))
|
||||
except Exception:
|
||||
return data.hex()
|
||||
|
||||
return super().to_string(
|
||||
indentation,
|
||||
{
|
||||
'event_type': HCI_LE_Extended_Advertising_Report_Event.event_type_string,
|
||||
'address_type': Address.address_type_name,
|
||||
'data': lambda x: str(AdvertisingData.from_bytes(x)),
|
||||
'data': data_to_str,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -6184,13 +6215,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 +6602,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 +6654,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 +6996,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,
|
||||
|
||||
@@ -24,16 +24,13 @@ import struct
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from bumble import gatt
|
||||
from bumble.device import Connection
|
||||
from bumble.att import ATT_Error
|
||||
from bumble.gatt import (
|
||||
Attribute,
|
||||
Characteristic,
|
||||
SerializableCharacteristicAdapter,
|
||||
PackedCharacteristicAdapter,
|
||||
TemplateService,
|
||||
CharacteristicValue,
|
||||
UTF8CharacteristicAdapter,
|
||||
GATT_AUDIO_INPUT_CONTROL_SERVICE,
|
||||
GATT_AUDIO_INPUT_STATE_CHARACTERISTIC,
|
||||
GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC,
|
||||
@@ -42,8 +39,16 @@ from bumble.gatt import (
|
||||
GATT_AUDIO_INPUT_CONTROL_POINT_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_DESCRIPTION_CHARACTERISTIC,
|
||||
)
|
||||
from bumble.gatt_adapters import (
|
||||
CharacteristicProxy,
|
||||
PackedCharacteristicProxyAdapter,
|
||||
SerializableCharacteristicAdapter,
|
||||
SerializableCharacteristicProxyAdapter,
|
||||
UTF8CharacteristicAdapter,
|
||||
UTF8CharacteristicProxyAdapter,
|
||||
)
|
||||
from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
|
||||
from bumble.utils import OpenIntEnum
|
||||
from bumble import utils
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -59,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
|
||||
'''
|
||||
@@ -71,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
|
||||
'''
|
||||
@@ -81,7 +86,7 @@ class Mute(OpenIntEnum):
|
||||
DISABLED = 0x02
|
||||
|
||||
|
||||
class GainMode(OpenIntEnum):
|
||||
class GainMode(utils.OpenIntEnum):
|
||||
'''
|
||||
Cf. 2.2.1.3 Gain Mode
|
||||
'''
|
||||
@@ -92,7 +97,7 @@ class GainMode(OpenIntEnum):
|
||||
AUTOMATIC = 0x03
|
||||
|
||||
|
||||
class AudioInputStatus(OpenIntEnum):
|
||||
class AudioInputStatus(utils.OpenIntEnum):
|
||||
'''
|
||||
Cf. 3.4 Audio Input Status
|
||||
'''
|
||||
@@ -101,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
|
||||
'''
|
||||
@@ -124,7 +129,7 @@ class AudioInputState:
|
||||
mute: Mute = Mute.NOT_MUTED
|
||||
gain_mode: GainMode = GainMode.MANUAL
|
||||
change_counter: int = 0
|
||||
attribute_value: Optional[CharacteristicValue] = None
|
||||
attribute: Optional[Attribute] = None
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return bytes(
|
||||
@@ -151,10 +156,8 @@ class AudioInputState:
|
||||
self.change_counter = (self.change_counter + 1) % (CHANGE_COUNTER_MAX_VALUE + 1)
|
||||
|
||||
async def notify_subscribers_via_connection(self, connection: Connection) -> None:
|
||||
assert self.attribute_value is not None
|
||||
await connection.device.notify_subscribers(
|
||||
attribute=self.attribute_value, value=bytes(self)
|
||||
)
|
||||
assert self.attribute is not None
|
||||
await connection.device.notify_subscribers(attribute=self.attribute)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -315,24 +318,28 @@ class AudioInputDescription:
|
||||
'''
|
||||
|
||||
audio_input_description: str = "Bluetooth"
|
||||
attribute_value: Optional[CharacteristicValue] = None
|
||||
attribute: Optional[Attribute] = None
|
||||
|
||||
def on_read(self, _connection: Optional[Connection]) -> str:
|
||||
return self.audio_input_description
|
||||
|
||||
async def on_write(self, connection: Optional[Connection], value: str) -> None:
|
||||
assert connection
|
||||
assert self.attribute_value
|
||||
assert self.attribute
|
||||
|
||||
self.audio_input_description = value
|
||||
await connection.device.notify_subscribers(
|
||||
attribute=self.attribute_value, value=value
|
||||
)
|
||||
await connection.device.notify_subscribers(attribute=self.attribute)
|
||||
|
||||
|
||||
class AICSService(TemplateService):
|
||||
UUID = GATT_AUDIO_INPUT_CONTROL_SERVICE
|
||||
|
||||
audio_input_state_characteristic: Characteristic[AudioInputState]
|
||||
audio_input_type_characteristic: Characteristic[bytes]
|
||||
audio_input_status_characteristic: Characteristic[bytes]
|
||||
audio_input_control_point_characteristic: Characteristic[bytes]
|
||||
gain_settings_properties_characteristic: Characteristic[GainSettingsProperties]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
audio_input_state: Optional[AudioInputState] = None,
|
||||
@@ -374,9 +381,7 @@ class AICSService(TemplateService):
|
||||
),
|
||||
AudioInputState,
|
||||
)
|
||||
self.audio_input_state.attribute_value = (
|
||||
self.audio_input_state_characteristic.value
|
||||
)
|
||||
self.audio_input_state.attribute = self.audio_input_state_characteristic
|
||||
|
||||
self.gain_settings_properties_characteristic = (
|
||||
SerializableCharacteristicAdapter(
|
||||
@@ -425,8 +430,8 @@ class AICSService(TemplateService):
|
||||
),
|
||||
)
|
||||
)
|
||||
self.audio_input_description.attribute_value = (
|
||||
self.audio_input_control_point_characteristic.value
|
||||
self.audio_input_description.attribute = (
|
||||
self.audio_input_control_point_characteristic
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
@@ -448,24 +453,29 @@ class AICSService(TemplateService):
|
||||
class AICSServiceProxy(ProfileServiceProxy):
|
||||
SERVICE_CLASS = AICSService
|
||||
|
||||
audio_input_state: CharacteristicProxy[AudioInputState]
|
||||
gain_settings_properties: CharacteristicProxy[GainSettingsProperties]
|
||||
audio_input_status: CharacteristicProxy[int]
|
||||
audio_input_control_point: CharacteristicProxy[bytes]
|
||||
|
||||
def __init__(self, service_proxy: ServiceProxy) -> None:
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
self.audio_input_state = SerializableCharacteristicAdapter(
|
||||
self.audio_input_state = SerializableCharacteristicProxyAdapter(
|
||||
service_proxy.get_required_characteristic_by_uuid(
|
||||
GATT_AUDIO_INPUT_STATE_CHARACTERISTIC
|
||||
),
|
||||
AudioInputState,
|
||||
)
|
||||
|
||||
self.gain_settings_properties = SerializableCharacteristicAdapter(
|
||||
self.gain_settings_properties = SerializableCharacteristicProxyAdapter(
|
||||
service_proxy.get_required_characteristic_by_uuid(
|
||||
GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC
|
||||
),
|
||||
GainSettingsProperties,
|
||||
)
|
||||
|
||||
self.audio_input_status = PackedCharacteristicAdapter(
|
||||
self.audio_input_status = PackedCharacteristicProxyAdapter(
|
||||
service_proxy.get_required_characteristic_by_uuid(
|
||||
GATT_AUDIO_INPUT_STATUS_CHARACTERISTIC
|
||||
),
|
||||
@@ -478,7 +488,7 @@ class AICSServiceProxy(ProfileServiceProxy):
|
||||
)
|
||||
)
|
||||
|
||||
self.audio_input_description = UTF8CharacteristicAdapter(
|
||||
self.audio_input_description = UTF8CharacteristicProxyAdapter(
|
||||
service_proxy.get_required_characteristic_by_uuid(
|
||||
GATT_AUDIO_INPUT_DESCRIPTION_CHARACTERISTIC
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@@ -134,12 +134,14 @@ class AshaService(gatt.TemplateService):
|
||||
),
|
||||
)
|
||||
|
||||
self.audio_control_point_characteristic = gatt.Characteristic(
|
||||
gatt.GATT_ASHA_AUDIO_CONTROL_POINT_CHARACTERISTIC,
|
||||
gatt.Characteristic.Properties.WRITE
|
||||
| gatt.Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
|
||||
gatt.Characteristic.WRITEABLE,
|
||||
gatt.CharacteristicValue(write=self._on_audio_control_point_write),
|
||||
self.audio_control_point_characteristic: gatt.Characteristic[bytes] = (
|
||||
gatt.Characteristic(
|
||||
gatt.GATT_ASHA_AUDIO_CONTROL_POINT_CHARACTERISTIC,
|
||||
gatt.Characteristic.Properties.WRITE
|
||||
| gatt.Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
|
||||
gatt.Characteristic.WRITEABLE,
|
||||
gatt.CharacteristicValue(write=self._on_audio_control_point_write),
|
||||
)
|
||||
)
|
||||
self.audio_status_characteristic = gatt.Characteristic(
|
||||
gatt.GATT_ASHA_AUDIO_STATUS_CHARACTERISTIC,
|
||||
@@ -147,7 +149,7 @@ class AshaService(gatt.TemplateService):
|
||||
gatt.Characteristic.READABLE,
|
||||
bytes([AudioStatus.OK]),
|
||||
)
|
||||
self.volume_characteristic = gatt.Characteristic(
|
||||
self.volume_characteristic: gatt.Characteristic[bytes] = gatt.Characteristic(
|
||||
gatt.GATT_ASHA_VOLUME_CHARACTERISTIC,
|
||||
gatt.Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
|
||||
gatt.Characteristic.WRITEABLE,
|
||||
@@ -166,13 +168,13 @@ class AshaService(gatt.TemplateService):
|
||||
struct.pack('<H', self.psm),
|
||||
)
|
||||
|
||||
characteristics = [
|
||||
characteristics = (
|
||||
self.read_only_properties_characteristic,
|
||||
self.audio_control_point_characteristic,
|
||||
self.audio_status_characteristic,
|
||||
self.volume_characteristic,
|
||||
self.le_psm_out_characteristic,
|
||||
]
|
||||
)
|
||||
|
||||
super().__init__(characteristics)
|
||||
|
||||
@@ -257,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
|
||||
|
||||
@@ -20,11 +20,12 @@ from __future__ import annotations
|
||||
import dataclasses
|
||||
import logging
|
||||
import struct
|
||||
from typing import ClassVar, List, Optional, Sequence
|
||||
from typing import ClassVar, Optional, Sequence
|
||||
|
||||
from bumble import core
|
||||
from bumble import device
|
||||
from bumble import gatt
|
||||
from bumble import gatt_adapters
|
||||
from bumble import gatt_client
|
||||
from bumble import hci
|
||||
from bumble import utils
|
||||
@@ -52,7 +53,7 @@ def encode_subgroups(subgroups: Sequence[SubgroupInfo]) -> bytes:
|
||||
)
|
||||
|
||||
|
||||
def decode_subgroups(data: bytes) -> List[SubgroupInfo]:
|
||||
def decode_subgroups(data: bytes) -> list[SubgroupInfo]:
|
||||
num_subgroups = data[0]
|
||||
offset = 1
|
||||
subgroups = []
|
||||
@@ -273,7 +274,7 @@ class BroadcastReceiveState:
|
||||
pa_sync_state: PeriodicAdvertisingSyncState
|
||||
big_encryption: BigEncryption
|
||||
bad_code: bytes
|
||||
subgroups: List[SubgroupInfo]
|
||||
subgroups: list[SubgroupInfo]
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data: bytes) -> BroadcastReceiveState:
|
||||
@@ -353,8 +354,10 @@ class BroadcastAudioScanService(gatt.TemplateService):
|
||||
class BroadcastAudioScanServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
SERVICE_CLASS = BroadcastAudioScanService
|
||||
|
||||
broadcast_audio_scan_control_point: gatt_client.CharacteristicProxy
|
||||
broadcast_receive_states: List[gatt.DelegatedCharacteristicAdapter]
|
||||
broadcast_audio_scan_control_point: gatt_client.CharacteristicProxy[bytes]
|
||||
broadcast_receive_states: list[
|
||||
gatt_client.CharacteristicProxy[Optional[BroadcastReceiveState]]
|
||||
]
|
||||
|
||||
def __init__(self, service_proxy: gatt_client.ServiceProxy):
|
||||
self.service_proxy = service_proxy
|
||||
@@ -366,7 +369,7 @@ class BroadcastAudioScanServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
)
|
||||
|
||||
self.broadcast_receive_states = [
|
||||
gatt.DelegatedCharacteristicAdapter(
|
||||
gatt_adapters.DelegatedCharacteristicProxyAdapter(
|
||||
characteristic,
|
||||
decode=lambda x: BroadcastReceiveState.from_bytes(x) if x else None,
|
||||
)
|
||||
|
||||
@@ -16,14 +16,20 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from ..gatt_client import ProfileServiceProxy
|
||||
from ..gatt import (
|
||||
from typing import Optional
|
||||
|
||||
from bumble.gatt_client import ProfileServiceProxy
|
||||
from bumble.gatt import (
|
||||
GATT_BATTERY_SERVICE,
|
||||
GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
CharacteristicValue,
|
||||
)
|
||||
from bumble.gatt_client import CharacteristicProxy
|
||||
from bumble.gatt_adapters import (
|
||||
PackedCharacteristicAdapter,
|
||||
PackedCharacteristicProxyAdapter,
|
||||
)
|
||||
|
||||
|
||||
@@ -32,6 +38,8 @@ class BatteryService(TemplateService):
|
||||
UUID = GATT_BATTERY_SERVICE
|
||||
BATTERY_LEVEL_FORMAT = 'B'
|
||||
|
||||
battery_level_characteristic: Characteristic[int]
|
||||
|
||||
def __init__(self, read_battery_level):
|
||||
self.battery_level_characteristic = PackedCharacteristicAdapter(
|
||||
Characteristic(
|
||||
@@ -49,13 +57,15 @@ class BatteryService(TemplateService):
|
||||
class BatteryServiceProxy(ProfileServiceProxy):
|
||||
SERVICE_CLASS = BatteryService
|
||||
|
||||
battery_level: Optional[CharacteristicProxy[int]]
|
||||
|
||||
def __init__(self, service_proxy):
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(
|
||||
GATT_BATTERY_LEVEL_CHARACTERISTIC
|
||||
):
|
||||
self.battery_level = PackedCharacteristicAdapter(
|
||||
self.battery_level = PackedCharacteristicProxyAdapter(
|
||||
characteristics[0], pack_format=BatteryService.BATTERY_LEVEL_FORMAT
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
import struct
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from bumble.gatt_client import ServiceProxy, ProfileServiceProxy, CharacteristicProxy
|
||||
from bumble.gatt import (
|
||||
GATT_DEVICE_INFORMATION_SERVICE,
|
||||
GATT_FIRMWARE_REVISION_STRING_CHARACTERISTIC,
|
||||
@@ -32,9 +31,12 @@ from bumble.gatt import (
|
||||
GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC,
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
DelegatedCharacteristicAdapter,
|
||||
UTF8CharacteristicAdapter,
|
||||
)
|
||||
from bumble.gatt_adapters import (
|
||||
DelegatedCharacteristicProxyAdapter,
|
||||
UTF8CharacteristicProxyAdapter,
|
||||
)
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -62,7 +64,7 @@ class DeviceInformationService(TemplateService):
|
||||
ieee_regulatory_certification_data_list: Optional[bytes] = None,
|
||||
# TODO: pnp_id
|
||||
):
|
||||
characteristics = [
|
||||
characteristics: list[Characteristic[bytes]] = [
|
||||
Characteristic(
|
||||
uuid,
|
||||
Characteristic.Properties.READ,
|
||||
@@ -107,14 +109,14 @@ class DeviceInformationService(TemplateService):
|
||||
class DeviceInformationServiceProxy(ProfileServiceProxy):
|
||||
SERVICE_CLASS = DeviceInformationService
|
||||
|
||||
manufacturer_name: Optional[UTF8CharacteristicAdapter]
|
||||
model_number: Optional[UTF8CharacteristicAdapter]
|
||||
serial_number: Optional[UTF8CharacteristicAdapter]
|
||||
hardware_revision: Optional[UTF8CharacteristicAdapter]
|
||||
firmware_revision: Optional[UTF8CharacteristicAdapter]
|
||||
software_revision: Optional[UTF8CharacteristicAdapter]
|
||||
system_id: Optional[DelegatedCharacteristicAdapter]
|
||||
ieee_regulatory_certification_data_list: Optional[CharacteristicProxy]
|
||||
manufacturer_name: Optional[CharacteristicProxy[str]]
|
||||
model_number: Optional[CharacteristicProxy[str]]
|
||||
serial_number: Optional[CharacteristicProxy[str]]
|
||||
hardware_revision: Optional[CharacteristicProxy[str]]
|
||||
firmware_revision: Optional[CharacteristicProxy[str]]
|
||||
software_revision: Optional[CharacteristicProxy[str]]
|
||||
system_id: Optional[CharacteristicProxy[tuple[int, int]]]
|
||||
ieee_regulatory_certification_data_list: Optional[CharacteristicProxy[bytes]]
|
||||
|
||||
def __init__(self, service_proxy: ServiceProxy):
|
||||
self.service_proxy = service_proxy
|
||||
@@ -128,7 +130,7 @@ class DeviceInformationServiceProxy(ProfileServiceProxy):
|
||||
('software_revision', GATT_SOFTWARE_REVISION_STRING_CHARACTERISTIC),
|
||||
):
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(uuid):
|
||||
characteristic = UTF8CharacteristicAdapter(characteristics[0])
|
||||
characteristic = UTF8CharacteristicProxyAdapter(characteristics[0])
|
||||
else:
|
||||
characteristic = None
|
||||
self.__setattr__(field, characteristic)
|
||||
@@ -136,7 +138,7 @@ class DeviceInformationServiceProxy(ProfileServiceProxy):
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(
|
||||
GATT_SYSTEM_ID_CHARACTERISTIC
|
||||
):
|
||||
self.system_id = DelegatedCharacteristicAdapter(
|
||||
self.system_id = DelegatedCharacteristicProxyAdapter(
|
||||
characteristics[0],
|
||||
encode=lambda v: DeviceInformationService.pack_system_id(*v),
|
||||
decode=DeviceInformationService.unpack_system_id,
|
||||
|
||||
@@ -25,14 +25,15 @@ from bumble.core import Appearance
|
||||
from bumble.gatt import (
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
CharacteristicAdapter,
|
||||
DelegatedCharacteristicAdapter,
|
||||
UTF8CharacteristicAdapter,
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||
GATT_APPEARANCE_CHARACTERISTIC,
|
||||
)
|
||||
from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
|
||||
from bumble.gatt_adapters import (
|
||||
DelegatedCharacteristicProxyAdapter,
|
||||
UTF8CharacteristicProxyAdapter,
|
||||
)
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -49,6 +50,9 @@ logger = logging.getLogger(__name__)
|
||||
class GenericAccessService(TemplateService):
|
||||
UUID = GATT_GENERIC_ACCESS_SERVICE
|
||||
|
||||
device_name_characteristic: Characteristic[bytes]
|
||||
appearance_characteristic: Characteristic[bytes]
|
||||
|
||||
def __init__(
|
||||
self, device_name: str, appearance: Union[Appearance, Tuple[int, int], int] = 0
|
||||
):
|
||||
@@ -84,8 +88,8 @@ class GenericAccessService(TemplateService):
|
||||
class GenericAccessServiceProxy(ProfileServiceProxy):
|
||||
SERVICE_CLASS = GenericAccessService
|
||||
|
||||
device_name: Optional[CharacteristicAdapter]
|
||||
appearance: Optional[DelegatedCharacteristicAdapter]
|
||||
device_name: Optional[CharacteristicProxy[str]]
|
||||
appearance: Optional[CharacteristicProxy[Appearance]]
|
||||
|
||||
def __init__(self, service_proxy: ServiceProxy):
|
||||
self.service_proxy = service_proxy
|
||||
@@ -93,14 +97,14 @@ class GenericAccessServiceProxy(ProfileServiceProxy):
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC
|
||||
):
|
||||
self.device_name = UTF8CharacteristicAdapter(characteristics[0])
|
||||
self.device_name = UTF8CharacteristicProxyAdapter(characteristics[0])
|
||||
else:
|
||||
self.device_name = None
|
||||
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(
|
||||
GATT_APPEARANCE_CHARACTERISTIC
|
||||
):
|
||||
self.appearance = DelegatedCharacteristicAdapter(
|
||||
self.appearance = DelegatedCharacteristicProxyAdapter(
|
||||
characteristics[0],
|
||||
decode=lambda value: Appearance.from_int(
|
||||
struct.unpack_from('<H', value, 0)[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,
|
||||
@@ -110,6 +110,7 @@ class GenericAttributeProfileService(gatt.TemplateService):
|
||||
gatt.GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||
gatt.GATT_CHARACTERISTIC_EXTENDED_PROPERTIES_DESCRIPTOR,
|
||||
):
|
||||
assert isinstance(attribute.value, bytes)
|
||||
return (
|
||||
struct.pack("<H", attribute.handle)
|
||||
+ attribute.type.to_bytes()
|
||||
@@ -142,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',
|
||||
|
||||
@@ -22,7 +22,6 @@ from typing import Optional
|
||||
|
||||
from bumble.gatt import (
|
||||
TemplateService,
|
||||
DelegatedCharacteristicAdapter,
|
||||
Characteristic,
|
||||
GATT_GAMING_AUDIO_SERVICE,
|
||||
GATT_GMAP_ROLE_CHARACTERISTIC,
|
||||
@@ -31,7 +30,8 @@ from bumble.gatt import (
|
||||
GATT_BGS_FEATURES_CHARACTERISTIC,
|
||||
GATT_BGR_FEATURES_CHARACTERISTIC,
|
||||
)
|
||||
from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
|
||||
from bumble.gatt_adapters import DelegatedCharacteristicProxyAdapter
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
||||
from enum import IntFlag
|
||||
|
||||
|
||||
@@ -150,10 +150,15 @@ class GamingAudioService(TemplateService):
|
||||
class GamingAudioServiceProxy(ProfileServiceProxy):
|
||||
SERVICE_CLASS = GamingAudioService
|
||||
|
||||
ugg_features: Optional[CharacteristicProxy[UggFeatures]] = None
|
||||
ugt_features: Optional[CharacteristicProxy[UgtFeatures]] = None
|
||||
bgs_features: Optional[CharacteristicProxy[BgsFeatures]] = None
|
||||
bgr_features: Optional[CharacteristicProxy[BgrFeatures]] = None
|
||||
|
||||
def __init__(self, service_proxy: ServiceProxy) -> None:
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
self.gmap_role = DelegatedCharacteristicAdapter(
|
||||
self.gmap_role = DelegatedCharacteristicProxyAdapter(
|
||||
service_proxy.get_required_characteristic_by_uuid(
|
||||
GATT_GMAP_ROLE_CHARACTERISTIC
|
||||
),
|
||||
@@ -163,31 +168,31 @@ class GamingAudioServiceProxy(ProfileServiceProxy):
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(
|
||||
GATT_UGG_FEATURES_CHARACTERISTIC
|
||||
):
|
||||
self.ugg_features = DelegatedCharacteristicAdapter(
|
||||
characteristic=characteristics[0],
|
||||
self.ugg_features = DelegatedCharacteristicProxyAdapter(
|
||||
characteristics[0],
|
||||
decode=lambda value: UggFeatures(value[0]),
|
||||
)
|
||||
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(
|
||||
GATT_UGT_FEATURES_CHARACTERISTIC
|
||||
):
|
||||
self.ugt_features = DelegatedCharacteristicAdapter(
|
||||
characteristic=characteristics[0],
|
||||
self.ugt_features = DelegatedCharacteristicProxyAdapter(
|
||||
characteristics[0],
|
||||
decode=lambda value: UgtFeatures(value[0]),
|
||||
)
|
||||
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(
|
||||
GATT_BGS_FEATURES_CHARACTERISTIC
|
||||
):
|
||||
self.bgs_features = DelegatedCharacteristicAdapter(
|
||||
characteristic=characteristics[0],
|
||||
self.bgs_features = DelegatedCharacteristicProxyAdapter(
|
||||
characteristics[0],
|
||||
decode=lambda value: BgsFeatures(value[0]),
|
||||
)
|
||||
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(
|
||||
GATT_BGR_FEATURES_CHARACTERISTIC
|
||||
):
|
||||
self.bgr_features = DelegatedCharacteristicAdapter(
|
||||
characteristic=characteristics[0],
|
||||
self.bgr_features = DelegatedCharacteristicProxyAdapter(
|
||||
characteristics[0],
|
||||
decode=lambda value: BgrFeatures(value[0]),
|
||||
)
|
||||
|
||||
@@ -18,20 +18,21 @@
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import functools
|
||||
from bumble import att, gatt, gatt_client
|
||||
from bumble.core import InvalidArgumentError, InvalidStateError
|
||||
from bumble.device import Device, Connection
|
||||
from bumble.utils import AsyncRunner, OpenIntEnum
|
||||
from bumble.hci import Address
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
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 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
|
||||
@@ -41,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
|
||||
@@ -49,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
|
||||
@@ -129,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
|
||||
@@ -189,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
|
||||
|
||||
@@ -223,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]
|
||||
|
||||
@@ -332,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]
|
||||
@@ -381,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]
|
||||
@@ -631,11 +632,12 @@ class HearingAccessServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
|
||||
hearing_aid_preset_control_point: gatt_client.CharacteristicProxy
|
||||
preset_control_point_indications: asyncio.Queue
|
||||
active_preset_index_notification: asyncio.Queue
|
||||
|
||||
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
self.server_features = gatt.PackedCharacteristicAdapter(
|
||||
self.server_features = gatt_adapters.PackedCharacteristicProxyAdapter(
|
||||
service_proxy.get_characteristics_by_uuid(
|
||||
gatt.GATT_HEARING_AID_FEATURES_CHARACTERISTIC
|
||||
)[0],
|
||||
@@ -648,7 +650,7 @@ class HearingAccessServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
)[0]
|
||||
)
|
||||
|
||||
self.active_preset_index = gatt.PackedCharacteristicAdapter(
|
||||
self.active_preset_index = gatt_adapters.PackedCharacteristicProxyAdapter(
|
||||
service_proxy.get_characteristics_by_uuid(
|
||||
gatt.GATT_ACTIVE_PRESET_INDEX_CHARACTERISTIC
|
||||
)[0],
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
from enum import IntEnum
|
||||
import struct
|
||||
from typing import Optional
|
||||
|
||||
from bumble import core
|
||||
from ..gatt_client import ProfileServiceProxy
|
||||
from ..att import ATT_Error
|
||||
from ..gatt import (
|
||||
from bumble.att import ATT_Error
|
||||
from bumble.gatt import (
|
||||
GATT_HEART_RATE_SERVICE,
|
||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||
GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC,
|
||||
@@ -30,10 +31,13 @@ from ..gatt import (
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
CharacteristicValue,
|
||||
SerializableCharacteristicAdapter,
|
||||
)
|
||||
from bumble.gatt_adapters import (
|
||||
DelegatedCharacteristicAdapter,
|
||||
PackedCharacteristicAdapter,
|
||||
SerializableCharacteristicAdapter,
|
||||
)
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -43,6 +47,10 @@ class HeartRateService(TemplateService):
|
||||
CONTROL_POINT_NOT_SUPPORTED = 0x80
|
||||
RESET_ENERGY_EXPENDED = 0x01
|
||||
|
||||
heart_rate_measurement_characteristic: Characteristic[HeartRateMeasurement]
|
||||
body_sensor_location_characteristic: Characteristic[BodySensorLocation]
|
||||
heart_rate_control_point_characteristic: Characteristic[int]
|
||||
|
||||
class BodySensorLocation(IntEnum):
|
||||
OTHER = 0
|
||||
CHEST = 1
|
||||
@@ -198,6 +206,14 @@ class HeartRateService(TemplateService):
|
||||
class HeartRateServiceProxy(ProfileServiceProxy):
|
||||
SERVICE_CLASS = HeartRateService
|
||||
|
||||
heart_rate_measurement: Optional[
|
||||
CharacteristicProxy[HeartRateService.HeartRateMeasurement]
|
||||
]
|
||||
body_sensor_location: Optional[
|
||||
CharacteristicProxy[HeartRateService.BodySensorLocation]
|
||||
]
|
||||
heart_rate_control_point: Optional[CharacteristicProxy[int]]
|
||||
|
||||
def __init__(self, service_proxy):
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ class MediaControlService(gatt.TemplateService):
|
||||
properties=gatt.Characteristic.Properties.READ
|
||||
| gatt.Characteristic.Properties.NOTIFY,
|
||||
permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
|
||||
value=media_player_name or 'Bumble Player',
|
||||
value=(media_player_name or 'Bumble Player').encode(),
|
||||
)
|
||||
self.track_changed_characteristic = gatt.Characteristic(
|
||||
uuid=gatt.GATT_TRACK_CHANGED_CHARACTERISTIC,
|
||||
@@ -247,14 +247,16 @@ class MediaControlService(gatt.TemplateService):
|
||||
permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
|
||||
value=b'',
|
||||
)
|
||||
self.media_control_point_characteristic = gatt.Characteristic(
|
||||
uuid=gatt.GATT_MEDIA_CONTROL_POINT_CHARACTERISTIC,
|
||||
properties=gatt.Characteristic.Properties.WRITE
|
||||
| gatt.Characteristic.Properties.WRITE_WITHOUT_RESPONSE
|
||||
| gatt.Characteristic.Properties.NOTIFY,
|
||||
permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION
|
||||
| gatt.Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION,
|
||||
value=gatt.CharacteristicValue(write=self.on_media_control_point),
|
||||
self.media_control_point_characteristic: gatt.Characteristic[bytes] = (
|
||||
gatt.Characteristic(
|
||||
uuid=gatt.GATT_MEDIA_CONTROL_POINT_CHARACTERISTIC,
|
||||
properties=gatt.Characteristic.Properties.WRITE
|
||||
| gatt.Characteristic.Properties.WRITE_WITHOUT_RESPONSE
|
||||
| gatt.Characteristic.Properties.NOTIFY,
|
||||
permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION
|
||||
| gatt.Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION,
|
||||
value=gatt.CharacteristicValue(write=self.on_media_control_point),
|
||||
)
|
||||
)
|
||||
self.media_control_point_opcodes_supported_characteristic = gatt.Characteristic(
|
||||
uuid=gatt.GATT_MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_CHARACTERISTIC,
|
||||
@@ -336,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]
|
||||
|
||||
@@ -25,6 +25,7 @@ from typing import Optional, Sequence, Union
|
||||
from bumble.profiles.bap import AudioLocation, CodecSpecificCapabilities, ContextType
|
||||
from bumble.profiles import le_audio
|
||||
from bumble import gatt
|
||||
from bumble import gatt_adapters
|
||||
from bumble import gatt_client
|
||||
from bumble import hci
|
||||
|
||||
@@ -103,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,
|
||||
@@ -185,34 +186,42 @@ class PublishedAudioCapabilitiesService(gatt.TemplateService):
|
||||
class PublishedAudioCapabilitiesServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
SERVICE_CLASS = PublishedAudioCapabilitiesService
|
||||
|
||||
sink_pac: Optional[gatt.DelegatedCharacteristicAdapter] = None
|
||||
sink_audio_locations: Optional[gatt.DelegatedCharacteristicAdapter] = None
|
||||
source_pac: Optional[gatt.DelegatedCharacteristicAdapter] = None
|
||||
source_audio_locations: Optional[gatt.DelegatedCharacteristicAdapter] = None
|
||||
available_audio_contexts: gatt.DelegatedCharacteristicAdapter
|
||||
supported_audio_contexts: gatt.DelegatedCharacteristicAdapter
|
||||
sink_pac: Optional[gatt_client.CharacteristicProxy[list[PacRecord]]] = None
|
||||
sink_audio_locations: Optional[gatt_client.CharacteristicProxy[AudioLocation]] = (
|
||||
None
|
||||
)
|
||||
source_pac: Optional[gatt_client.CharacteristicProxy[list[PacRecord]]] = None
|
||||
source_audio_locations: Optional[gatt_client.CharacteristicProxy[AudioLocation]] = (
|
||||
None
|
||||
)
|
||||
available_audio_contexts: gatt_client.CharacteristicProxy[tuple[ContextType, ...]]
|
||||
supported_audio_contexts: gatt_client.CharacteristicProxy[tuple[ContextType, ...]]
|
||||
|
||||
def __init__(self, service_proxy: gatt_client.ServiceProxy):
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
self.available_audio_contexts = gatt.DelegatedCharacteristicAdapter(
|
||||
service_proxy.get_required_characteristic_by_uuid(
|
||||
gatt.GATT_AVAILABLE_AUDIO_CONTEXTS_CHARACTERISTIC
|
||||
),
|
||||
decode=lambda x: tuple(map(ContextType, struct.unpack('<HH', x))),
|
||||
self.available_audio_contexts = (
|
||||
gatt_adapters.DelegatedCharacteristicProxyAdapter(
|
||||
service_proxy.get_required_characteristic_by_uuid(
|
||||
gatt.GATT_AVAILABLE_AUDIO_CONTEXTS_CHARACTERISTIC
|
||||
),
|
||||
decode=lambda x: tuple(map(ContextType, struct.unpack('<HH', x))),
|
||||
)
|
||||
)
|
||||
|
||||
self.supported_audio_contexts = gatt.DelegatedCharacteristicAdapter(
|
||||
service_proxy.get_required_characteristic_by_uuid(
|
||||
gatt.GATT_SUPPORTED_AUDIO_CONTEXTS_CHARACTERISTIC
|
||||
),
|
||||
decode=lambda x: tuple(map(ContextType, struct.unpack('<HH', x))),
|
||||
self.supported_audio_contexts = (
|
||||
gatt_adapters.DelegatedCharacteristicProxyAdapter(
|
||||
service_proxy.get_required_characteristic_by_uuid(
|
||||
gatt.GATT_SUPPORTED_AUDIO_CONTEXTS_CHARACTERISTIC
|
||||
),
|
||||
decode=lambda x: tuple(map(ContextType, struct.unpack('<HH', x))),
|
||||
)
|
||||
)
|
||||
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(
|
||||
gatt.GATT_SINK_PAC_CHARACTERISTIC
|
||||
):
|
||||
self.sink_pac = gatt.DelegatedCharacteristicAdapter(
|
||||
self.sink_pac = gatt_adapters.DelegatedCharacteristicProxyAdapter(
|
||||
characteristics[0],
|
||||
decode=PacRecord.list_from_bytes,
|
||||
)
|
||||
@@ -220,7 +229,7 @@ class PublishedAudioCapabilitiesServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(
|
||||
gatt.GATT_SOURCE_PAC_CHARACTERISTIC
|
||||
):
|
||||
self.source_pac = gatt.DelegatedCharacteristicAdapter(
|
||||
self.source_pac = gatt_adapters.DelegatedCharacteristicProxyAdapter(
|
||||
characteristics[0],
|
||||
decode=PacRecord.list_from_bytes,
|
||||
)
|
||||
@@ -228,15 +237,19 @@ class PublishedAudioCapabilitiesServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(
|
||||
gatt.GATT_SINK_AUDIO_LOCATION_CHARACTERISTIC
|
||||
):
|
||||
self.sink_audio_locations = gatt.DelegatedCharacteristicAdapter(
|
||||
characteristics[0],
|
||||
decode=lambda x: AudioLocation(struct.unpack('<I', x)[0]),
|
||||
self.sink_audio_locations = (
|
||||
gatt_adapters.DelegatedCharacteristicProxyAdapter(
|
||||
characteristics[0],
|
||||
decode=lambda x: AudioLocation(struct.unpack('<I', x)[0]),
|
||||
)
|
||||
)
|
||||
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(
|
||||
gatt.GATT_SOURCE_AUDIO_LOCATION_CHARACTERISTIC
|
||||
):
|
||||
self.source_audio_locations = gatt.DelegatedCharacteristicAdapter(
|
||||
characteristics[0],
|
||||
decode=lambda x: AudioLocation(struct.unpack('<I', x)[0]),
|
||||
self.source_audio_locations = (
|
||||
gatt_adapters.DelegatedCharacteristicProxyAdapter(
|
||||
characteristics[0],
|
||||
decode=lambda x: AudioLocation(struct.unpack('<I', x)[0]),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -24,11 +24,11 @@ import struct
|
||||
from bumble.gatt import (
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
DelegatedCharacteristicAdapter,
|
||||
GATT_TELEPHONY_AND_MEDIA_AUDIO_SERVICE,
|
||||
GATT_TMAP_ROLE_CHARACTERISTIC,
|
||||
)
|
||||
from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
|
||||
from bumble.gatt_adapters import DelegatedCharacteristicProxyAdapter
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -53,6 +53,8 @@ class Role(enum.IntFlag):
|
||||
class TelephonyAndMediaAudioService(TemplateService):
|
||||
UUID = GATT_TELEPHONY_AND_MEDIA_AUDIO_SERVICE
|
||||
|
||||
role_characteristic: Characteristic[bytes]
|
||||
|
||||
def __init__(self, role: Role):
|
||||
self.role_characteristic = Characteristic(
|
||||
GATT_TMAP_ROLE_CHARACTERISTIC,
|
||||
@@ -68,12 +70,12 @@ class TelephonyAndMediaAudioService(TemplateService):
|
||||
class TelephonyAndMediaAudioServiceProxy(ProfileServiceProxy):
|
||||
SERVICE_CLASS = TelephonyAndMediaAudioService
|
||||
|
||||
role: DelegatedCharacteristicAdapter
|
||||
role: CharacteristicProxy[Role]
|
||||
|
||||
def __init__(self, service_proxy: ServiceProxy):
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
self.role = DelegatedCharacteristicAdapter(
|
||||
self.role = DelegatedCharacteristicProxyAdapter(
|
||||
service_proxy.get_required_characteristic_by_uuid(
|
||||
GATT_TMAP_ROLE_CHARACTERISTIC
|
||||
),
|
||||
|
||||
@@ -23,8 +23,10 @@ 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
|
||||
from bumble import gatt_client
|
||||
|
||||
|
||||
@@ -89,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
|
||||
@@ -159,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),
|
||||
)
|
||||
@@ -209,14 +212,14 @@ class VolumeControlService(gatt.TemplateService):
|
||||
class VolumeControlServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
SERVICE_CLASS = VolumeControlService
|
||||
|
||||
volume_control_point: gatt_client.CharacteristicProxy
|
||||
volume_state: gatt.SerializableCharacteristicAdapter
|
||||
volume_flags: gatt.DelegatedCharacteristicAdapter
|
||||
volume_control_point: gatt_client.CharacteristicProxy[bytes]
|
||||
volume_state: gatt_client.CharacteristicProxy[VolumeState]
|
||||
volume_flags: gatt_client.CharacteristicProxy[VolumeFlags]
|
||||
|
||||
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
self.volume_state = gatt.SerializableCharacteristicAdapter(
|
||||
self.volume_state = gatt_adapters.SerializableCharacteristicProxyAdapter(
|
||||
service_proxy.get_required_characteristic_by_uuid(
|
||||
gatt.GATT_VOLUME_STATE_CHARACTERISTIC
|
||||
),
|
||||
@@ -227,7 +230,7 @@ class VolumeControlServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
gatt.GATT_VOLUME_CONTROL_POINT_CHARACTERISTIC
|
||||
)
|
||||
|
||||
self.volume_flags = gatt.DelegatedCharacteristicAdapter(
|
||||
self.volume_flags = gatt_adapters.DelegatedCharacteristicProxyAdapter(
|
||||
service_proxy.get_required_characteristic_by_uuid(
|
||||
gatt.GATT_VOLUME_FLAGS_CHARACTERISTIC
|
||||
),
|
||||
|
||||
@@ -24,19 +24,21 @@ from bumble.device import Connection
|
||||
from bumble.att import ATT_Error
|
||||
from bumble.gatt import (
|
||||
Characteristic,
|
||||
DelegatedCharacteristicAdapter,
|
||||
TemplateService,
|
||||
CharacteristicValue,
|
||||
SerializableCharacteristicAdapter,
|
||||
UTF8CharacteristicAdapter,
|
||||
GATT_VOLUME_OFFSET_CONTROL_SERVICE,
|
||||
GATT_VOLUME_OFFSET_STATE_CHARACTERISTIC,
|
||||
GATT_AUDIO_LOCATION_CHARACTERISTIC,
|
||||
GATT_VOLUME_OFFSET_CONTROL_POINT_CHARACTERISTIC,
|
||||
GATT_AUDIO_OUTPUT_DESCRIPTION_CHARACTERISTIC,
|
||||
)
|
||||
from bumble.gatt_adapters import (
|
||||
DelegatedCharacteristicProxyAdapter,
|
||||
SerializableCharacteristicProxyAdapter,
|
||||
UTF8CharacteristicProxyAdapter,
|
||||
)
|
||||
from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
|
||||
from bumble.utils import OpenIntEnum
|
||||
from bumble import utils
|
||||
from bumble.profiles.bap import AudioLocation
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -48,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.
|
||||
"""
|
||||
@@ -67,7 +69,7 @@ class ErrorCode(OpenIntEnum):
|
||||
class VolumeOffsetState:
|
||||
volume_offset: int = 0
|
||||
change_counter: int = 0
|
||||
attribute_value: Optional[CharacteristicValue] = None
|
||||
attribute: Optional[Characteristic] = None
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return struct.pack('<hB', self.volume_offset, self.change_counter)
|
||||
@@ -81,8 +83,8 @@ class VolumeOffsetState:
|
||||
self.change_counter = (self.change_counter + 1) % (CHANGE_COUNTER_MAX_VALUE + 1)
|
||||
|
||||
async def notify_subscribers_via_connection(self, connection: Connection) -> None:
|
||||
assert self.attribute_value is not None
|
||||
await connection.device.notify_subscribers(attribute=self.attribute_value)
|
||||
assert self.attribute is not None
|
||||
await connection.device.notify_subscribers(attribute=self.attribute)
|
||||
|
||||
def on_read(self, _connection: Optional[Connection]) -> bytes:
|
||||
return bytes(self)
|
||||
@@ -91,7 +93,7 @@ class VolumeOffsetState:
|
||||
@dataclass
|
||||
class VocsAudioLocation:
|
||||
audio_location: AudioLocation = AudioLocation.NOT_ALLOWED
|
||||
attribute_value: Optional[CharacteristicValue] = None
|
||||
attribute: Optional[Characteristic] = None
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return struct.pack('<I', self.audio_location)
|
||||
@@ -106,10 +108,10 @@ class VocsAudioLocation:
|
||||
|
||||
async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
|
||||
assert connection
|
||||
assert self.attribute_value
|
||||
assert self.attribute
|
||||
|
||||
self.audio_location = AudioLocation(int.from_bytes(value, 'little'))
|
||||
await connection.device.notify_subscribers(attribute=self.attribute_value)
|
||||
await connection.device.notify_subscribers(attribute=self.attribute)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -148,7 +150,7 @@ class VolumeOffsetControlPoint:
|
||||
@dataclass
|
||||
class AudioOutputDescription:
|
||||
audio_output_description: str = ''
|
||||
attribute_value: Optional[CharacteristicValue] = None
|
||||
attribute: Optional[Characteristic] = None
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data: bytes):
|
||||
@@ -162,10 +164,10 @@ class AudioOutputDescription:
|
||||
|
||||
async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
|
||||
assert connection
|
||||
assert self.attribute_value
|
||||
assert self.attribute
|
||||
|
||||
self.audio_output_description = value.decode('utf-8')
|
||||
await connection.device.notify_subscribers(attribute=self.attribute_value)
|
||||
await connection.device.notify_subscribers(attribute=self.attribute)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -197,7 +199,7 @@ class VolumeOffsetControlService(TemplateService):
|
||||
VolumeOffsetControlPoint(self.volume_offset_state)
|
||||
)
|
||||
|
||||
self.volume_offset_state_characteristic = Characteristic(
|
||||
self.volume_offset_state_characteristic: Characteristic[bytes] = Characteristic(
|
||||
uuid=GATT_VOLUME_OFFSET_STATE_CHARACTERISTIC,
|
||||
properties=(
|
||||
Characteristic.Properties.READ | Characteristic.Properties.NOTIFY
|
||||
@@ -206,7 +208,7 @@ class VolumeOffsetControlService(TemplateService):
|
||||
value=CharacteristicValue(read=self.volume_offset_state.on_read),
|
||||
)
|
||||
|
||||
self.audio_location_characteristic = Characteristic(
|
||||
self.audio_location_characteristic: Characteristic[bytes] = Characteristic(
|
||||
uuid=GATT_AUDIO_LOCATION_CHARACTERISTIC,
|
||||
properties=(
|
||||
Characteristic.Properties.READ
|
||||
@@ -222,33 +224,39 @@ class VolumeOffsetControlService(TemplateService):
|
||||
write=self.audio_location.on_write,
|
||||
),
|
||||
)
|
||||
self.audio_location.attribute_value = self.audio_location_characteristic.value
|
||||
self.audio_location.attribute = self.audio_location_characteristic
|
||||
|
||||
self.volume_offset_control_point_characteristic = Characteristic(
|
||||
uuid=GATT_VOLUME_OFFSET_CONTROL_POINT_CHARACTERISTIC,
|
||||
properties=Characteristic.Properties.WRITE,
|
||||
permissions=Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION,
|
||||
value=CharacteristicValue(write=self.volume_offset_control_point.on_write),
|
||||
self.volume_offset_control_point_characteristic: Characteristic[bytes] = (
|
||||
Characteristic(
|
||||
uuid=GATT_VOLUME_OFFSET_CONTROL_POINT_CHARACTERISTIC,
|
||||
properties=Characteristic.Properties.WRITE,
|
||||
permissions=Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION,
|
||||
value=CharacteristicValue(
|
||||
write=self.volume_offset_control_point.on_write
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
self.audio_output_description_characteristic = Characteristic(
|
||||
uuid=GATT_AUDIO_OUTPUT_DESCRIPTION_CHARACTERISTIC,
|
||||
properties=(
|
||||
Characteristic.Properties.READ
|
||||
| Characteristic.Properties.NOTIFY
|
||||
| Characteristic.Properties.WRITE_WITHOUT_RESPONSE
|
||||
),
|
||||
permissions=(
|
||||
Characteristic.Permissions.READ_REQUIRES_ENCRYPTION
|
||||
| Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION
|
||||
),
|
||||
value=CharacteristicValue(
|
||||
read=self.audio_output_description.on_read,
|
||||
write=self.audio_output_description.on_write,
|
||||
),
|
||||
self.audio_output_description_characteristic: Characteristic[bytes] = (
|
||||
Characteristic(
|
||||
uuid=GATT_AUDIO_OUTPUT_DESCRIPTION_CHARACTERISTIC,
|
||||
properties=(
|
||||
Characteristic.Properties.READ
|
||||
| Characteristic.Properties.NOTIFY
|
||||
| Characteristic.Properties.WRITE_WITHOUT_RESPONSE
|
||||
),
|
||||
permissions=(
|
||||
Characteristic.Permissions.READ_REQUIRES_ENCRYPTION
|
||||
| Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION
|
||||
),
|
||||
value=CharacteristicValue(
|
||||
read=self.audio_output_description.on_read,
|
||||
write=self.audio_output_description.on_write,
|
||||
),
|
||||
)
|
||||
)
|
||||
self.audio_output_description.attribute_value = (
|
||||
self.audio_output_description_characteristic.value
|
||||
self.audio_output_description.attribute = (
|
||||
self.audio_output_description_characteristic
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
@@ -271,14 +279,14 @@ class VolumeOffsetControlServiceProxy(ProfileServiceProxy):
|
||||
def __init__(self, service_proxy: ServiceProxy) -> None:
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
self.volume_offset_state = SerializableCharacteristicAdapter(
|
||||
self.volume_offset_state = SerializableCharacteristicProxyAdapter(
|
||||
service_proxy.get_required_characteristic_by_uuid(
|
||||
GATT_VOLUME_OFFSET_STATE_CHARACTERISTIC
|
||||
),
|
||||
VolumeOffsetState,
|
||||
)
|
||||
|
||||
self.audio_location = DelegatedCharacteristicAdapter(
|
||||
self.audio_location = DelegatedCharacteristicProxyAdapter(
|
||||
service_proxy.get_required_characteristic_by_uuid(
|
||||
GATT_AUDIO_LOCATION_CHARACTERISTIC
|
||||
),
|
||||
@@ -292,7 +300,7 @@ class VolumeOffsetControlServiceProxy(ProfileServiceProxy):
|
||||
)
|
||||
)
|
||||
|
||||
self.audio_output_description = UTF8CharacteristicAdapter(
|
||||
self.audio_output_description = UTF8CharacteristicProxyAdapter(
|
||||
service_proxy.get_required_characteristic_by_uuid(
|
||||
GATT_AUDIO_OUTPUT_DESCRIPTION_CHARACTERISTIC
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
181
bumble/utils.py
181
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):
|
||||
"""
|
||||
@@ -502,3 +515,13 @@ class ByteSerializable(Protocol):
|
||||
def from_bytes(cls, data: bytes) -> Self: ...
|
||||
|
||||
def __bytes__(self) -> bytes: ...
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class IntConvertible(Protocol):
|
||||
"""
|
||||
Type protocol for classes that can be instantiated from int and converted to int.
|
||||
"""
|
||||
|
||||
def __init__(self, value: int) -> None: ...
|
||||
def __int__(self) -> int: ...
|
||||
|
||||
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
|
||||
@@ -102,7 +102,6 @@ async def main() -> None:
|
||||
)
|
||||
|
||||
# Notify subscribers of the current value as soon as they subscribe
|
||||
@heart_rate_service.heart_rate_measurement_characteristic.on('subscription')
|
||||
def on_subscription(connection, notify_enabled, indicate_enabled):
|
||||
if notify_enabled or indicate_enabled:
|
||||
AsyncRunner.spawn(
|
||||
@@ -112,6 +111,10 @@ async def main() -> None:
|
||||
)
|
||||
)
|
||||
|
||||
heart_rate_service.heart_rate_measurement_characteristic.on(
|
||||
'subscription', on_subscription
|
||||
)
|
||||
|
||||
# Go!
|
||||
await device.power_on()
|
||||
await device.start_advertising(auto_restart=True)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -70,13 +70,13 @@ async def main() -> None:
|
||||
descriptor = Descriptor(
|
||||
GATT_CHARACTERISTIC_USER_DESCRIPTION_DESCRIPTOR,
|
||||
Descriptor.READABLE,
|
||||
'My Description',
|
||||
'My Description'.encode(),
|
||||
)
|
||||
manufacturer_name_characteristic = Characteristic(
|
||||
manufacturer_name_characteristic = Characteristic[bytes](
|
||||
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
|
||||
Characteristic.Properties.READ,
|
||||
Characteristic.READABLE,
|
||||
"Fitbit",
|
||||
"Fitbit".encode(),
|
||||
[descriptor],
|
||||
)
|
||||
device_info_service = Service(
|
||||
|
||||
@@ -94,13 +94,13 @@ async def main() -> None:
|
||||
descriptor = Descriptor(
|
||||
GATT_CHARACTERISTIC_USER_DESCRIPTION_DESCRIPTOR,
|
||||
Descriptor.READABLE,
|
||||
'My Description',
|
||||
'My Description'.encode(),
|
||||
)
|
||||
manufacturer_name_characteristic = Characteristic(
|
||||
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
|
||||
Characteristic.Properties.READ,
|
||||
Characteristic.READABLE,
|
||||
'Fitbit',
|
||||
'Fitbit'.encode(),
|
||||
[descriptor],
|
||||
)
|
||||
device_info_service = Service(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2024 Google LLC
|
||||
# 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.
|
||||
@@ -18,6 +18,8 @@
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import functools
|
||||
import enum
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
@@ -28,6 +30,8 @@ from typing import Any, List, Union
|
||||
from bumble.device import Device, Peer
|
||||
from bumble import transport
|
||||
from bumble import gatt
|
||||
from bumble import gatt_adapters
|
||||
from bumble import gatt_client
|
||||
from bumble import hci
|
||||
from bumble import core
|
||||
|
||||
@@ -36,6 +40,9 @@ from bumble import core
|
||||
SERVICE_UUID = core.UUID("50DB505C-8AC4-4738-8448-3B1D9CC09CC5")
|
||||
CHARACTERISTIC_UUID_BASE = "D901B45B-4916-412E-ACCA-0000000000"
|
||||
|
||||
DEFAULT_CLIENT_ADDRESS = "F0:F1:F2:F3:F4:F5"
|
||||
DEFAULT_SERVER_ADDRESS = "F1:F2:F3:F4:F5:F6"
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclasses.dataclass
|
||||
@@ -65,6 +72,12 @@ class CustomClass:
|
||||
return struct.pack(">II", self.a, self.b)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class CustomEnum(enum.IntEnum):
|
||||
FOO = 1234
|
||||
BAR = 5678
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def client(device: Device, address: hci.Address) -> None:
|
||||
print(f'=== Connecting to {address}...')
|
||||
@@ -78,8 +91,8 @@ async def client(device: Device, address: hci.Address) -> None:
|
||||
print("*** Discovery complete")
|
||||
|
||||
service = peer.get_services_by_uuid(SERVICE_UUID)[0]
|
||||
characteristics = []
|
||||
for index in range(1, 9):
|
||||
characteristics: list[gatt_client.CharacteristicProxy] = []
|
||||
for index in range(1, 10):
|
||||
characteristics.append(
|
||||
service.get_characteristics_by_uuid(
|
||||
core.UUID(CHARACTERISTIC_UUID_BASE + f"{index:02X}")
|
||||
@@ -91,59 +104,92 @@ async def client(device: Device, address: hci.Address) -> None:
|
||||
value = await characteristic.read_value()
|
||||
print(f"### {characteristic} = {value!r} ({value.hex()})")
|
||||
|
||||
# Subscribe to all characteristics as a raw bytes listener.
|
||||
def on_raw_characteristic_update(characteristic, value):
|
||||
print(f"^^^ Update[RAW] {characteristic.uuid} value = {value.hex()}")
|
||||
|
||||
for characteristic in characteristics:
|
||||
await characteristic.subscribe(
|
||||
functools.partial(on_raw_characteristic_update, characteristic)
|
||||
)
|
||||
|
||||
# Function to subscribe to adapted characteristics
|
||||
def on_adapted_characteristic_update(characteristic, value):
|
||||
print(
|
||||
f"^^^ Update[ADAPTED] {characteristic.uuid} value = {value!r}, "
|
||||
f"type={type(value)}"
|
||||
)
|
||||
|
||||
# Static characteristic with a bytes value.
|
||||
c1 = characteristics[0]
|
||||
c1_value = await c1.read_value()
|
||||
print(f"@@@ C1 {c1} value = {c1_value!r} (type={type(c1_value)})")
|
||||
await c1.write_value("happy π day".encode("utf-8"))
|
||||
await c1.subscribe(functools.partial(on_adapted_characteristic_update, c1))
|
||||
|
||||
# Static characteristic with a string value.
|
||||
c2 = gatt.UTF8CharacteristicAdapter(characteristics[1])
|
||||
c2 = gatt_adapters.UTF8CharacteristicProxyAdapter(characteristics[1])
|
||||
c2_value = await c2.read_value()
|
||||
print(f"@@@ C2 {c2} value = {c2_value} (type={type(c2_value)})")
|
||||
await c2.write_value("happy π day")
|
||||
await c2.subscribe(functools.partial(on_adapted_characteristic_update, c2))
|
||||
|
||||
# Static characteristic with a tuple value.
|
||||
c3 = gatt.PackedCharacteristicAdapter(characteristics[2], ">III")
|
||||
c3 = gatt_adapters.PackedCharacteristicProxyAdapter(characteristics[2], ">III")
|
||||
c3_value = await c3.read_value()
|
||||
print(f"@@@ C3 {c3} value = {c3_value} (type={type(c3_value)})")
|
||||
await c3.write_value((2001, 2002, 2003))
|
||||
await c3.subscribe(functools.partial(on_adapted_characteristic_update, c3))
|
||||
|
||||
# Static characteristic with a named tuple value.
|
||||
c4 = gatt.MappedCharacteristicAdapter(
|
||||
c4 = gatt_adapters.MappedCharacteristicProxyAdapter(
|
||||
characteristics[3], ">III", ["f1", "f2", "f3"]
|
||||
)
|
||||
c4_value = await c4.read_value()
|
||||
print(f"@@@ C4 {c4} value = {c4_value} (type={type(c4_value)})")
|
||||
await c4.write_value({"f1": 4001, "f2": 4002, "f3": 4003})
|
||||
await c4.subscribe(functools.partial(on_adapted_characteristic_update, c4))
|
||||
|
||||
# Static characteristic with a serializable value.
|
||||
c5 = gatt.SerializableCharacteristicAdapter(
|
||||
c5 = gatt_adapters.SerializableCharacteristicProxyAdapter(
|
||||
characteristics[4], CustomSerializableClass
|
||||
)
|
||||
c5_value = await c5.read_value()
|
||||
print(f"@@@ C5 {c5} value = {c5_value} (type={type(c5_value)})")
|
||||
await c5.write_value(CustomSerializableClass(56, 57))
|
||||
await c5.subscribe(functools.partial(on_adapted_characteristic_update, c5))
|
||||
|
||||
# Static characteristic with a delegated value.
|
||||
c6 = gatt.DelegatedCharacteristicAdapter(
|
||||
c6 = gatt_adapters.DelegatedCharacteristicProxyAdapter(
|
||||
characteristics[5], encode=CustomClass.encode, decode=CustomClass.decode
|
||||
)
|
||||
c6_value = await c6.read_value()
|
||||
print(f"@@@ C6 {c6} value = {c6_value} (type={type(c6_value)})")
|
||||
await c6.write_value(CustomClass(6, 7))
|
||||
await c6.subscribe(functools.partial(on_adapted_characteristic_update, c6))
|
||||
|
||||
# Dynamic characteristic with a bytes value.
|
||||
c7 = characteristics[6]
|
||||
c7_value = await c7.read_value()
|
||||
print(f"@@@ C7 {c7} value = {c7_value!r} (type={type(c7_value)})")
|
||||
await c7.write_value(bytes.fromhex("01020304"))
|
||||
await c7.subscribe(functools.partial(on_adapted_characteristic_update, c7))
|
||||
|
||||
# Dynamic characteristic with a string value.
|
||||
c8 = gatt.UTF8CharacteristicAdapter(characteristics[7])
|
||||
c8 = gatt_adapters.UTF8CharacteristicProxyAdapter(characteristics[7])
|
||||
c8_value = await c8.read_value()
|
||||
print(f"@@@ C8 {c8} value = {c8_value} (type={type(c8_value)})")
|
||||
await c8.write_value("howdy")
|
||||
await c8.subscribe(functools.partial(on_adapted_characteristic_update, c8))
|
||||
|
||||
# Static characteristic with an enum value
|
||||
c9 = gatt_adapters.EnumCharacteristicProxyAdapter(
|
||||
characteristics[8], CustomEnum, 3, 'big'
|
||||
)
|
||||
c9_value = await c9.read_value()
|
||||
print(f"@@@ C9 {c9} value = {c9_value.name} (type={type(c9_value)})")
|
||||
await c9.write_value(CustomEnum.BAR)
|
||||
await c9.subscribe(functools.partial(on_adapted_characteristic_update, c9))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -175,142 +221,213 @@ def on_characteristic_write(characteristic: gatt.Characteristic, value: Any) ->
|
||||
print(f"<<< WRITE: {characteristic} <- {value} ({type(value)})")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def server(device: Device) -> None:
|
||||
# Static characteristic with a bytes value.
|
||||
c1 = gatt.Characteristic(
|
||||
CHARACTERISTIC_UUID_BASE + "01",
|
||||
gatt.Characteristic.Properties.READ
|
||||
| gatt.Characteristic.Properties.WRITE
|
||||
| gatt.Characteristic.Properties.NOTIFY,
|
||||
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||
b'hello',
|
||||
)
|
||||
|
||||
# Static characteristic with a string value.
|
||||
c2 = gatt_adapters.UTF8CharacteristicAdapter(
|
||||
gatt.Characteristic(
|
||||
CHARACTERISTIC_UUID_BASE + "02",
|
||||
gatt.Characteristic.Properties.READ
|
||||
| gatt.Characteristic.Properties.WRITE
|
||||
| gatt.Characteristic.Properties.NOTIFY,
|
||||
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||
'hello',
|
||||
)
|
||||
)
|
||||
|
||||
# Static characteristic with a tuple value.
|
||||
c3 = gatt_adapters.PackedCharacteristicAdapter(
|
||||
gatt.Characteristic(
|
||||
CHARACTERISTIC_UUID_BASE + "03",
|
||||
gatt.Characteristic.Properties.READ
|
||||
| gatt.Characteristic.Properties.WRITE
|
||||
| gatt.Characteristic.Properties.NOTIFY,
|
||||
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||
(1007, 1008, 1009),
|
||||
),
|
||||
">III",
|
||||
)
|
||||
|
||||
# Static characteristic with a named tuple value.
|
||||
c4 = gatt_adapters.MappedCharacteristicAdapter(
|
||||
gatt.Characteristic(
|
||||
CHARACTERISTIC_UUID_BASE + "04",
|
||||
gatt.Characteristic.Properties.READ
|
||||
| gatt.Characteristic.Properties.WRITE
|
||||
| gatt.Characteristic.Properties.NOTIFY,
|
||||
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||
{"f1": 3007, "f2": 3008, "f3": 3009},
|
||||
),
|
||||
">III",
|
||||
["f1", "f2", "f3"],
|
||||
)
|
||||
|
||||
# Static characteristic with a serializable value.
|
||||
c5 = gatt_adapters.SerializableCharacteristicAdapter(
|
||||
gatt.Characteristic(
|
||||
CHARACTERISTIC_UUID_BASE + "05",
|
||||
gatt.Characteristic.Properties.READ
|
||||
| gatt.Characteristic.Properties.WRITE
|
||||
| gatt.Characteristic.Properties.NOTIFY,
|
||||
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||
CustomSerializableClass(11, 12),
|
||||
),
|
||||
CustomSerializableClass,
|
||||
)
|
||||
|
||||
# Static characteristic with a delegated value.
|
||||
c6 = gatt_adapters.DelegatedCharacteristicAdapter(
|
||||
gatt.Characteristic(
|
||||
CHARACTERISTIC_UUID_BASE + "06",
|
||||
gatt.Characteristic.Properties.READ
|
||||
| gatt.Characteristic.Properties.WRITE
|
||||
| gatt.Characteristic.Properties.NOTIFY,
|
||||
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||
CustomClass(1, 2),
|
||||
),
|
||||
encode=CustomClass.encode,
|
||||
decode=CustomClass.decode,
|
||||
)
|
||||
|
||||
# Dynamic characteristic with a bytes value.
|
||||
c7 = gatt.Characteristic(
|
||||
CHARACTERISTIC_UUID_BASE + "07",
|
||||
gatt.Characteristic.Properties.READ
|
||||
| gatt.Characteristic.Properties.WRITE
|
||||
| gatt.Characteristic.Properties.NOTIFY,
|
||||
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||
gatt.CharacteristicValue(
|
||||
read=lambda connection: dynamic_read("bytes"),
|
||||
write=lambda connection, value: dynamic_write("bytes", value),
|
||||
),
|
||||
)
|
||||
|
||||
# Dynamic characteristic with a string value.
|
||||
c8 = gatt_adapters.UTF8CharacteristicAdapter(
|
||||
gatt.Characteristic(
|
||||
CHARACTERISTIC_UUID_BASE + "08",
|
||||
gatt.Characteristic.Properties.READ
|
||||
| gatt.Characteristic.Properties.WRITE
|
||||
| gatt.Characteristic.Properties.NOTIFY,
|
||||
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||
gatt.CharacteristicValue(
|
||||
read=lambda connection: dynamic_read("string"),
|
||||
write=lambda connection, value: dynamic_write("string", value),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# Static characteristic with an enum value
|
||||
c9 = gatt_adapters.EnumCharacteristicAdapter(
|
||||
gatt.Characteristic(
|
||||
CHARACTERISTIC_UUID_BASE + "09",
|
||||
gatt.Characteristic.Properties.READ
|
||||
| gatt.Characteristic.Properties.WRITE
|
||||
| gatt.Characteristic.Properties.NOTIFY,
|
||||
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||
CustomEnum.FOO,
|
||||
),
|
||||
cls=CustomEnum,
|
||||
length=3,
|
||||
byteorder='big',
|
||||
)
|
||||
|
||||
characteristics: List[gatt.Characteristic] = [
|
||||
c1,
|
||||
c2,
|
||||
c3,
|
||||
c4,
|
||||
c5,
|
||||
c6,
|
||||
c7,
|
||||
c8,
|
||||
c9,
|
||||
]
|
||||
|
||||
# Listen for read and write events.
|
||||
for characteristic in characteristics:
|
||||
characteristic.on(
|
||||
"read",
|
||||
lambda _, value, c=characteristic: on_characteristic_read(c, value),
|
||||
)
|
||||
characteristic.on(
|
||||
"write",
|
||||
lambda _, value, c=characteristic: on_characteristic_write(c, value),
|
||||
)
|
||||
|
||||
device.add_service(gatt.Service(SERVICE_UUID, characteristics))
|
||||
|
||||
# Notify every 3 seconds
|
||||
i = 0
|
||||
while True:
|
||||
await asyncio.sleep(3)
|
||||
|
||||
# Notifying can be done with the characteristic's current value, or
|
||||
# by explicitly passing a value to notify with. Both variants are used
|
||||
# here: for c1..c4 we set the value and then notify, for c4..c9 we notify
|
||||
# with an explicit value.
|
||||
c1.value = f'hello c1 {i}'.encode()
|
||||
await device.notify_subscribers(c1)
|
||||
c2.value = f'hello c2 {i}'
|
||||
await device.notify_subscribers(c2)
|
||||
c3.value = (1000 + i, 2000 + i, 3000 + i)
|
||||
await device.notify_subscribers(c3)
|
||||
c4.value = {"f1": 4000 + i, "f2": 5000 + i, "f3": 6000 + i}
|
||||
await device.notify_subscribers(c4)
|
||||
await device.notify_subscribers(c5, CustomSerializableClass(1000 + i, 2000 + i))
|
||||
await device.notify_subscribers(c6, CustomClass(3000 + i, 4000 + i))
|
||||
await device.notify_subscribers(c7, bytes([1, 2, 3, i % 256]))
|
||||
await device.notify_subscribers(c8, f'hello c8 {i}')
|
||||
await device.notify_subscribers(
|
||||
c9, CustomEnum.FOO if i % 2 == 0 else CustomEnum.BAR
|
||||
)
|
||||
|
||||
i += 1
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def main() -> None:
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: run_gatt_with_adapters.py <transport-spec> [<bluetooth-address>]")
|
||||
print("example: run_gatt_with_adapters.py usb:0 E1:CA:72:48:C4:E8")
|
||||
print("Usage: run_gatt_with_adapters.py <transport-spec> client|server")
|
||||
print("example: run_gatt_with_adapters.py usb:0 F0:F1:F2:F3:F4:F5")
|
||||
return
|
||||
|
||||
async with await transport.open_transport(sys.argv[1]) as hci_transport:
|
||||
is_client = sys.argv[2] == "client"
|
||||
|
||||
# Create a device to manage the host
|
||||
device = Device.with_hci(
|
||||
"Bumble",
|
||||
hci.Address("F0:F1:F2:F3:F4:F5"),
|
||||
hci.Address(
|
||||
DEFAULT_CLIENT_ADDRESS if is_client else DEFAULT_SERVER_ADDRESS
|
||||
),
|
||||
hci_transport.source,
|
||||
hci_transport.sink,
|
||||
)
|
||||
|
||||
# Static characteristic with a bytes value.
|
||||
c1 = gatt.Characteristic(
|
||||
CHARACTERISTIC_UUID_BASE + "01",
|
||||
gatt.Characteristic.Properties.READ | gatt.Characteristic.Properties.WRITE,
|
||||
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||
b'hello',
|
||||
)
|
||||
|
||||
# Static characteristic with a string value.
|
||||
c2 = gatt.UTF8CharacteristicAdapter(
|
||||
gatt.Characteristic(
|
||||
CHARACTERISTIC_UUID_BASE + "02",
|
||||
gatt.Characteristic.Properties.READ
|
||||
| gatt.Characteristic.Properties.WRITE,
|
||||
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||
'hello',
|
||||
)
|
||||
)
|
||||
|
||||
# Static characteristic with a tuple value.
|
||||
c3 = gatt.PackedCharacteristicAdapter(
|
||||
gatt.Characteristic(
|
||||
CHARACTERISTIC_UUID_BASE + "03",
|
||||
gatt.Characteristic.Properties.READ
|
||||
| gatt.Characteristic.Properties.WRITE,
|
||||
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||
(1007, 1008, 1009),
|
||||
),
|
||||
">III",
|
||||
)
|
||||
|
||||
# Static characteristic with a named tuple value.
|
||||
c4 = gatt.MappedCharacteristicAdapter(
|
||||
gatt.Characteristic(
|
||||
CHARACTERISTIC_UUID_BASE + "04",
|
||||
gatt.Characteristic.Properties.READ
|
||||
| gatt.Characteristic.Properties.WRITE,
|
||||
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||
{"f1": 3007, "f2": 3008, "f3": 3009},
|
||||
),
|
||||
">III",
|
||||
["f1", "f2", "f3"],
|
||||
)
|
||||
|
||||
# Static characteristic with a serializable value.
|
||||
c5 = gatt.SerializableCharacteristicAdapter(
|
||||
gatt.Characteristic(
|
||||
CHARACTERISTIC_UUID_BASE + "05",
|
||||
gatt.Characteristic.Properties.READ
|
||||
| gatt.Characteristic.Properties.WRITE,
|
||||
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||
CustomSerializableClass(11, 12),
|
||||
),
|
||||
CustomSerializableClass,
|
||||
)
|
||||
|
||||
# Static characteristic with a delegated value.
|
||||
c6 = gatt.DelegatedCharacteristicAdapter(
|
||||
gatt.Characteristic(
|
||||
CHARACTERISTIC_UUID_BASE + "06",
|
||||
gatt.Characteristic.Properties.READ
|
||||
| gatt.Characteristic.Properties.WRITE,
|
||||
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||
CustomClass(1, 2),
|
||||
),
|
||||
encode=CustomClass.encode,
|
||||
decode=CustomClass.decode,
|
||||
)
|
||||
|
||||
# Dynamic characteristic with a bytes value.
|
||||
c7 = gatt.Characteristic(
|
||||
CHARACTERISTIC_UUID_BASE + "07",
|
||||
gatt.Characteristic.Properties.READ | gatt.Characteristic.Properties.WRITE,
|
||||
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||
gatt.CharacteristicValue(
|
||||
read=lambda connection: dynamic_read("bytes"),
|
||||
write=lambda connection, value: dynamic_write("bytes", value),
|
||||
),
|
||||
)
|
||||
|
||||
# Dynamic characteristic with a string value.
|
||||
c8 = gatt.UTF8CharacteristicAdapter(
|
||||
gatt.Characteristic(
|
||||
CHARACTERISTIC_UUID_BASE + "08",
|
||||
gatt.Characteristic.Properties.READ
|
||||
| gatt.Characteristic.Properties.WRITE,
|
||||
gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
|
||||
gatt.CharacteristicValue(
|
||||
read=lambda connection: dynamic_read("string"),
|
||||
write=lambda connection, value: dynamic_write("string", value),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
characteristics: List[
|
||||
Union[gatt.Characteristic, gatt.CharacteristicAdapter]
|
||||
] = [c1, c2, c3, c4, c5, c6, c7, c8]
|
||||
|
||||
# Listen for read and write events.
|
||||
for characteristic in characteristics:
|
||||
characteristic.on(
|
||||
"read",
|
||||
lambda _, value, c=characteristic: on_characteristic_read(c, value),
|
||||
)
|
||||
characteristic.on(
|
||||
"write",
|
||||
lambda _, value, c=characteristic: on_characteristic_write(c, value),
|
||||
)
|
||||
|
||||
device.add_service(gatt.Service(SERVICE_UUID, characteristics)) # type: ignore
|
||||
|
||||
# Get things going
|
||||
await device.power_on()
|
||||
|
||||
# Connect to a peer
|
||||
if len(sys.argv) > 2:
|
||||
await client(device, hci.Address(sys.argv[2]))
|
||||
if is_client:
|
||||
# Connect a client to a peer
|
||||
await client(device, hci.Address(DEFAULT_SERVER_ADDRESS))
|
||||
else:
|
||||
# Advertise so a peer can connect
|
||||
await device.start_advertising(auto_restart=True)
|
||||
|
||||
# Setup a server
|
||||
await server(device)
|
||||
|
||||
await hci_transport.source.wait_for_termination()
|
||||
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -26,6 +26,7 @@ class Advertiser(private val bluetoothAdapter: BluetoothAdapter) : AdvertiseCall
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun stop() {
|
||||
Log.info("stopping advertiser")
|
||||
bluetoothAdapter.bluetoothLeAdvertiser.stopAdvertising(this)
|
||||
}
|
||||
|
||||
|
||||
@@ -381,7 +381,7 @@ fun MainView(
|
||||
label = {
|
||||
Text(text = "Packet Interval (ms)")
|
||||
},
|
||||
value = appViewModel.senderPacketInterval.toString(),
|
||||
value = (if (appViewModel.senderPacketInterval != 0) appViewModel.senderPacketInterval else "").toString(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
@@ -389,7 +389,9 @@ fun MainView(
|
||||
keyboardType = KeyboardType.Number, imeAction = ImeAction.Done
|
||||
),
|
||||
onValueChange = {
|
||||
if (it.isNotEmpty()) {
|
||||
if (it.isEmpty()) {
|
||||
appViewModel.updateSenderPacketInterval(0)
|
||||
} else {
|
||||
val interval = it.toIntOrNull()
|
||||
if (interval != null) {
|
||||
appViewModel.updateSenderPacketInterval(interval)
|
||||
|
||||
@@ -27,8 +27,8 @@ val DEFAULT_RFCOMM_UUID: UUID = UUID.fromString("E6D55659-C8B4-4B85-96BB-B1143AF
|
||||
const val DEFAULT_PEER_BLUETOOTH_ADDRESS = "AA:BB:CC:DD:EE:FF"
|
||||
const val DEFAULT_STARTUP_DELAY = 3000
|
||||
const val DEFAULT_SENDER_PACKET_COUNT = 100
|
||||
const val DEFAULT_SENDER_PACKET_SIZE = 1024
|
||||
const val DEFAULT_SENDER_PACKET_INTERVAL = 100
|
||||
const val DEFAULT_SENDER_PACKET_SIZE = 970 // 970 is a value that works well on Android.
|
||||
const val DEFAULT_SENDER_PACKET_INTERVAL = 0
|
||||
const val DEFAULT_PSM = 128
|
||||
|
||||
const val L2CAP_CLIENT_MODE = "L2CAP Client"
|
||||
@@ -192,7 +192,6 @@ class AppViewModel : ViewModel() {
|
||||
} else if (senderPacketSizeSlider < 0.5F) {
|
||||
512
|
||||
} else if (senderPacketSizeSlider < 0.7F) {
|
||||
// 970 is a value that works well on Android.
|
||||
970
|
||||
} else if (senderPacketSizeSlider < 0.9F) {
|
||||
2048
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user