forked from auracaster/bumble_mirror
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 97757c0c3d | |||
| ab60b42b85 | |||
| febed8179b | |||
| 1bd83273e8 | |||
| 5e9fc89f80 | |||
| 2686663eb2 | |||
| 55801bc2ca | |||
| 6cecc16519 | |||
| a57cf13e2e | |||
| 58f153afc4 | |||
| 7569da37e4 | |||
| a8019a70da | |||
| 685f1dc43e | |||
| 220b3b0236 | |||
| 1f7a1401eb | |||
| ce2b02b62a | |||
| ebeb0dc9f1 | |||
| 776bdae519 |
+3
-5
@@ -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
|
||||
@@ -121,9 +119,9 @@ def broadcast_code_bytes(broadcast_code: str) -> bytes:
|
||||
# -----------------------------------------------------------------------------
|
||||
# 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
|
||||
@@ -376,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:
|
||||
|
||||
+13
-9
@@ -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:
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ from prompt_toolkit.layout import (
|
||||
from bumble import __version__
|
||||
import bumble.core
|
||||
from bumble import colors
|
||||
from bumble.core import UUID, AdvertisingData, BT_LE_TRANSPORT
|
||||
from bumble.core import UUID, AdvertisingData, PhysicalTransport
|
||||
from bumble.device import (
|
||||
ConnectionParametersPreferences,
|
||||
ConnectionPHY,
|
||||
|
||||
@@ -37,6 +37,7 @@ import click
|
||||
import aiohttp.web
|
||||
|
||||
import bumble
|
||||
from bumble import utils
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device, DeviceConfiguration, AdvertisingParameters, CisLink
|
||||
@@ -359,7 +360,9 @@ class Speaker:
|
||||
pcm = decoder.decode(
|
||||
pdu.iso_sdu_fragment, bit_depth=DEFAULT_PCM_BYTES_PER_SAMPLE * 8
|
||||
)
|
||||
self.device.abort_on('disconnection', self.ui_server.send_audio(pcm))
|
||||
utils.cancel_on_event(
|
||||
self.device, 'disconnection', self.ui_server.send_audio(pcm)
|
||||
)
|
||||
|
||||
def on_ase_state_change(ase: ascs.AseStateMachine) -> None:
|
||||
codec_config = ase.codec_specific_configuration
|
||||
@@ -373,7 +376,8 @@ class Speaker:
|
||||
or codec_config.codec_frames_per_sdu is None
|
||||
):
|
||||
return
|
||||
ase.cis_link.abort_on(
|
||||
utils.cancel_on_event(
|
||||
ase.cis_link,
|
||||
'disconnection',
|
||||
lc3_source_task(
|
||||
filename=self.lc3_input_file_path,
|
||||
|
||||
+4
-3
@@ -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
|
||||
|
||||
+7
-7
@@ -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 [
|
||||
|
||||
+12
-5
@@ -41,7 +41,6 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from pyee import EventEmitter
|
||||
|
||||
from bumble import utils
|
||||
from bumble.core import UUID, name_or_number, InvalidOperationError, ProtocolError
|
||||
@@ -223,7 +222,12 @@ UUID_2_FIELD_SPEC = lambda x, y: UUID.parse_uuid_2(x, y) # noqa: E731
|
||||
# Exceptions
|
||||
# -----------------------------------------------------------------------------
|
||||
class ATT_Error(ProtocolError):
|
||||
def __init__(self, error_code, att_handle=0x0000, message=''):
|
||||
error_code: int
|
||||
att_handle: int
|
||||
|
||||
def __init__(
|
||||
self, error_code: int, att_handle: int = 0x0000, message: str = ''
|
||||
) -> None:
|
||||
super().__init__(
|
||||
error_code,
|
||||
error_namespace='att',
|
||||
@@ -233,7 +237,10 @@ class ATT_Error(ProtocolError):
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return f'ATT_Error(error={self.error_name}, handle={self.att_handle:04X}): {self.message}'
|
||||
return (
|
||||
f'ATT_Error(error={self.error_name}, '
|
||||
f'handle={self.att_handle:04X}): {self.message}'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -790,7 +797,7 @@ class AttributeValue(Generic[_T]):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Attribute(EventEmitter, Generic[_T]):
|
||||
class Attribute(utils.EventEmitter, Generic[_T]):
|
||||
class Permissions(enum.IntFlag):
|
||||
READABLE = 0x01
|
||||
WRITEABLE = 0x02
|
||||
@@ -837,7 +844,7 @@ class Attribute(EventEmitter, Generic[_T]):
|
||||
permissions: Union[str, Attribute.Permissions],
|
||||
value: Union[AttributeValue[_T], _T, None] = None,
|
||||
) -> None:
|
||||
EventEmitter.__init__(self)
|
||||
utils.EventEmitter.__init__(self)
|
||||
self.handle = 0
|
||||
self.end_group_handle = 0
|
||||
if isinstance(permissions, str):
|
||||
|
||||
+5
-5
@@ -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
|
||||
|
||||
+9
-10
@@ -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
|
||||
|
||||
+18
-19
@@ -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)
|
||||
|
||||
+2
-2
@@ -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,8 +25,7 @@ import random
|
||||
import struct
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
BT_LE_TRANSPORT,
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
PhysicalTransport,
|
||||
)
|
||||
|
||||
from bumble.hci import (
|
||||
@@ -392,7 +391,7 @@ class Controller:
|
||||
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
|
||||
@@ -452,7 +451,7 @@ class Controller:
|
||||
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
|
||||
@@ -530,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)
|
||||
@@ -695,7 +694,7 @@ class Controller:
|
||||
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
|
||||
@@ -763,7 +762,7 @@ class Controller:
|
||||
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
|
||||
|
||||
+56
-56
@@ -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
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -730,7 +730,7 @@ class DeviceClass:
|
||||
# Appearance
|
||||
# -----------------------------------------------------------------------------
|
||||
class Appearance:
|
||||
class Category(OpenIntEnum):
|
||||
class Category(utils.OpenIntEnum):
|
||||
UNKNOWN = 0x0000
|
||||
PHONE = 0x0001
|
||||
COMPUTER = 0x0002
|
||||
@@ -784,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
|
||||
@@ -808,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
|
||||
@@ -863,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
|
||||
@@ -880,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
|
||||
@@ -895,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
|
||||
@@ -929,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
|
||||
@@ -957,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
|
||||
@@ -966,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
|
||||
@@ -980,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
|
||||
@@ -996,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
|
||||
@@ -1008,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
|
||||
@@ -1016,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
|
||||
@@ -1028,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
|
||||
@@ -1039,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
|
||||
@@ -1048,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
|
||||
@@ -1056,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
|
||||
@@ -1068,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
|
||||
@@ -1086,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
|
||||
@@ -1104,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
|
||||
@@ -1131,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 = {
|
||||
@@ -1296,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
|
||||
|
||||
+171
-136
@@ -49,16 +49,14 @@ from typing import (
|
||||
)
|
||||
from typing_extensions import Self
|
||||
|
||||
from pyee import EventEmitter
|
||||
|
||||
from .colors import color
|
||||
from .att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU
|
||||
from .gatt import Attribute, Characteristic, Descriptor, Service
|
||||
from .host import DataPacketQueue, Host
|
||||
from .profiles.gap import GenericAccessService
|
||||
from .core import (
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
BT_LE_TRANSPORT,
|
||||
from bumble.colors import color
|
||||
from bumble.att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU
|
||||
from bumble.gatt import Attribute, Characteristic, Descriptor, Service
|
||||
from bumble.host import DataPacketQueue, Host
|
||||
from bumble.profiles.gap import GenericAccessService
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
AdvertisingData,
|
||||
BaseBumbleError,
|
||||
ConnectionParameterUpdateError,
|
||||
@@ -72,16 +70,8 @@ from .core import (
|
||||
OutOfResourcesError,
|
||||
UnreachableError,
|
||||
)
|
||||
from .utils import (
|
||||
AsyncRunner,
|
||||
CompositeEventEmitter,
|
||||
EventWatcher,
|
||||
setup_event_forwarding,
|
||||
composite_listener,
|
||||
deprecated,
|
||||
experimental,
|
||||
)
|
||||
from .keys import (
|
||||
from bumble import utils
|
||||
from bumble.keys import (
|
||||
KeyStore,
|
||||
PairingKeys,
|
||||
)
|
||||
@@ -96,7 +86,7 @@ from bumble import core
|
||||
from bumble.profiles import gatt_service
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .transport.common import TransportSource, TransportSink
|
||||
from bumble.transport.common import TransportSource, TransportSink
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -577,7 +567,7 @@ class PeriodicAdvertisingParameters:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclass
|
||||
class AdvertisingSet(EventEmitter):
|
||||
class AdvertisingSet(utils.EventEmitter):
|
||||
device: Device
|
||||
advertising_handle: int
|
||||
auto_restart: bool
|
||||
@@ -794,13 +784,24 @@ class AdvertisingSet(EventEmitter):
|
||||
)
|
||||
del self.device.extended_advertising_sets[self.advertising_handle]
|
||||
|
||||
async def transfer_periodic_info(
|
||||
self, connection: Connection, service_data: int = 0
|
||||
) -> None:
|
||||
if not self.periodic_enabled:
|
||||
raise core.InvalidStateError(
|
||||
f"Periodic Advertising is not enabled on Advertising Set 0x{self.advertising_handle:02X}"
|
||||
)
|
||||
await connection.transfer_periodic_set_info(
|
||||
self.advertising_handle, service_data
|
||||
)
|
||||
|
||||
def on_termination(self, status: int) -> None:
|
||||
self.enabled = False
|
||||
self.emit('termination', status)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PeriodicAdvertisingSync(EventEmitter):
|
||||
class PeriodicAdvertisingSync(utils.EventEmitter):
|
||||
class State(Enum):
|
||||
INIT = 0
|
||||
PENDING = 1
|
||||
@@ -929,7 +930,7 @@ class PeriodicAdvertisingSync(EventEmitter):
|
||||
"received established event for cancelled sync, will terminate"
|
||||
)
|
||||
self.state = self.State.ESTABLISHED
|
||||
AsyncRunner.spawn(self.terminate())
|
||||
utils.AsyncRunner.spawn(self.terminate())
|
||||
return
|
||||
|
||||
if status == hci.HCI_SUCCESS:
|
||||
@@ -1015,7 +1016,7 @@ class BigParameters:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclass
|
||||
class Big(EventEmitter):
|
||||
class Big(utils.EventEmitter):
|
||||
class State(IntEnum):
|
||||
PENDING = 0
|
||||
ACTIVE = 1
|
||||
@@ -1055,7 +1056,7 @@ class Big(EventEmitter):
|
||||
logger.error('BIG %d is not active.', self.big_handle)
|
||||
return
|
||||
|
||||
with closing(EventWatcher()) as watcher:
|
||||
with closing(utils.EventWatcher()) as watcher:
|
||||
terminated = asyncio.Event()
|
||||
watcher.once(self, Big.Event.TERMINATION, lambda _: terminated.set())
|
||||
await self.device.send_command(
|
||||
@@ -1078,7 +1079,7 @@ class BigSyncParameters:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclass
|
||||
class BigSync(EventEmitter):
|
||||
class BigSync(utils.EventEmitter):
|
||||
class State(IntEnum):
|
||||
PENDING = 0
|
||||
ACTIVE = 1
|
||||
@@ -1113,7 +1114,7 @@ class BigSync(EventEmitter):
|
||||
logger.error('BIG Sync %d is not active.', self.big_handle)
|
||||
return
|
||||
|
||||
with closing(EventWatcher()) as watcher:
|
||||
with closing(utils.EventWatcher()) as watcher:
|
||||
terminated = asyncio.Event()
|
||||
watcher.once(self, BigSync.Event.TERMINATION, lambda _: terminated.set())
|
||||
await self.device.send_command(
|
||||
@@ -1243,7 +1244,7 @@ class Peer:
|
||||
self,
|
||||
uuids: Iterable[Union[core.UUID, str]] = (),
|
||||
service: Optional[gatt_client.ServiceProxy] = None,
|
||||
) -> list[gatt_client.CharacteristicProxy]:
|
||||
) -> list[gatt_client.CharacteristicProxy[bytes]]:
|
||||
return await self.gatt_client.discover_characteristics(
|
||||
uuids=uuids, service=service
|
||||
)
|
||||
@@ -1258,7 +1259,7 @@ class Peer:
|
||||
characteristic, start_handle, end_handle
|
||||
)
|
||||
|
||||
async def discover_attributes(self) -> list[gatt_client.AttributeProxy]:
|
||||
async def discover_attributes(self) -> list[gatt_client.AttributeProxy[bytes]]:
|
||||
return await self.gatt_client.discover_attributes()
|
||||
|
||||
async def discover_all(self):
|
||||
@@ -1312,7 +1313,7 @@ class Peer:
|
||||
self,
|
||||
uuid: core.UUID,
|
||||
service: Optional[Union[gatt_client.ServiceProxy, core.UUID]] = None,
|
||||
) -> list[gatt_client.CharacteristicProxy]:
|
||||
) -> list[gatt_client.CharacteristicProxy[bytes]]:
|
||||
if isinstance(service, core.UUID):
|
||||
return list(
|
||||
itertools.chain(
|
||||
@@ -1382,7 +1383,7 @@ ConnectionParametersPreferences.default = ConnectionParametersPreferences()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclass
|
||||
class ScoLink(CompositeEventEmitter):
|
||||
class ScoLink(utils.CompositeEventEmitter):
|
||||
device: Device
|
||||
acl_connection: Connection
|
||||
handle: int
|
||||
@@ -1473,7 +1474,7 @@ class _IsoLink:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclass
|
||||
class CisLink(CompositeEventEmitter, _IsoLink):
|
||||
class CisLink(utils.EventEmitter, _IsoLink):
|
||||
class State(IntEnum):
|
||||
PENDING = 0
|
||||
ESTABLISHED = 1
|
||||
@@ -1550,7 +1551,7 @@ class IsoPacketStream:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Connection(CompositeEventEmitter):
|
||||
class Connection(utils.CompositeEventEmitter):
|
||||
device: Device
|
||||
handle: int
|
||||
transport: core.PhysicalTransport
|
||||
@@ -1570,7 +1571,7 @@ class Connection(CompositeEventEmitter):
|
||||
cs_configs: dict[int, ChannelSoundingConfig] # Config ID to Configuration
|
||||
cs_procedures: dict[int, ChannelSoundingProcedure] # Config ID to Procedures
|
||||
|
||||
@composite_listener
|
||||
@utils.composite_listener
|
||||
class Listener:
|
||||
def on_disconnection(self, reason):
|
||||
pass
|
||||
@@ -1649,7 +1650,7 @@ class Connection(CompositeEventEmitter):
|
||||
return cls(
|
||||
device,
|
||||
None,
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
PhysicalTransport.BR_EDR,
|
||||
device.public_address,
|
||||
None,
|
||||
peer_address,
|
||||
@@ -1664,7 +1665,7 @@ class Connection(CompositeEventEmitter):
|
||||
Finish an incomplete connection upon completion.
|
||||
"""
|
||||
assert self.handle is None
|
||||
assert self.transport == BT_BR_EDR_TRANSPORT
|
||||
assert self.transport == PhysicalTransport.BR_EDR
|
||||
self.handle = handle
|
||||
self.parameters = parameters
|
||||
|
||||
@@ -1689,7 +1690,7 @@ class Connection(CompositeEventEmitter):
|
||||
def send_l2cap_pdu(self, cid: int, pdu: bytes) -> None:
|
||||
self.device.send_l2cap_pdu(self.handle, cid, pdu)
|
||||
|
||||
@deprecated("Please use create_l2cap_channel()")
|
||||
@utils.deprecated("Please use create_l2cap_channel()")
|
||||
async def open_l2cap_channel(
|
||||
self,
|
||||
psm,
|
||||
@@ -1743,7 +1744,9 @@ class Connection(CompositeEventEmitter):
|
||||
self.on('disconnection_failure', abort.set_exception)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(self.device.abort_on('flush', abort), timeout)
|
||||
await asyncio.wait_for(
|
||||
utils.cancel_on_event(self.device, 'flush', abort), timeout
|
||||
)
|
||||
finally:
|
||||
self.remove_listener('disconnection', abort.set_result)
|
||||
self.remove_listener('disconnection_failure', abort.set_exception)
|
||||
@@ -1782,6 +1785,13 @@ class Connection(CompositeEventEmitter):
|
||||
) -> None:
|
||||
await self.device.transfer_periodic_sync(self, sync_handle, service_data)
|
||||
|
||||
async def transfer_periodic_set_info(
|
||||
self, advertising_handle: int, service_data: int = 0
|
||||
) -> None:
|
||||
await self.device.transfer_periodic_set_info(
|
||||
self, advertising_handle, service_data
|
||||
)
|
||||
|
||||
# [Classic only]
|
||||
async def request_remote_name(self):
|
||||
return await self.device.request_remote_name(self)
|
||||
@@ -1812,7 +1822,7 @@ class Connection(CompositeEventEmitter):
|
||||
raise
|
||||
|
||||
def __str__(self):
|
||||
if self.transport == BT_LE_TRANSPORT:
|
||||
if self.transport == PhysicalTransport.LE:
|
||||
return (
|
||||
f'Connection(transport=LE, handle=0x{self.handle:04X}, '
|
||||
f'role={self.role_name}, '
|
||||
@@ -2020,7 +2030,7 @@ device_host_event_handlers: list[str] = []
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Device(CompositeEventEmitter):
|
||||
class Device(utils.CompositeEventEmitter):
|
||||
# Incomplete list of fields.
|
||||
random_address: hci.Address # Random private address that may change periodically
|
||||
public_address: (
|
||||
@@ -2052,7 +2062,7 @@ class Device(CompositeEventEmitter):
|
||||
_pending_cis: Dict[int, tuple[int, int]]
|
||||
gatt_service: gatt_service.GenericAttributeProfileService | None = None
|
||||
|
||||
@composite_listener
|
||||
@utils.composite_listener
|
||||
class Listener:
|
||||
def on_advertisement(self, advertisement):
|
||||
pass
|
||||
@@ -2273,7 +2283,9 @@ class Device(CompositeEventEmitter):
|
||||
self.l2cap_channel_manager.register_fixed_channel(ATT_CID, self.on_gatt_pdu)
|
||||
|
||||
# Forward some events
|
||||
setup_event_forwarding(self.gatt_server, self, 'characteristic_subscription')
|
||||
utils.setup_event_forwarding(
|
||||
self.gatt_server, self, 'characteristic_subscription'
|
||||
)
|
||||
|
||||
# Set the initial host
|
||||
if host:
|
||||
@@ -2362,11 +2374,11 @@ class Device(CompositeEventEmitter):
|
||||
None,
|
||||
)
|
||||
|
||||
@deprecated("Please use create_l2cap_server()")
|
||||
@utils.deprecated("Please use create_l2cap_server()")
|
||||
def register_l2cap_server(self, psm, server) -> int:
|
||||
return self.l2cap_channel_manager.register_server(psm, server)
|
||||
|
||||
@deprecated("Please use create_l2cap_server()")
|
||||
@utils.deprecated("Please use create_l2cap_server()")
|
||||
def register_l2cap_channel_server(
|
||||
self,
|
||||
psm,
|
||||
@@ -2379,7 +2391,7 @@ class Device(CompositeEventEmitter):
|
||||
psm, server, max_credits, mtu, mps
|
||||
)
|
||||
|
||||
@deprecated("Please use create_l2cap_channel()")
|
||||
@utils.deprecated("Please use create_l2cap_channel()")
|
||||
async def open_l2cap_channel(
|
||||
self,
|
||||
connection,
|
||||
@@ -3220,7 +3232,7 @@ class Device(CompositeEventEmitter):
|
||||
advertiser_clock_accuracy,
|
||||
)
|
||||
|
||||
AsyncRunner.spawn(self._update_periodic_advertising_syncs())
|
||||
utils.AsyncRunner.spawn(self._update_periodic_advertising_syncs())
|
||||
|
||||
return
|
||||
|
||||
@@ -3379,7 +3391,7 @@ class Device(CompositeEventEmitter):
|
||||
async def connect(
|
||||
self,
|
||||
peer_address: Union[hci.Address, str],
|
||||
transport: core.PhysicalTransport = BT_LE_TRANSPORT,
|
||||
transport: core.PhysicalTransport = PhysicalTransport.LE,
|
||||
connection_parameters_preferences: Optional[
|
||||
dict[hci.Phy, ConnectionParametersPreferences]
|
||||
] = None,
|
||||
@@ -3429,23 +3441,23 @@ class Device(CompositeEventEmitter):
|
||||
'''
|
||||
|
||||
# Check parameters
|
||||
if transport not in (BT_LE_TRANSPORT, BT_BR_EDR_TRANSPORT):
|
||||
if transport not in (PhysicalTransport.LE, PhysicalTransport.BR_EDR):
|
||||
raise InvalidArgumentError('invalid transport')
|
||||
transport = core.PhysicalTransport(transport)
|
||||
|
||||
# Adjust the transport automatically if we need to
|
||||
if transport == BT_LE_TRANSPORT and not self.le_enabled:
|
||||
transport = BT_BR_EDR_TRANSPORT
|
||||
elif transport == BT_BR_EDR_TRANSPORT and not self.classic_enabled:
|
||||
transport = BT_LE_TRANSPORT
|
||||
if transport == PhysicalTransport.LE and not self.le_enabled:
|
||||
transport = PhysicalTransport.BR_EDR
|
||||
elif transport == PhysicalTransport.BR_EDR and not self.classic_enabled:
|
||||
transport = PhysicalTransport.LE
|
||||
|
||||
# Check that there isn't already a pending connection
|
||||
if transport == BT_LE_TRANSPORT and self.is_le_connecting:
|
||||
if transport == PhysicalTransport.LE and self.is_le_connecting:
|
||||
raise InvalidStateError('connection already pending')
|
||||
|
||||
if isinstance(peer_address, str):
|
||||
try:
|
||||
if transport == BT_LE_TRANSPORT and peer_address.endswith('@'):
|
||||
if transport == PhysicalTransport.LE and peer_address.endswith('@'):
|
||||
peer_address = hci.Address.from_string_for_transport(
|
||||
peer_address[:-1], transport
|
||||
)
|
||||
@@ -3465,21 +3477,21 @@ class Device(CompositeEventEmitter):
|
||||
else:
|
||||
# All BR/EDR addresses should be public addresses
|
||||
if (
|
||||
transport == BT_BR_EDR_TRANSPORT
|
||||
transport == PhysicalTransport.BR_EDR
|
||||
and peer_address.address_type != hci.Address.PUBLIC_DEVICE_ADDRESS
|
||||
):
|
||||
raise InvalidArgumentError('BR/EDR addresses must be PUBLIC')
|
||||
|
||||
assert isinstance(peer_address, hci.Address)
|
||||
|
||||
if transport == BT_LE_TRANSPORT and always_resolve:
|
||||
if transport == PhysicalTransport.LE and always_resolve:
|
||||
logger.debug('resolving address')
|
||||
peer_address = await self.find_peer_by_identity_address(
|
||||
peer_address
|
||||
) # TODO: timeout
|
||||
|
||||
def on_connection(connection):
|
||||
if transport == BT_LE_TRANSPORT or (
|
||||
if transport == PhysicalTransport.LE or (
|
||||
# match BR/EDR connection event against peer address
|
||||
connection.transport == transport
|
||||
and connection.peer_address == peer_address
|
||||
@@ -3487,7 +3499,7 @@ class Device(CompositeEventEmitter):
|
||||
pending_connection.set_result(connection)
|
||||
|
||||
def on_connection_failure(error):
|
||||
if transport == BT_LE_TRANSPORT or (
|
||||
if transport == PhysicalTransport.LE or (
|
||||
# match BR/EDR connection failure event against peer address
|
||||
error.transport == transport
|
||||
and error.peer_address == peer_address
|
||||
@@ -3501,7 +3513,7 @@ class Device(CompositeEventEmitter):
|
||||
|
||||
try:
|
||||
# Tell the controller to connect
|
||||
if transport == BT_LE_TRANSPORT:
|
||||
if transport == PhysicalTransport.LE:
|
||||
if connection_parameters_preferences is None:
|
||||
if connection_parameters_preferences is None:
|
||||
connection_parameters_preferences = {
|
||||
@@ -3646,18 +3658,18 @@ class Device(CompositeEventEmitter):
|
||||
raise hci.HCI_StatusError(result)
|
||||
|
||||
# Wait for the connection process to complete
|
||||
if transport == BT_LE_TRANSPORT:
|
||||
if transport == PhysicalTransport.LE:
|
||||
self.le_connecting = True
|
||||
|
||||
if timeout is None:
|
||||
return await self.abort_on('flush', pending_connection)
|
||||
return await utils.cancel_on_event(self, 'flush', pending_connection)
|
||||
|
||||
try:
|
||||
return await asyncio.wait_for(
|
||||
asyncio.shield(pending_connection), timeout
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
if transport == BT_LE_TRANSPORT:
|
||||
if transport == PhysicalTransport.LE:
|
||||
await self.send_command(
|
||||
hci.HCI_LE_Create_Connection_Cancel_Command()
|
||||
)
|
||||
@@ -3667,13 +3679,15 @@ class Device(CompositeEventEmitter):
|
||||
)
|
||||
|
||||
try:
|
||||
return await self.abort_on('flush', pending_connection)
|
||||
return await utils.cancel_on_event(
|
||||
self, 'flush', pending_connection
|
||||
)
|
||||
except core.ConnectionError as error:
|
||||
raise core.TimeoutError() from error
|
||||
finally:
|
||||
self.remove_listener('connection', on_connection)
|
||||
self.remove_listener('connection_failure', on_connection_failure)
|
||||
if transport == BT_LE_TRANSPORT:
|
||||
if transport == PhysicalTransport.LE:
|
||||
self.le_connecting = False
|
||||
self.connect_own_address_type = None
|
||||
else:
|
||||
@@ -3703,7 +3717,7 @@ class Device(CompositeEventEmitter):
|
||||
# If the address is not parsable, assume it is a name instead
|
||||
logger.debug('looking for peer by name')
|
||||
peer_address = await self.find_peer_by_name(
|
||||
peer_address, BT_BR_EDR_TRANSPORT
|
||||
peer_address, PhysicalTransport.BR_EDR
|
||||
) # TODO: timeout
|
||||
|
||||
assert isinstance(peer_address, hci.Address)
|
||||
@@ -3723,7 +3737,7 @@ class Device(CompositeEventEmitter):
|
||||
|
||||
try:
|
||||
# Wait for a request or a completed connection
|
||||
pending_request = self.abort_on('flush', pending_request_fut)
|
||||
pending_request = utils.cancel_on_event(self, 'flush', pending_request_fut)
|
||||
result = await (
|
||||
asyncio.wait_for(pending_request, timeout)
|
||||
if timeout
|
||||
@@ -3753,14 +3767,14 @@ class Device(CompositeEventEmitter):
|
||||
|
||||
def on_connection(connection):
|
||||
if (
|
||||
connection.transport == BT_BR_EDR_TRANSPORT
|
||||
connection.transport == PhysicalTransport.BR_EDR
|
||||
and connection.peer_address == peer_address
|
||||
):
|
||||
pending_connection.set_result(connection)
|
||||
|
||||
def on_connection_failure(error):
|
||||
if (
|
||||
error.transport == BT_BR_EDR_TRANSPORT
|
||||
error.transport == PhysicalTransport.BR_EDR
|
||||
and error.peer_address == peer_address
|
||||
):
|
||||
pending_connection.set_exception(error)
|
||||
@@ -3785,7 +3799,7 @@ class Device(CompositeEventEmitter):
|
||||
)
|
||||
|
||||
# Wait for connection complete
|
||||
return await self.abort_on('flush', pending_connection)
|
||||
return await utils.cancel_on_event(self, 'flush', pending_connection)
|
||||
|
||||
finally:
|
||||
self.remove_listener('connection', on_connection)
|
||||
@@ -3830,7 +3844,7 @@ class Device(CompositeEventEmitter):
|
||||
# If the address is not parsable, assume it is a name instead
|
||||
logger.debug('looking for peer by name')
|
||||
peer_address = await self.find_peer_by_name(
|
||||
peer_address, BT_BR_EDR_TRANSPORT
|
||||
peer_address, PhysicalTransport.BR_EDR
|
||||
) # TODO: timeout
|
||||
|
||||
await self.send_command(
|
||||
@@ -3859,7 +3873,7 @@ class Device(CompositeEventEmitter):
|
||||
|
||||
# Wait for the disconnection process to complete
|
||||
self.disconnecting = True
|
||||
return await self.abort_on('flush', pending_disconnection)
|
||||
return await utils.cancel_on_event(self, 'flush', pending_disconnection)
|
||||
finally:
|
||||
connection.remove_listener(
|
||||
'disconnection', pending_disconnection.set_result
|
||||
@@ -3939,6 +3953,9 @@ class Device(CompositeEventEmitter):
|
||||
return result.return_parameters.rssi
|
||||
|
||||
async def get_connection_phy(self, connection: Connection) -> ConnectionPHY:
|
||||
if not self.host.supports_command(hci.HCI_LE_READ_PHY_COMMAND):
|
||||
return ConnectionPHY(hci.Phy.LE_1M, hci.Phy.LE_1M)
|
||||
|
||||
result = await self.send_command(
|
||||
hci.HCI_LE_Read_PHY_Command(connection_handle=connection.handle),
|
||||
check_result=True,
|
||||
@@ -4001,7 +4018,19 @@ class Device(CompositeEventEmitter):
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
async def find_peer_by_name(self, name, transport=BT_LE_TRANSPORT):
|
||||
async def transfer_periodic_set_info(
|
||||
self, connection: Connection, advertising_handle: int, service_data: int = 0
|
||||
) -> None:
|
||||
return await self.send_command(
|
||||
hci.HCI_LE_Periodic_Advertising_Set_Info_Transfer_Command(
|
||||
connection_handle=connection.handle,
|
||||
service_data=service_data,
|
||||
advertising_handle=advertising_handle,
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
async def find_peer_by_name(self, name, transport=PhysicalTransport.LE):
|
||||
"""
|
||||
Scan for a peer with a given name and return its address.
|
||||
"""
|
||||
@@ -4020,7 +4049,7 @@ class Device(CompositeEventEmitter):
|
||||
was_scanning = self.scanning
|
||||
was_discovering = self.discovering
|
||||
try:
|
||||
if transport == BT_LE_TRANSPORT:
|
||||
if transport == PhysicalTransport.LE:
|
||||
event_name = 'advertisement'
|
||||
listener = self.on(
|
||||
event_name,
|
||||
@@ -4032,7 +4061,7 @@ class Device(CompositeEventEmitter):
|
||||
if not self.scanning:
|
||||
await self.start_scanning(filter_duplicates=True)
|
||||
|
||||
elif transport == BT_BR_EDR_TRANSPORT:
|
||||
elif transport == PhysicalTransport.BR_EDR:
|
||||
event_name = 'inquiry_result'
|
||||
listener = self.on(
|
||||
event_name,
|
||||
@@ -4046,14 +4075,14 @@ class Device(CompositeEventEmitter):
|
||||
else:
|
||||
return None
|
||||
|
||||
return await self.abort_on('flush', peer_address)
|
||||
return await utils.cancel_on_event(self, 'flush', peer_address)
|
||||
finally:
|
||||
if listener is not None:
|
||||
self.remove_listener(event_name, listener)
|
||||
|
||||
if transport == BT_LE_TRANSPORT and not was_scanning:
|
||||
if transport == PhysicalTransport.LE and not was_scanning:
|
||||
await self.stop_scanning()
|
||||
elif transport == BT_BR_EDR_TRANSPORT and not was_discovering:
|
||||
elif transport == PhysicalTransport.BR_EDR and not was_discovering:
|
||||
await self.stop_discovery()
|
||||
|
||||
async def find_peer_by_identity_address(
|
||||
@@ -4096,7 +4125,7 @@ class Device(CompositeEventEmitter):
|
||||
if not self.scanning:
|
||||
await self.start_scanning(filter_duplicates=True)
|
||||
|
||||
return await self.abort_on('flush', peer_address)
|
||||
return await utils.cancel_on_event(self, 'flush', peer_address)
|
||||
finally:
|
||||
if listener is not None:
|
||||
self.remove_listener(event_name, listener)
|
||||
@@ -4200,7 +4229,9 @@ class Device(CompositeEventEmitter):
|
||||
raise hci.HCI_StatusError(result)
|
||||
|
||||
# Wait for the authentication to complete
|
||||
await connection.abort_on('disconnection', pending_authentication)
|
||||
await utils.cancel_on_event(
|
||||
connection, 'disconnection', pending_authentication
|
||||
)
|
||||
finally:
|
||||
connection.remove_listener('connection_authentication', on_authentication)
|
||||
connection.remove_listener(
|
||||
@@ -4208,7 +4239,7 @@ class Device(CompositeEventEmitter):
|
||||
)
|
||||
|
||||
async def encrypt(self, connection, enable=True):
|
||||
if not enable and connection.transport == BT_LE_TRANSPORT:
|
||||
if not enable and connection.transport == PhysicalTransport.LE:
|
||||
raise InvalidArgumentError('`enable` parameter is classic only.')
|
||||
|
||||
# Set up event handlers
|
||||
@@ -4225,7 +4256,7 @@ class Device(CompositeEventEmitter):
|
||||
|
||||
# Request the encryption
|
||||
try:
|
||||
if connection.transport == BT_LE_TRANSPORT:
|
||||
if connection.transport == PhysicalTransport.LE:
|
||||
# Look for a key in the key store
|
||||
if self.keystore is None:
|
||||
raise InvalidOperationError('no key store')
|
||||
@@ -4246,7 +4277,7 @@ class Device(CompositeEventEmitter):
|
||||
else:
|
||||
raise InvalidOperationError('no LTK found for peer')
|
||||
|
||||
if connection.role != hci.HCI_CENTRAL_ROLE:
|
||||
if connection.role != hci.Role.CENTRAL:
|
||||
raise InvalidStateError('only centrals can start encryption')
|
||||
|
||||
result = await self.send_command(
|
||||
@@ -4280,7 +4311,7 @@ class Device(CompositeEventEmitter):
|
||||
raise hci.HCI_StatusError(result)
|
||||
|
||||
# Wait for the result
|
||||
await connection.abort_on('disconnection', pending_encryption)
|
||||
await utils.cancel_on_event(connection, 'disconnection', pending_encryption)
|
||||
finally:
|
||||
connection.remove_listener(
|
||||
'connection_encryption_change', on_encryption_change
|
||||
@@ -4324,7 +4355,9 @@ class Device(CompositeEventEmitter):
|
||||
f'{hci.HCI_Constant.error_name(result.status)}'
|
||||
)
|
||||
raise hci.HCI_StatusError(result)
|
||||
await connection.abort_on('disconnection', pending_role_change)
|
||||
await utils.cancel_on_event(
|
||||
connection, 'disconnection', pending_role_change
|
||||
)
|
||||
finally:
|
||||
connection.remove_listener('role_change', on_role_change)
|
||||
connection.remove_listener('role_change_failure', on_role_change_failure)
|
||||
@@ -4373,13 +4406,13 @@ class Device(CompositeEventEmitter):
|
||||
raise hci.HCI_StatusError(result)
|
||||
|
||||
# Wait for the result
|
||||
return await self.abort_on('flush', pending_name)
|
||||
return await utils.cancel_on_event(self, 'flush', pending_name)
|
||||
finally:
|
||||
self.remove_listener('remote_name', handler)
|
||||
self.remove_listener('remote_name_failure', failure_handler)
|
||||
|
||||
# [LE only]
|
||||
@experimental('Only for testing.')
|
||||
@utils.experimental('Only for testing.')
|
||||
async def setup_cig(
|
||||
self,
|
||||
cig_id: int,
|
||||
@@ -4437,7 +4470,7 @@ class Device(CompositeEventEmitter):
|
||||
return cis_handles
|
||||
|
||||
# [LE only]
|
||||
@experimental('Only for testing.')
|
||||
@utils.experimental('Only for testing.')
|
||||
async def create_cis(
|
||||
self, cis_acl_pairs: Sequence[tuple[int, int]]
|
||||
) -> list[CisLink]:
|
||||
@@ -4453,7 +4486,7 @@ class Device(CompositeEventEmitter):
|
||||
cig_id=cig_id,
|
||||
)
|
||||
|
||||
with closing(EventWatcher()) as watcher:
|
||||
with closing(utils.EventWatcher()) as watcher:
|
||||
pending_cis_establishments = {
|
||||
cis_handle: asyncio.get_running_loop().create_future()
|
||||
for cis_handle, _ in cis_acl_pairs
|
||||
@@ -4480,7 +4513,7 @@ class Device(CompositeEventEmitter):
|
||||
return await asyncio.gather(*pending_cis_establishments.values())
|
||||
|
||||
# [LE only]
|
||||
@experimental('Only for testing.')
|
||||
@utils.experimental('Only for testing.')
|
||||
async def accept_cis_request(self, handle: int) -> CisLink:
|
||||
"""[LE Only] Accepts an incoming CIS request.
|
||||
|
||||
@@ -4502,7 +4535,7 @@ class Device(CompositeEventEmitter):
|
||||
if cis_link.state == CisLink.State.ESTABLISHED:
|
||||
return cis_link
|
||||
|
||||
with closing(EventWatcher()) as watcher:
|
||||
with closing(utils.EventWatcher()) as watcher:
|
||||
pending_establishment = asyncio.get_running_loop().create_future()
|
||||
|
||||
def on_establishment() -> None:
|
||||
@@ -4526,7 +4559,7 @@ class Device(CompositeEventEmitter):
|
||||
raise UnreachableError()
|
||||
|
||||
# [LE only]
|
||||
@experimental('Only for testing.')
|
||||
@utils.experimental('Only for testing.')
|
||||
async def reject_cis_request(
|
||||
self,
|
||||
handle: int,
|
||||
@@ -4540,14 +4573,14 @@ class Device(CompositeEventEmitter):
|
||||
)
|
||||
|
||||
# [LE only]
|
||||
@experimental('Only for testing.')
|
||||
@utils.experimental('Only for testing.')
|
||||
async def create_big(
|
||||
self, advertising_set: AdvertisingSet, parameters: BigParameters
|
||||
) -> Big:
|
||||
if (big_handle := self.next_big_handle()) is None:
|
||||
raise core.OutOfResourcesError("All valid BIG handles already in use")
|
||||
|
||||
with closing(EventWatcher()) as watcher:
|
||||
with closing(utils.EventWatcher()) as watcher:
|
||||
big = Big(
|
||||
big_handle=big_handle,
|
||||
parameters=parameters,
|
||||
@@ -4590,7 +4623,7 @@ class Device(CompositeEventEmitter):
|
||||
return big
|
||||
|
||||
# [LE only]
|
||||
@experimental('Only for testing.')
|
||||
@utils.experimental('Only for testing.')
|
||||
async def create_big_sync(
|
||||
self, pa_sync: PeriodicAdvertisingSync, parameters: BigSyncParameters
|
||||
) -> BigSync:
|
||||
@@ -4600,7 +4633,7 @@ class Device(CompositeEventEmitter):
|
||||
if (pa_sync_handle := pa_sync.sync_handle) is None:
|
||||
raise core.InvalidStateError("PA Sync is not established")
|
||||
|
||||
with closing(EventWatcher()) as watcher:
|
||||
with closing(utils.EventWatcher()) as watcher:
|
||||
big_sync = BigSync(
|
||||
big_handle=big_handle,
|
||||
parameters=parameters,
|
||||
@@ -4648,7 +4681,7 @@ class Device(CompositeEventEmitter):
|
||||
Returns:
|
||||
LE features supported by the remote device.
|
||||
"""
|
||||
with closing(EventWatcher()) as watcher:
|
||||
with closing(utils.EventWatcher()) as watcher:
|
||||
read_feature_future: asyncio.Future[hci.LeFeatureMask] = (
|
||||
asyncio.get_running_loop().create_future()
|
||||
)
|
||||
@@ -4671,7 +4704,7 @@ class Device(CompositeEventEmitter):
|
||||
)
|
||||
return await read_feature_future
|
||||
|
||||
@experimental('Only for testing.')
|
||||
@utils.experimental('Only for testing.')
|
||||
async def get_remote_cs_capabilities(
|
||||
self, connection: Connection
|
||||
) -> ChannelSoundingCapabilities:
|
||||
@@ -4679,7 +4712,7 @@ class Device(CompositeEventEmitter):
|
||||
asyncio.get_running_loop().create_future()
|
||||
)
|
||||
|
||||
with closing(EventWatcher()) as watcher:
|
||||
with closing(utils.EventWatcher()) as watcher:
|
||||
watcher.once(
|
||||
connection, 'channel_sounding_capabilities', complete_future.set_result
|
||||
)
|
||||
@@ -4696,7 +4729,7 @@ class Device(CompositeEventEmitter):
|
||||
)
|
||||
return await complete_future
|
||||
|
||||
@experimental('Only for testing.')
|
||||
@utils.experimental('Only for testing.')
|
||||
async def set_default_cs_settings(
|
||||
self,
|
||||
connection: Connection,
|
||||
@@ -4716,7 +4749,7 @@ class Device(CompositeEventEmitter):
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
@experimental('Only for testing.')
|
||||
@utils.experimental('Only for testing.')
|
||||
async def create_cs_config(
|
||||
self,
|
||||
connection: Connection,
|
||||
@@ -4753,7 +4786,7 @@ class Device(CompositeEventEmitter):
|
||||
if config_id is None:
|
||||
raise OutOfResourcesError("No available config ID on this connection!")
|
||||
|
||||
with closing(EventWatcher()) as watcher:
|
||||
with closing(utils.EventWatcher()) as watcher:
|
||||
watcher.once(
|
||||
connection, 'channel_sounding_config', complete_future.set_result
|
||||
)
|
||||
@@ -4787,12 +4820,12 @@ class Device(CompositeEventEmitter):
|
||||
)
|
||||
return await complete_future
|
||||
|
||||
@experimental('Only for testing.')
|
||||
@utils.experimental('Only for testing.')
|
||||
async def enable_cs_security(self, connection: Connection) -> None:
|
||||
complete_future: asyncio.Future[None] = (
|
||||
asyncio.get_running_loop().create_future()
|
||||
)
|
||||
with closing(EventWatcher()) as watcher:
|
||||
with closing(utils.EventWatcher()) as watcher:
|
||||
|
||||
def on_event(event: hci.HCI_LE_CS_Security_Enable_Complete_Event) -> None:
|
||||
if event.connection_handle != connection.handle:
|
||||
@@ -4811,7 +4844,7 @@ class Device(CompositeEventEmitter):
|
||||
)
|
||||
return await complete_future
|
||||
|
||||
@experimental('Only for testing.')
|
||||
@utils.experimental('Only for testing.')
|
||||
async def set_cs_procedure_parameters(
|
||||
self,
|
||||
connection: Connection,
|
||||
@@ -4849,7 +4882,7 @@ class Device(CompositeEventEmitter):
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
@experimental('Only for testing.')
|
||||
@utils.experimental('Only for testing.')
|
||||
async def enable_cs_procedure(
|
||||
self,
|
||||
connection: Connection,
|
||||
@@ -4859,7 +4892,7 @@ class Device(CompositeEventEmitter):
|
||||
complete_future: asyncio.Future[ChannelSoundingProcedure] = (
|
||||
asyncio.get_running_loop().create_future()
|
||||
)
|
||||
with closing(EventWatcher()) as watcher:
|
||||
with closing(utils.EventWatcher()) as watcher:
|
||||
watcher.once(
|
||||
connection, 'channel_sounding_procedure', complete_future.set_result
|
||||
)
|
||||
@@ -4899,10 +4932,12 @@ class Device(CompositeEventEmitter):
|
||||
value=link_key, authenticated=authenticated
|
||||
)
|
||||
|
||||
self.abort_on('flush', self.update_keys(str(bd_addr), pairing_keys))
|
||||
utils.cancel_on_event(
|
||||
self, 'flush', self.update_keys(str(bd_addr), pairing_keys)
|
||||
)
|
||||
|
||||
if connection := self.find_connection_by_bd_addr(
|
||||
bd_addr, transport=BT_BR_EDR_TRANSPORT
|
||||
bd_addr, transport=PhysicalTransport.BR_EDR
|
||||
):
|
||||
connection.link_key_type = key_type
|
||||
|
||||
@@ -5168,7 +5203,7 @@ class Device(CompositeEventEmitter):
|
||||
if advertising_set.auto_restart:
|
||||
connection.once(
|
||||
'disconnection',
|
||||
lambda _: self.abort_on('flush', advertising_set.start()),
|
||||
lambda _: utils.cancel_on_event(self, 'flush', advertising_set.start()),
|
||||
)
|
||||
|
||||
self.emit('connection', connection)
|
||||
@@ -5202,7 +5237,7 @@ class Device(CompositeEventEmitter):
|
||||
'new connection reuses the same handle as a previous connection'
|
||||
)
|
||||
|
||||
if transport == BT_BR_EDR_TRANSPORT:
|
||||
if transport == PhysicalTransport.BR_EDR:
|
||||
# Create a new connection
|
||||
connection = self.pending_connections.pop(peer_address)
|
||||
connection.complete(connection_handle, connection_parameters)
|
||||
@@ -5225,7 +5260,7 @@ class Device(CompositeEventEmitter):
|
||||
|
||||
self_address = None
|
||||
own_address_type: Optional[hci.OwnAddressType] = None
|
||||
if role == hci.HCI_CENTRAL_ROLE:
|
||||
if role == hci.Role.CENTRAL:
|
||||
own_address_type = self.connect_own_address_type
|
||||
assert own_address_type is not None
|
||||
else:
|
||||
@@ -5272,22 +5307,22 @@ class Device(CompositeEventEmitter):
|
||||
)
|
||||
self.connections[connection_handle] = connection
|
||||
|
||||
if role == hci.HCI_PERIPHERAL_ROLE and self.legacy_advertiser:
|
||||
if role == hci.Role.PERIPHERAL and self.legacy_advertiser:
|
||||
if self.legacy_advertiser.auto_restart:
|
||||
advertiser = self.legacy_advertiser
|
||||
connection.once(
|
||||
'disconnection',
|
||||
lambda _: self.abort_on('flush', advertiser.start()),
|
||||
lambda _: utils.cancel_on_event(self, 'flush', advertiser.start()),
|
||||
)
|
||||
else:
|
||||
self.legacy_advertiser = None
|
||||
|
||||
if role == hci.HCI_CENTRAL_ROLE or not self.supports_le_extended_advertising:
|
||||
if role == hci.Role.CENTRAL or not self.supports_le_extended_advertising:
|
||||
# We can emit now, we have all the info we need
|
||||
self.emit('connection', connection)
|
||||
return
|
||||
|
||||
if role == hci.HCI_PERIPHERAL_ROLE and self.supports_le_extended_advertising:
|
||||
if role == hci.Role.PERIPHERAL and self.supports_le_extended_advertising:
|
||||
if advertising_set := self.connecting_extended_advertising_sets.pop(
|
||||
connection_handle, None
|
||||
):
|
||||
@@ -5304,7 +5339,7 @@ class Device(CompositeEventEmitter):
|
||||
|
||||
# For directed advertising, this means a timeout
|
||||
if (
|
||||
transport == BT_LE_TRANSPORT
|
||||
transport == PhysicalTransport.LE
|
||||
and self.legacy_advertiser
|
||||
and self.legacy_advertiser.advertising_type.is_directed
|
||||
):
|
||||
@@ -5331,7 +5366,7 @@ class Device(CompositeEventEmitter):
|
||||
hci.HCI_Connection_Complete_Event.ESCO_LINK_TYPE,
|
||||
):
|
||||
if connection := self.find_connection_by_bd_addr(
|
||||
bd_addr, transport=BT_BR_EDR_TRANSPORT
|
||||
bd_addr, transport=PhysicalTransport.BR_EDR
|
||||
):
|
||||
self.emit('sco_request', connection, link_type)
|
||||
else:
|
||||
@@ -5404,7 +5439,7 @@ class Device(CompositeEventEmitter):
|
||||
connection.emit('disconnection_failure', error)
|
||||
|
||||
@host_event_handler
|
||||
@AsyncRunner.run_in_task()
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_inquiry_complete(self):
|
||||
if self.auto_restart_inquiry:
|
||||
# Inquire again
|
||||
@@ -5536,7 +5571,7 @@ class Device(CompositeEventEmitter):
|
||||
|
||||
async def reply() -> None:
|
||||
try:
|
||||
if await connection.abort_on('disconnection', method()):
|
||||
if await utils.cancel_on_event(connection, 'disconnection', method()):
|
||||
await self.host.send_command(
|
||||
hci.HCI_User_Confirmation_Request_Reply_Command(
|
||||
bd_addr=connection.peer_address
|
||||
@@ -5552,7 +5587,7 @@ class Device(CompositeEventEmitter):
|
||||
)
|
||||
)
|
||||
|
||||
AsyncRunner.spawn(reply())
|
||||
utils.AsyncRunner.spawn(reply())
|
||||
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@@ -5563,8 +5598,8 @@ class Device(CompositeEventEmitter):
|
||||
|
||||
async def reply() -> None:
|
||||
try:
|
||||
number = await connection.abort_on(
|
||||
'disconnection', pairing_config.delegate.get_number()
|
||||
number = await utils.cancel_on_event(
|
||||
connection, 'disconnection', pairing_config.delegate.get_number()
|
||||
)
|
||||
if number is not None:
|
||||
await self.host.send_command(
|
||||
@@ -5582,7 +5617,7 @@ class Device(CompositeEventEmitter):
|
||||
)
|
||||
)
|
||||
|
||||
AsyncRunner.spawn(reply())
|
||||
utils.AsyncRunner.spawn(reply())
|
||||
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@@ -5597,8 +5632,8 @@ class Device(CompositeEventEmitter):
|
||||
if io_capability == hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY:
|
||||
# Ask the user to enter a string
|
||||
async def get_pin_code():
|
||||
pin_code = await connection.abort_on(
|
||||
'disconnection', pairing_config.delegate.get_string(16)
|
||||
pin_code = await utils.cancel_on_event(
|
||||
connection, 'disconnection', pairing_config.delegate.get_string(16)
|
||||
)
|
||||
|
||||
if pin_code is not None:
|
||||
@@ -5636,8 +5671,8 @@ class Device(CompositeEventEmitter):
|
||||
pairing_config = self.pairing_config_factory(connection)
|
||||
|
||||
# Show the passkey to the user
|
||||
connection.abort_on(
|
||||
'disconnection', pairing_config.delegate.display_number(passkey)
|
||||
utils.cancel_on_event(
|
||||
connection, 'disconnection', pairing_config.delegate.display_number(passkey)
|
||||
)
|
||||
|
||||
# [Classic only]
|
||||
@@ -5669,7 +5704,7 @@ class Device(CompositeEventEmitter):
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
@experimental('Only for testing.')
|
||||
@utils.experimental('Only for testing.')
|
||||
def on_sco_connection(
|
||||
self, acl_connection: Connection, sco_handle: int, link_type: int
|
||||
) -> None:
|
||||
@@ -5689,7 +5724,7 @@ class Device(CompositeEventEmitter):
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
@experimental('Only for testing.')
|
||||
@utils.experimental('Only for testing.')
|
||||
def on_sco_connection_failure(
|
||||
self, acl_connection: Connection, status: int
|
||||
) -> None:
|
||||
@@ -5698,7 +5733,7 @@ class Device(CompositeEventEmitter):
|
||||
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@experimental('Only for testing')
|
||||
@utils.experimental('Only for testing')
|
||||
def on_sco_packet(
|
||||
self, sco_handle: int, packet: hci.HCI_SynchronousDataPacket
|
||||
) -> None:
|
||||
@@ -5708,7 +5743,7 @@ class Device(CompositeEventEmitter):
|
||||
# [LE only]
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
@experimental('Only for testing')
|
||||
@utils.experimental('Only for testing')
|
||||
def on_cis_request(
|
||||
self,
|
||||
acl_connection: Connection,
|
||||
@@ -5735,7 +5770,7 @@ class Device(CompositeEventEmitter):
|
||||
|
||||
# [LE only]
|
||||
@host_event_handler
|
||||
@experimental('Only for testing')
|
||||
@utils.experimental('Only for testing')
|
||||
def on_cis_establishment(self, cis_handle: int) -> None:
|
||||
cis_link = self.cis_links[cis_handle]
|
||||
cis_link.state = CisLink.State.ESTABLISHED
|
||||
@@ -5755,7 +5790,7 @@ class Device(CompositeEventEmitter):
|
||||
|
||||
# [LE only]
|
||||
@host_event_handler
|
||||
@experimental('Only for testing')
|
||||
@utils.experimental('Only for testing')
|
||||
def on_cis_establishment_failure(self, cis_handle: int, status: int) -> None:
|
||||
logger.debug(f'*** CIS Establishment Failure: cis=[0x{cis_handle:04X}] ***')
|
||||
if cis_link := self.cis_links.pop(cis_handle):
|
||||
@@ -5764,7 +5799,7 @@ class Device(CompositeEventEmitter):
|
||||
|
||||
# [LE only]
|
||||
@host_event_handler
|
||||
@experimental('Only for testing')
|
||||
@utils.experimental('Only for testing')
|
||||
def on_iso_packet(self, handle: int, packet: hci.HCI_IsoDataPacket) -> None:
|
||||
if (cis_link := self.cis_links.get(handle)) and cis_link.sink:
|
||||
cis_link.sink(packet)
|
||||
@@ -5782,14 +5817,14 @@ class Device(CompositeEventEmitter):
|
||||
connection.encryption = encryption
|
||||
if (
|
||||
not connection.authenticated
|
||||
and connection.transport == BT_BR_EDR_TRANSPORT
|
||||
and connection.transport == PhysicalTransport.BR_EDR
|
||||
and encryption == hci.HCI_Encryption_Change_Event.AES_CCM
|
||||
):
|
||||
connection.authenticated = True
|
||||
connection.sc = True
|
||||
if (
|
||||
not connection.authenticated
|
||||
and connection.transport == BT_LE_TRANSPORT
|
||||
and connection.transport == PhysicalTransport.LE
|
||||
and encryption == hci.HCI_Encryption_Change_Event.E0_OR_AES_CCM
|
||||
):
|
||||
connection.authenticated = True
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from .gatt import (
|
||||
from bumble.gatt import (
|
||||
Service,
|
||||
Characteristic,
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
|
||||
@@ -286,6 +286,22 @@ GATT_ASHA_AUDIO_STATUS_CHARACTERISTIC = UUID('38663f1a-e711-4cac-b641-32
|
||||
GATT_ASHA_VOLUME_CHARACTERISTIC = UUID('00e4ca9e-ab14-41e4-8823-f9e70c7e91df', 'Volume')
|
||||
GATT_ASHA_LE_PSM_OUT_CHARACTERISTIC = UUID('2d410339-82b6-42aa-b34e-e2e01df8cc1a', 'LE_PSM_OUT')
|
||||
|
||||
# Apple Notification Center Service
|
||||
GATT_ANCS_SERVICE = UUID('7905F431-B5CE-4E99-A40F-4B1E122D00D0', 'Apple Notification Center')
|
||||
GATT_ANCS_NOTIFICATION_SOURCE_CHARACTERISTIC = UUID('9FBF120D-6301-42D9-8C58-25E699A21DBD', 'Notification Source')
|
||||
GATT_ANCS_CONTROL_POINT_CHARACTERISTIC = UUID('69D1D8F3-45E1-49A8-9821-9BBDFDAAD9D9', 'Control Point')
|
||||
GATT_ANCS_DATA_SOURCE_CHARACTERISTIC = UUID('22EAC6E9-24D6-4BB5-BE44-B36ACE7C7BFB', 'Data Source')
|
||||
|
||||
# Apple Media Service
|
||||
GATT_AMS_SERVICE = UUID('89D3502B-0F36-433A-8EF4-C502AD55F8DC', 'Apple Media')
|
||||
GATT_AMS_REMOTE_COMMAND_CHARACTERISTIC = UUID('9B3C81D8-57B1-4A8A-B8DF-0E56F7CA51C2', 'Remote Command')
|
||||
GATT_AMS_ENTITY_UPDATE_CHARACTERISTIC = UUID('2F7CABCE-808D-411F-9A0C-BB92BA96C102', 'Entity Update')
|
||||
GATT_AMS_ENTITY_ATTRIBUTE_CHARACTERISTIC = UUID('C6B2F38C-23AB-46D8-A6AB-A3A870BBD5D7', 'Entity Attribute')
|
||||
|
||||
# Misc Apple Services
|
||||
GATT_APPLE_CONTINUITY_SERVICE = UUID('D0611E78-BBB4-4591-A5F8-487910AE4366', 'Apple Continuity')
|
||||
GATT_APPLE_NEARBY_SERVICE = UUID('9FA480E0-4967-4542-9390-D343DC5D04AE', 'Apple Nearby')
|
||||
|
||||
# Misc
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC = UUID.from_16_bits(0x2A00, 'Device Name')
|
||||
GATT_APPEARANCE_CHARACTERISTIC = UUID.from_16_bits(0x2A01, 'Appearance')
|
||||
|
||||
@@ -35,15 +35,15 @@ from typing import (
|
||||
from bumble.core import InvalidOperationError
|
||||
from bumble.gatt import Characteristic
|
||||
from bumble.gatt_client import CharacteristicProxy
|
||||
from bumble.utils import ByteSerializable, IntConvertible
|
||||
from bumble import utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Typing
|
||||
# -----------------------------------------------------------------------------
|
||||
_T = TypeVar('_T')
|
||||
_T2 = TypeVar('_T2', bound=ByteSerializable)
|
||||
_T3 = TypeVar('_T3', bound=IntConvertible)
|
||||
_T2 = TypeVar('_T2', bound=utils.ByteSerializable)
|
||||
_T3 = TypeVar('_T3', bound=utils.IntConvertible)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
+27
-21
@@ -44,11 +44,10 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from pyee import EventEmitter
|
||||
|
||||
from .colors import color
|
||||
from .hci import HCI_Constant
|
||||
from .att import (
|
||||
from bumble.colors import color
|
||||
from bumble.hci import HCI_Constant
|
||||
from bumble.att import (
|
||||
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
ATT_CID,
|
||||
@@ -69,9 +68,10 @@ from .att import (
|
||||
ATT_Write_Request,
|
||||
ATT_Error,
|
||||
)
|
||||
from . import core
|
||||
from .core import UUID, InvalidStateError
|
||||
from .gatt import (
|
||||
from bumble import utils
|
||||
from bumble import core
|
||||
from bumble.core import UUID, InvalidStateError
|
||||
from bumble.gatt import (
|
||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
@@ -117,11 +117,11 @@ def show_services(services: Iterable[ServiceProxy]) -> None:
|
||||
# -----------------------------------------------------------------------------
|
||||
# Proxies
|
||||
# -----------------------------------------------------------------------------
|
||||
class AttributeProxy(EventEmitter, Generic[_T]):
|
||||
class AttributeProxy(utils.EventEmitter, Generic[_T]):
|
||||
def __init__(
|
||||
self, client: Client, handle: int, end_group_handle: int, attribute_type: UUID
|
||||
) -> None:
|
||||
EventEmitter.__init__(self)
|
||||
utils.EventEmitter.__init__(self)
|
||||
self.client = client
|
||||
self.handle = handle
|
||||
self.end_group_handle = end_group_handle
|
||||
@@ -149,7 +149,7 @@ class AttributeProxy(EventEmitter, Generic[_T]):
|
||||
|
||||
class ServiceProxy(AttributeProxy):
|
||||
uuid: UUID
|
||||
characteristics: List[CharacteristicProxy]
|
||||
characteristics: List[CharacteristicProxy[bytes]]
|
||||
included_services: List[ServiceProxy]
|
||||
|
||||
@staticmethod
|
||||
@@ -170,14 +170,20 @@ class ServiceProxy(AttributeProxy):
|
||||
self.uuid = uuid
|
||||
self.characteristics = []
|
||||
|
||||
async def discover_characteristics(self, uuids=()) -> list[CharacteristicProxy]:
|
||||
async def discover_characteristics(
|
||||
self, uuids=()
|
||||
) -> list[CharacteristicProxy[bytes]]:
|
||||
return await self.client.discover_characteristics(uuids, self)
|
||||
|
||||
def get_characteristics_by_uuid(self, uuid: UUID) -> list[CharacteristicProxy]:
|
||||
def get_characteristics_by_uuid(
|
||||
self, uuid: UUID
|
||||
) -> list[CharacteristicProxy[bytes]]:
|
||||
"""Get all the characteristics with a specified UUID."""
|
||||
return self.client.get_characteristics_by_uuid(uuid, self)
|
||||
|
||||
def get_required_characteristic_by_uuid(self, uuid: UUID) -> CharacteristicProxy:
|
||||
def get_required_characteristic_by_uuid(
|
||||
self, uuid: UUID
|
||||
) -> CharacteristicProxy[bytes]:
|
||||
"""
|
||||
Get the first characteristic with a specified UUID.
|
||||
|
||||
@@ -256,7 +262,7 @@ class CharacteristicProxy(AttributeProxy[_T]):
|
||||
)
|
||||
|
||||
|
||||
class DescriptorProxy(AttributeProxy):
|
||||
class DescriptorProxy(AttributeProxy[bytes]):
|
||||
def __init__(self, client: Client, handle: int, descriptor_type: UUID) -> None:
|
||||
super().__init__(client, handle, 0, descriptor_type)
|
||||
|
||||
@@ -376,7 +382,7 @@ class Client:
|
||||
|
||||
def get_characteristics_by_uuid(
|
||||
self, uuid: UUID, service: Optional[ServiceProxy] = None
|
||||
) -> List[CharacteristicProxy]:
|
||||
) -> List[CharacteristicProxy[bytes]]:
|
||||
services = [service] if service else self.services
|
||||
return [
|
||||
c
|
||||
@@ -628,7 +634,7 @@ class Client:
|
||||
|
||||
async def discover_characteristics(
|
||||
self, uuids, service: Optional[ServiceProxy]
|
||||
) -> List[CharacteristicProxy]:
|
||||
) -> List[CharacteristicProxy[bytes]]:
|
||||
'''
|
||||
See Vol 3, Part G - 4.6.1 Discover All Characteristics of a Service and 4.6.2
|
||||
Discover Characteristics by UUID
|
||||
@@ -641,12 +647,12 @@ class Client:
|
||||
services = [service] if service else self.services
|
||||
|
||||
# Perform characteristic discovery for each service
|
||||
discovered_characteristics: List[CharacteristicProxy] = []
|
||||
discovered_characteristics: List[CharacteristicProxy[bytes]] = []
|
||||
for service in services:
|
||||
starting_handle = service.handle
|
||||
ending_handle = service.end_group_handle
|
||||
|
||||
characteristics: List[CharacteristicProxy] = []
|
||||
characteristics: List[CharacteristicProxy[bytes]] = []
|
||||
while starting_handle <= ending_handle:
|
||||
response = await self.send_request(
|
||||
ATT_Read_By_Type_Request(
|
||||
@@ -686,7 +692,7 @@ class Client:
|
||||
|
||||
properties, handle = struct.unpack_from('<BH', attribute_value)
|
||||
characteristic_uuid = UUID.from_bytes(attribute_value[3:])
|
||||
characteristic: CharacteristicProxy = CharacteristicProxy(
|
||||
characteristic = CharacteristicProxy[bytes](
|
||||
self, handle, 0, characteristic_uuid, properties
|
||||
)
|
||||
|
||||
@@ -779,7 +785,7 @@ class Client:
|
||||
|
||||
return descriptors
|
||||
|
||||
async def discover_attributes(self) -> List[AttributeProxy]:
|
||||
async def discover_attributes(self) -> List[AttributeProxy[bytes]]:
|
||||
'''
|
||||
Discover all attributes, regardless of type
|
||||
'''
|
||||
@@ -812,7 +818,7 @@ class Client:
|
||||
logger.warning(f'bogus handle value: {attribute_handle}')
|
||||
return []
|
||||
|
||||
attribute: AttributeProxy = AttributeProxy(
|
||||
attribute = AttributeProxy[bytes](
|
||||
self, attribute_handle, 0, UUID.from_bytes(attribute_uuid)
|
||||
)
|
||||
attributes.append(attribute)
|
||||
|
||||
+9
-10
@@ -38,7 +38,6 @@ from typing import (
|
||||
Type,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
from pyee import EventEmitter
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.core import UUID
|
||||
@@ -83,7 +82,7 @@ from bumble.gatt import (
|
||||
Descriptor,
|
||||
Service,
|
||||
)
|
||||
from bumble.utils import AsyncRunner
|
||||
from bumble import utils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Device, Connection
|
||||
@@ -103,7 +102,7 @@ GATT_SERVER_DEFAULT_MAX_MTU = 517
|
||||
# -----------------------------------------------------------------------------
|
||||
# GATT Server
|
||||
# -----------------------------------------------------------------------------
|
||||
class Server(EventEmitter):
|
||||
class Server(utils.EventEmitter):
|
||||
attributes: List[Attribute]
|
||||
services: List[Service]
|
||||
attributes_by_handle: Dict[int, Attribute]
|
||||
@@ -662,7 +661,7 @@ class Server(EventEmitter):
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_find_by_type_value_request(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.3.3 Find By Type Value Request
|
||||
@@ -715,7 +714,7 @@ class Server(EventEmitter):
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_read_by_type_request(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.1 Read By Type Request
|
||||
@@ -781,7 +780,7 @@ class Server(EventEmitter):
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_read_request(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.3 Read Request
|
||||
@@ -807,7 +806,7 @@ class Server(EventEmitter):
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_read_blob_request(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.5 Read Blob Request
|
||||
@@ -852,7 +851,7 @@ class Server(EventEmitter):
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_read_by_group_type_request(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.9 Read by Group Type Request
|
||||
@@ -920,7 +919,7 @@ class Server(EventEmitter):
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_write_request(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.5.1 Write Request
|
||||
@@ -967,7 +966,7 @@ class Server(EventEmitter):
|
||||
response = ATT_Write_Response()
|
||||
self.send_response(connection, response)
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_write_command(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.5.3 Write Command
|
||||
|
||||
+48
-22
@@ -29,7 +29,7 @@ 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,
|
||||
@@ -40,7 +40,7 @@ from bumble.core import (
|
||||
name_or_number,
|
||||
padded_bytes,
|
||||
)
|
||||
from bumble.utils import OpenIntEnum
|
||||
from bumble import utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -739,7 +739,7 @@ class PhyBit(enum.IntFlag):
|
||||
LE_CODED = 1 << HCI_LE_CODED_PHY_BIT
|
||||
|
||||
|
||||
class CsRole(OpenIntEnum):
|
||||
class CsRole(utils.OpenIntEnum):
|
||||
INITIATOR = 0x00
|
||||
REFLECTOR = 0x01
|
||||
|
||||
@@ -749,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
|
||||
@@ -760,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
|
||||
@@ -770,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
|
||||
@@ -779,20 +779,20 @@ 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
|
||||
@@ -890,7 +890,7 @@ HCI_LINK_TYPE_NAMES = {
|
||||
}
|
||||
|
||||
# Address types
|
||||
class AddressType(OpenIntEnum):
|
||||
class AddressType(utils.OpenIntEnum):
|
||||
PUBLIC_DEVICE = 0x00
|
||||
RANDOM_DEVICE = 0x01
|
||||
PUBLIC_IDENTITY = 0x02
|
||||
@@ -1239,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
|
||||
@@ -1537,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
|
||||
@@ -1976,7 +1976,7 @@ class Address:
|
||||
def from_string_for_transport(
|
||||
cls: type[Self], string: str, transport: PhysicalTransport
|
||||
) -> Self:
|
||||
if transport == BT_BR_EDR_TRANSPORT:
|
||||
if transport == PhysicalTransport.BR_EDR:
|
||||
address_type = Address.PUBLIC_DEVICE_ADDRESS
|
||||
else:
|
||||
address_type = Address.RANDOM_DEVICE_ADDRESS
|
||||
@@ -4883,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=[
|
||||
@@ -5356,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
|
||||
|
||||
@@ -5811,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,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -6041,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,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -6189,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
|
||||
@@ -6576,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
|
||||
|
||||
@@ -6628,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
|
||||
|
||||
@@ -6970,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
|
||||
|
||||
+3
-3
@@ -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.
|
||||
|
||||
|
||||
+4
-3
@@ -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
|
||||
|
||||
+22
-16
@@ -34,7 +34,6 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
import pyee
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.l2cap import L2CAP_PDU
|
||||
@@ -42,17 +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
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -62,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).
|
||||
|
||||
@@ -200,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
|
||||
@@ -235,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]
|
||||
@@ -967,7 +965,7 @@ class Host(AbortableEventEmitter):
|
||||
self,
|
||||
event.connection_handle,
|
||||
event.peer_address,
|
||||
BT_LE_TRANSPORT,
|
||||
PhysicalTransport.LE,
|
||||
)
|
||||
self.connections[event.connection_handle] = connection
|
||||
|
||||
@@ -980,7 +978,7 @@ 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),
|
||||
@@ -992,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):
|
||||
@@ -1017,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
|
||||
|
||||
@@ -1025,7 +1026,7 @@ class Host(AbortableEventEmitter):
|
||||
self.emit(
|
||||
'connection',
|
||||
event.connection_handle,
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
PhysicalTransport.BR_EDR,
|
||||
event.bd_addr,
|
||||
None,
|
||||
None,
|
||||
@@ -1037,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):
|
||||
@@ -1284,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(
|
||||
@@ -1442,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),
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
+19
-17
@@ -23,7 +23,6 @@ import logging
|
||||
import struct
|
||||
|
||||
from collections import deque
|
||||
from pyee import EventEmitter
|
||||
from typing import (
|
||||
Dict,
|
||||
Type,
|
||||
@@ -39,16 +38,16 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from .utils import deprecated
|
||||
from .colors import color
|
||||
from .core import (
|
||||
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,
|
||||
@@ -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,
|
||||
@@ -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)
|
||||
|
||||
+3
-4
@@ -20,8 +20,7 @@ import asyncio
|
||||
from functools import partial
|
||||
|
||||
from bumble.core import (
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
BT_LE_TRANSPORT,
|
||||
PhysicalTransport,
|
||||
InvalidStateError,
|
||||
)
|
||||
from bumble.colors import color
|
||||
@@ -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:
|
||||
|
||||
+3
-3
@@ -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 (
|
||||
|
||||
+18
-12
@@ -20,11 +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,
|
||||
PhysicalTransport,
|
||||
UUID,
|
||||
AdvertisingData,
|
||||
Appearance,
|
||||
@@ -185,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:
|
||||
@@ -218,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.
|
||||
@@ -250,7 +250,7 @@ class HostService(HostServicer):
|
||||
try:
|
||||
connection = await self.device.connect(
|
||||
address,
|
||||
transport=BT_LE_TRANSPORT,
|
||||
transport=PhysicalTransport.LE,
|
||||
own_address_type=OwnAddressType(request.own_address_type),
|
||||
)
|
||||
except ConnectionError as e:
|
||||
@@ -378,7 +378,7 @@ class HostService(HostServicer):
|
||||
|
||||
def on_connection(connection: bumble.device.Connection) -> None:
|
||||
if (
|
||||
connection.transport == BT_LE_TRANSPORT
|
||||
connection.transport == PhysicalTransport.LE
|
||||
and connection.role == Role.PERIPHERAL
|
||||
):
|
||||
connections.put_nowait(connection)
|
||||
@@ -496,7 +496,7 @@ class HostService(HostServicer):
|
||||
|
||||
def on_connection(connection: bumble.device.Connection) -> None:
|
||||
if (
|
||||
connection.transport == BT_LE_TRANSPORT
|
||||
connection.transport == PhysicalTransport.LE
|
||||
and connection.role == Role.PERIPHERAL
|
||||
):
|
||||
connections.put_nowait(connection)
|
||||
@@ -535,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
|
||||
|
||||
@@ -603,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
|
||||
|
||||
@@ -643,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 (
|
||||
|
||||
+15
-16
@@ -18,17 +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,
|
||||
PhysicalTransport,
|
||||
ProtocolError,
|
||||
)
|
||||
import bumble.utils
|
||||
from bumble.device import Connection as BumbleConnection, Device
|
||||
from bumble.hci import HCI_Error, Role
|
||||
from bumble.utils import EventWatcher
|
||||
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
|
||||
@@ -94,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
|
||||
@@ -173,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
|
||||
@@ -286,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
|
||||
|
||||
@@ -301,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:
|
||||
@@ -316,7 +315,7 @@ class SecurityService(SecurityServicer):
|
||||
security_result.set_result('connection_died')
|
||||
|
||||
if (
|
||||
connection.transport == BT_LE_TRANSPORT
|
||||
connection.transport == PhysicalTransport.LE
|
||||
and connection.role == Role.PERIPHERAL
|
||||
):
|
||||
connection.request_pairing()
|
||||
@@ -378,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()
|
||||
|
||||
@@ -426,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
|
||||
@@ -450,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)
|
||||
@@ -504,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
|
||||
@@ -517,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
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ from bumble.gatt_adapters import (
|
||||
UTF8CharacteristicProxyAdapter,
|
||||
)
|
||||
from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
|
||||
from bumble.utils import OpenIntEnum
|
||||
from bumble import utils
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -64,7 +64,7 @@ GAIN_SETTINGS_MIN_VALUE = 0
|
||||
GAIN_SETTINGS_MAX_VALUE = 255
|
||||
|
||||
|
||||
class ErrorCode(OpenIntEnum):
|
||||
class ErrorCode(utils.OpenIntEnum):
|
||||
'''
|
||||
Cf. 1.6 Application error codes
|
||||
'''
|
||||
@@ -76,7 +76,7 @@ class ErrorCode(OpenIntEnum):
|
||||
GAIN_MODE_CHANGE_NOT_ALLOWED = 0x84
|
||||
|
||||
|
||||
class Mute(OpenIntEnum):
|
||||
class Mute(utils.OpenIntEnum):
|
||||
'''
|
||||
Cf. 2.2.1.2 Mute Field
|
||||
'''
|
||||
@@ -86,7 +86,7 @@ class Mute(OpenIntEnum):
|
||||
DISABLED = 0x02
|
||||
|
||||
|
||||
class GainMode(OpenIntEnum):
|
||||
class GainMode(utils.OpenIntEnum):
|
||||
'''
|
||||
Cf. 2.2.1.3 Gain Mode
|
||||
'''
|
||||
@@ -97,7 +97,7 @@ class GainMode(OpenIntEnum):
|
||||
AUTOMATIC = 0x03
|
||||
|
||||
|
||||
class AudioInputStatus(OpenIntEnum):
|
||||
class AudioInputStatus(utils.OpenIntEnum):
|
||||
'''
|
||||
Cf. 3.4 Audio Input Status
|
||||
'''
|
||||
@@ -106,7 +106,7 @@ class AudioInputStatus(OpenIntEnum):
|
||||
ACTIVE = 0x01
|
||||
|
||||
|
||||
class AudioInputControlPointOpCode(OpenIntEnum):
|
||||
class AudioInputControlPointOpCode(utils.OpenIntEnum):
|
||||
'''
|
||||
Cf. 3.5.1 Audio Input Control Point procedure requirements
|
||||
'''
|
||||
|
||||
@@ -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)
|
||||
+17
-10
@@ -23,6 +23,7 @@ import logging
|
||||
import struct
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union
|
||||
|
||||
from bumble import utils
|
||||
from bumble import colors
|
||||
from bumble.profiles.bap import CodecSpecificConfiguration
|
||||
from bumble.profiles import le_audio
|
||||
@@ -343,8 +344,10 @@ class AseStateMachine(gatt.Characteristic):
|
||||
and cis_id == self.cis_id
|
||||
and self.state == self.State.ENABLING
|
||||
):
|
||||
acl_connection.abort_on(
|
||||
'flush', self.service.device.accept_cis_request(cis_handle)
|
||||
utils.cancel_on_event(
|
||||
acl_connection,
|
||||
'flush',
|
||||
self.service.device.accept_cis_request(cis_handle),
|
||||
)
|
||||
|
||||
def on_cis_establishment(self, cis_link: device.CisLink) -> None:
|
||||
@@ -361,7 +364,9 @@ class AseStateMachine(gatt.Characteristic):
|
||||
self.state = self.State.STREAMING
|
||||
await self.service.device.notify_subscribers(self, self.value)
|
||||
|
||||
cis_link.acl_connection.abort_on('flush', post_cis_established())
|
||||
utils.cancel_on_event(
|
||||
cis_link.acl_connection, 'flush', post_cis_established()
|
||||
)
|
||||
self.cis_link = cis_link
|
||||
|
||||
def on_cis_disconnection(self, _reason) -> None:
|
||||
@@ -509,7 +514,7 @@ class AseStateMachine(gatt.Characteristic):
|
||||
self.state = self.State.IDLE
|
||||
await self.service.device.notify_subscribers(self, self.value)
|
||||
|
||||
self.service.device.abort_on('flush', remove_cis_async())
|
||||
utils.cancel_on_event(self.service.device, 'flush', remove_cis_async())
|
||||
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
||||
|
||||
@property
|
||||
@@ -594,7 +599,7 @@ class AudioStreamControlService(gatt.TemplateService):
|
||||
UUID = gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE
|
||||
|
||||
ase_state_machines: Dict[int, AseStateMachine]
|
||||
ase_control_point: gatt.Characteristic
|
||||
ase_control_point: gatt.Characteristic[bytes]
|
||||
_active_client: Optional[device.Connection] = None
|
||||
|
||||
def __init__(
|
||||
@@ -691,7 +696,8 @@ class AudioStreamControlService(gatt.TemplateService):
|
||||
control_point_notification = bytes(
|
||||
[operation.op_code, len(responses)]
|
||||
) + b''.join(map(bytes, responses))
|
||||
self.device.abort_on(
|
||||
utils.cancel_on_event(
|
||||
self.device,
|
||||
'flush',
|
||||
self.device.notify_subscribers(
|
||||
self.ase_control_point, control_point_notification
|
||||
@@ -700,7 +706,8 @@ class AudioStreamControlService(gatt.TemplateService):
|
||||
|
||||
for ase_id, *_ in responses:
|
||||
if ase := self.ase_state_machines.get(ase_id):
|
||||
self.device.abort_on(
|
||||
utils.cancel_on_event(
|
||||
self.device,
|
||||
'flush',
|
||||
self.device.notify_subscribers(ase, ase.value),
|
||||
)
|
||||
@@ -710,9 +717,9 @@ class AudioStreamControlService(gatt.TemplateService):
|
||||
class AudioStreamControlServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
SERVICE_CLASS = AudioStreamControlService
|
||||
|
||||
sink_ase: List[gatt_client.CharacteristicProxy]
|
||||
source_ase: List[gatt_client.CharacteristicProxy]
|
||||
ase_control_point: gatt_client.CharacteristicProxy
|
||||
sink_ase: List[gatt_client.CharacteristicProxy[bytes]]
|
||||
source_ase: List[gatt_client.CharacteristicProxy[bytes]]
|
||||
ase_control_point: gatt_client.CharacteristicProxy[bytes]
|
||||
|
||||
def __init__(self, service_proxy: gatt_client.ServiceProxy):
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
@@ -259,11 +259,11 @@ class AshaService(gatt.TemplateService):
|
||||
# -----------------------------------------------------------------------------
|
||||
class AshaServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
SERVICE_CLASS = AshaService
|
||||
read_only_properties_characteristic: gatt_client.CharacteristicProxy
|
||||
audio_control_point_characteristic: gatt_client.CharacteristicProxy
|
||||
audio_status_point_characteristic: gatt_client.CharacteristicProxy
|
||||
volume_characteristic: gatt_client.CharacteristicProxy
|
||||
psm_characteristic: gatt_client.CharacteristicProxy
|
||||
read_only_properties_characteristic: gatt_client.CharacteristicProxy[bytes]
|
||||
audio_control_point_characteristic: gatt_client.CharacteristicProxy[bytes]
|
||||
audio_status_point_characteristic: gatt_client.CharacteristicProxy[bytes]
|
||||
volume_characteristic: gatt_client.CharacteristicProxy[bytes]
|
||||
psm_characteristic: gatt_client.CharacteristicProxy[bytes]
|
||||
|
||||
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
@@ -354,7 +354,7 @@ class BroadcastAudioScanService(gatt.TemplateService):
|
||||
class BroadcastAudioScanServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
SERVICE_CLASS = BroadcastAudioScanService
|
||||
|
||||
broadcast_audio_scan_control_point: gatt_client.CharacteristicProxy
|
||||
broadcast_audio_scan_control_point: gatt_client.CharacteristicProxy[bytes]
|
||||
broadcast_receive_states: list[
|
||||
gatt_client.CharacteristicProxy[Optional[BroadcastReceiveState]]
|
||||
]
|
||||
|
||||
+10
-10
@@ -99,10 +99,10 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
|
||||
UUID = gatt.GATT_COORDINATED_SET_IDENTIFICATION_SERVICE
|
||||
|
||||
set_identity_resolving_key: bytes
|
||||
set_identity_resolving_key_characteristic: gatt.Characteristic
|
||||
coordinated_set_size_characteristic: Optional[gatt.Characteristic] = None
|
||||
set_member_lock_characteristic: Optional[gatt.Characteristic] = None
|
||||
set_member_rank_characteristic: Optional[gatt.Characteristic] = None
|
||||
set_identity_resolving_key_characteristic: gatt.Characteristic[bytes]
|
||||
coordinated_set_size_characteristic: Optional[gatt.Characteristic[bytes]] = None
|
||||
set_member_lock_characteristic: Optional[gatt.Characteristic[bytes]] = None
|
||||
set_member_rank_characteristic: Optional[gatt.Characteristic[bytes]] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -170,7 +170,7 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
|
||||
else:
|
||||
assert connection
|
||||
|
||||
if connection.transport == core.BT_LE_TRANSPORT:
|
||||
if connection.transport == core.PhysicalTransport.LE:
|
||||
key = await connection.device.get_long_term_key(
|
||||
connection_handle=connection.handle, rand=b'', ediv=0
|
||||
)
|
||||
@@ -203,10 +203,10 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
|
||||
class CoordinatedSetIdentificationProxy(gatt_client.ProfileServiceProxy):
|
||||
SERVICE_CLASS = CoordinatedSetIdentificationService
|
||||
|
||||
set_identity_resolving_key: gatt_client.CharacteristicProxy
|
||||
coordinated_set_size: Optional[gatt_client.CharacteristicProxy] = None
|
||||
set_member_lock: Optional[gatt_client.CharacteristicProxy] = None
|
||||
set_member_rank: Optional[gatt_client.CharacteristicProxy] = None
|
||||
set_identity_resolving_key: gatt_client.CharacteristicProxy[bytes]
|
||||
coordinated_set_size: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
set_member_lock: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
set_member_rank: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
|
||||
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
||||
self.service_proxy = service_proxy
|
||||
@@ -242,7 +242,7 @@ class CoordinatedSetIdentificationProxy(gatt_client.ProfileServiceProxy):
|
||||
else:
|
||||
connection = self.service_proxy.client.connection
|
||||
device = connection.device
|
||||
if connection.transport == core.BT_LE_TRANSPORT:
|
||||
if connection.transport == core.PhysicalTransport.LE:
|
||||
key = await device.get_long_term_key(
|
||||
connection_handle=connection.handle, rand=b'', ediv=0
|
||||
)
|
||||
|
||||
@@ -32,10 +32,10 @@ class GenericAttributeProfileService(gatt.TemplateService):
|
||||
|
||||
UUID = gatt.GATT_GENERIC_ATTRIBUTE_SERVICE
|
||||
|
||||
client_supported_features_characteristic: gatt.Characteristic | None = None
|
||||
server_supported_features_characteristic: gatt.Characteristic | None = None
|
||||
database_hash_characteristic: gatt.Characteristic | None = None
|
||||
service_changed_characteristic: gatt.Characteristic | None = None
|
||||
client_supported_features_characteristic: gatt.Characteristic[bytes] | None = None
|
||||
server_supported_features_characteristic: gatt.Characteristic[bytes] | None = None
|
||||
database_hash_characteristic: gatt.Characteristic[bytes] | None = None
|
||||
service_changed_characteristic: gatt.Characteristic[bytes] | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -143,14 +143,14 @@ class GenericAttributeProfileService(gatt.TemplateService):
|
||||
class GenericAttributeProfileServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
SERVICE_CLASS = GenericAttributeProfileService
|
||||
|
||||
client_supported_features_characteristic: gatt_client.CharacteristicProxy | None = (
|
||||
None
|
||||
)
|
||||
server_supported_features_characteristic: gatt_client.CharacteristicProxy | None = (
|
||||
None
|
||||
)
|
||||
database_hash_characteristic: gatt_client.CharacteristicProxy | None = None
|
||||
service_changed_characteristic: gatt_client.CharacteristicProxy | None = None
|
||||
client_supported_features_characteristic: (
|
||||
gatt_client.CharacteristicProxy[bytes] | None
|
||||
) = None
|
||||
server_supported_features_characteristic: (
|
||||
gatt_client.CharacteristicProxy[bytes] | None
|
||||
) = None
|
||||
database_hash_characteristic: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
service_changed_characteristic: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
|
||||
_CHARACTERISTICS = {
|
||||
gatt.GATT_CLIENT_SUPPORTED_FEATURES_CHARACTERISTIC: 'client_supported_features_characteristic',
|
||||
|
||||
+16
-16
@@ -25,14 +25,14 @@ from typing import Any, Dict, List, Optional, Set, Union
|
||||
from bumble import att, gatt, gatt_adapters, gatt_client
|
||||
from bumble.core import InvalidArgumentError, InvalidStateError
|
||||
from bumble.device import Device, Connection
|
||||
from bumble.utils import AsyncRunner, OpenIntEnum
|
||||
from bumble import utils
|
||||
from bumble.hci import Address
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
class ErrorCode(OpenIntEnum):
|
||||
class ErrorCode(utils.OpenIntEnum):
|
||||
'''See Hearing Access Service 2.4. Attribute Profile error codes.'''
|
||||
|
||||
INVALID_OPCODE = 0x80
|
||||
@@ -42,7 +42,7 @@ class ErrorCode(OpenIntEnum):
|
||||
INVALID_PARAMETERS_LENGTH = 0x84
|
||||
|
||||
|
||||
class HearingAidType(OpenIntEnum):
|
||||
class HearingAidType(utils.OpenIntEnum):
|
||||
'''See Hearing Access Service 3.1. Hearing Aid Features.'''
|
||||
|
||||
BINAURAL_HEARING_AID = 0b00
|
||||
@@ -50,35 +50,35 @@ class HearingAidType(OpenIntEnum):
|
||||
BANDED_HEARING_AID = 0b10
|
||||
|
||||
|
||||
class PresetSynchronizationSupport(OpenIntEnum):
|
||||
class PresetSynchronizationSupport(utils.OpenIntEnum):
|
||||
'''See Hearing Access Service 3.1. Hearing Aid Features.'''
|
||||
|
||||
PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED = 0b0
|
||||
PRESET_SYNCHRONIZATION_IS_SUPPORTED = 0b1
|
||||
|
||||
|
||||
class IndependentPresets(OpenIntEnum):
|
||||
class IndependentPresets(utils.OpenIntEnum):
|
||||
'''See Hearing Access Service 3.1. Hearing Aid Features.'''
|
||||
|
||||
IDENTICAL_PRESET_RECORD = 0b0
|
||||
DIFFERENT_PRESET_RECORD = 0b1
|
||||
|
||||
|
||||
class DynamicPresets(OpenIntEnum):
|
||||
class DynamicPresets(utils.OpenIntEnum):
|
||||
'''See Hearing Access Service 3.1. Hearing Aid Features.'''
|
||||
|
||||
PRESET_RECORDS_DOES_NOT_CHANGE = 0b0
|
||||
PRESET_RECORDS_MAY_CHANGE = 0b1
|
||||
|
||||
|
||||
class WritablePresetsSupport(OpenIntEnum):
|
||||
class WritablePresetsSupport(utils.OpenIntEnum):
|
||||
'''See Hearing Access Service 3.1. Hearing Aid Features.'''
|
||||
|
||||
WRITABLE_PRESET_RECORDS_NOT_SUPPORTED = 0b0
|
||||
WRITABLE_PRESET_RECORDS_SUPPORTED = 0b1
|
||||
|
||||
|
||||
class HearingAidPresetControlPointOpcode(OpenIntEnum):
|
||||
class HearingAidPresetControlPointOpcode(utils.OpenIntEnum):
|
||||
'''See Hearing Access Service 3.3.1 Hearing Aid Preset Control Point operation requirements.'''
|
||||
|
||||
# fmt: off
|
||||
@@ -130,7 +130,7 @@ def HearingAidFeatures_from_bytes(data: int) -> HearingAidFeatures:
|
||||
class PresetChangedOperation:
|
||||
'''See Hearing Access Service 3.2.2.2. Preset Changed operation.'''
|
||||
|
||||
class ChangeId(OpenIntEnum):
|
||||
class ChangeId(utils.OpenIntEnum):
|
||||
# fmt: off
|
||||
GENERIC_UPDATE = 0x00
|
||||
PRESET_RECORD_DELETED = 0x01
|
||||
@@ -190,11 +190,11 @@ class PresetRecord:
|
||||
|
||||
@dataclass
|
||||
class Property:
|
||||
class Writable(OpenIntEnum):
|
||||
class Writable(utils.OpenIntEnum):
|
||||
CANNOT_BE_WRITTEN = 0b0
|
||||
CAN_BE_WRITTEN = 0b1
|
||||
|
||||
class IsAvailable(OpenIntEnum):
|
||||
class IsAvailable(utils.OpenIntEnum):
|
||||
IS_UNAVAILABLE = 0b0
|
||||
IS_AVAILABLE = 0b1
|
||||
|
||||
@@ -224,9 +224,9 @@ class PresetRecord:
|
||||
class HearingAccessService(gatt.TemplateService):
|
||||
UUID = gatt.GATT_HEARING_ACCESS_SERVICE
|
||||
|
||||
hearing_aid_features_characteristic: gatt.Characteristic
|
||||
hearing_aid_preset_control_point: gatt.Characteristic
|
||||
active_preset_index_characteristic: gatt.Characteristic
|
||||
hearing_aid_features_characteristic: gatt.Characteristic[bytes]
|
||||
hearing_aid_preset_control_point: gatt.Characteristic[bytes]
|
||||
active_preset_index_characteristic: gatt.Characteristic[bytes]
|
||||
active_preset_index: int
|
||||
active_preset_index_per_device: Dict[Address, int]
|
||||
|
||||
@@ -333,7 +333,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
# Update the active preset index if needed
|
||||
await self.notify_active_preset_for_connection(connection)
|
||||
|
||||
connection.abort_on('disconnection', on_connection_async())
|
||||
utils.cancel_on_event(connection, 'disconnection', on_connection_async())
|
||||
|
||||
def _on_read_active_preset_index(
|
||||
self, __connection__: Optional[Connection]
|
||||
@@ -382,7 +382,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
if len(presets) == 0:
|
||||
raise att.ATT_Error(att.ErrorCode.OUT_OF_RANGE)
|
||||
|
||||
AsyncRunner.spawn(self._read_preset_response(connection, presets))
|
||||
utils.AsyncRunner.spawn(self._read_preset_response(connection, presets))
|
||||
|
||||
async def _read_preset_response(
|
||||
self, connection: Connection, presets: List[PresetRecord]
|
||||
|
||||
+26
-24
@@ -338,30 +338,32 @@ class MediaControlServiceProxy(
|
||||
'content_control_id': gatt.GATT_CONTENT_CONTROL_ID_CHARACTERISTIC,
|
||||
}
|
||||
|
||||
media_player_name: Optional[gatt_client.CharacteristicProxy] = None
|
||||
media_player_icon_object_id: Optional[gatt_client.CharacteristicProxy] = None
|
||||
media_player_icon_url: Optional[gatt_client.CharacteristicProxy] = None
|
||||
track_changed: Optional[gatt_client.CharacteristicProxy] = None
|
||||
track_title: Optional[gatt_client.CharacteristicProxy] = None
|
||||
track_duration: Optional[gatt_client.CharacteristicProxy] = None
|
||||
track_position: Optional[gatt_client.CharacteristicProxy] = None
|
||||
playback_speed: Optional[gatt_client.CharacteristicProxy] = None
|
||||
seeking_speed: Optional[gatt_client.CharacteristicProxy] = None
|
||||
current_track_segments_object_id: Optional[gatt_client.CharacteristicProxy] = None
|
||||
current_track_object_id: Optional[gatt_client.CharacteristicProxy] = None
|
||||
next_track_object_id: Optional[gatt_client.CharacteristicProxy] = None
|
||||
parent_group_object_id: Optional[gatt_client.CharacteristicProxy] = None
|
||||
current_group_object_id: Optional[gatt_client.CharacteristicProxy] = None
|
||||
playing_order: Optional[gatt_client.CharacteristicProxy] = None
|
||||
playing_orders_supported: Optional[gatt_client.CharacteristicProxy] = None
|
||||
media_state: Optional[gatt_client.CharacteristicProxy] = None
|
||||
media_control_point: Optional[gatt_client.CharacteristicProxy] = None
|
||||
media_control_point_opcodes_supported: Optional[gatt_client.CharacteristicProxy] = (
|
||||
None
|
||||
)
|
||||
search_control_point: Optional[gatt_client.CharacteristicProxy] = None
|
||||
search_results_object_id: Optional[gatt_client.CharacteristicProxy] = None
|
||||
content_control_id: Optional[gatt_client.CharacteristicProxy] = None
|
||||
media_player_name: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
media_player_icon_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
media_player_icon_url: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
track_changed: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
track_title: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
track_duration: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
track_position: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
playback_speed: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
seeking_speed: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
current_track_segments_object_id: Optional[
|
||||
gatt_client.CharacteristicProxy[bytes]
|
||||
] = None
|
||||
current_track_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
next_track_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
parent_group_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
current_group_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
playing_order: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
playing_orders_supported: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
media_state: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
media_control_point: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
media_control_point_opcodes_supported: Optional[
|
||||
gatt_client.CharacteristicProxy[bytes]
|
||||
] = None
|
||||
search_control_point: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
search_results_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
content_control_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
|
||||
if TYPE_CHECKING:
|
||||
media_control_point_notifications: asyncio.Queue[bytes]
|
||||
|
||||
@@ -104,12 +104,12 @@ class PacRecord:
|
||||
class PublishedAudioCapabilitiesService(gatt.TemplateService):
|
||||
UUID = gatt.GATT_PUBLISHED_AUDIO_CAPABILITIES_SERVICE
|
||||
|
||||
sink_pac: Optional[gatt.Characteristic]
|
||||
sink_audio_locations: Optional[gatt.Characteristic]
|
||||
source_pac: Optional[gatt.Characteristic]
|
||||
source_audio_locations: Optional[gatt.Characteristic]
|
||||
available_audio_contexts: gatt.Characteristic
|
||||
supported_audio_contexts: gatt.Characteristic
|
||||
sink_pac: Optional[gatt.Characteristic[bytes]]
|
||||
sink_audio_locations: Optional[gatt.Characteristic[bytes]]
|
||||
source_pac: Optional[gatt.Characteristic[bytes]]
|
||||
source_audio_locations: Optional[gatt.Characteristic[bytes]]
|
||||
available_audio_contexts: gatt.Characteristic[bytes]
|
||||
supported_audio_contexts: gatt.Characteristic[bytes]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -23,6 +23,7 @@ import enum
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from bumble import att
|
||||
from bumble import utils
|
||||
from bumble import device
|
||||
from bumble import gatt
|
||||
from bumble import gatt_adapters
|
||||
@@ -90,9 +91,9 @@ class VolumeState:
|
||||
class VolumeControlService(gatt.TemplateService):
|
||||
UUID = gatt.GATT_VOLUME_CONTROL_SERVICE
|
||||
|
||||
volume_state: gatt.Characteristic
|
||||
volume_control_point: gatt.Characteristic
|
||||
volume_flags: gatt.Characteristic
|
||||
volume_state: gatt.Characteristic[bytes]
|
||||
volume_control_point: gatt.Characteristic[bytes]
|
||||
volume_flags: gatt.Characteristic[bytes]
|
||||
|
||||
volume_setting: int
|
||||
muted: int
|
||||
@@ -160,7 +161,8 @@ class VolumeControlService(gatt.TemplateService):
|
||||
handler = getattr(self, '_on_' + opcode.name.lower())
|
||||
if handler(*value[2:]):
|
||||
self.change_counter = (self.change_counter + 1) % 256
|
||||
connection.abort_on(
|
||||
utils.cancel_on_event(
|
||||
connection,
|
||||
'disconnection',
|
||||
connection.device.notify_subscribers(attribute=self.volume_state),
|
||||
)
|
||||
|
||||
@@ -38,7 +38,7 @@ from bumble.gatt_adapters import (
|
||||
UTF8CharacteristicProxyAdapter,
|
||||
)
|
||||
from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
|
||||
from bumble.utils import OpenIntEnum
|
||||
from bumble import utils
|
||||
from bumble.profiles.bap import AudioLocation
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -50,11 +50,11 @@ MAX_VOLUME_OFFSET = 255
|
||||
CHANGE_COUNTER_MAX_VALUE = 0xFF
|
||||
|
||||
|
||||
class SetVolumeOffsetOpCode(OpenIntEnum):
|
||||
class SetVolumeOffsetOpCode(utils.OpenIntEnum):
|
||||
SET_VOLUME_OFFSET = 0x01
|
||||
|
||||
|
||||
class ErrorCode(OpenIntEnum):
|
||||
class ErrorCode(utils.OpenIntEnum):
|
||||
"""
|
||||
See Volume Offset Control Service 1.6. Application error codes.
|
||||
"""
|
||||
|
||||
+8
-8
@@ -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:
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+36
-30
@@ -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_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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+92
-79
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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}!')
|
||||
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
from bumble.colors import color
|
||||
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.transport import open_transport
|
||||
from bumble.profiles.ancs import (
|
||||
AncsClient,
|
||||
AppAttribute,
|
||||
AppAttributeId,
|
||||
EventFlags,
|
||||
EventId,
|
||||
Notification,
|
||||
NotificationAttributeId,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
_cached_app_names: dict[str, str] = {}
|
||||
_notification_queue = asyncio.Queue[Notification]()
|
||||
|
||||
|
||||
async def process_notifications(ancs_client: AncsClient):
|
||||
while True:
|
||||
notification = await _notification_queue.get()
|
||||
|
||||
prefix = " "
|
||||
if notification.event_id == EventId.NOTIFICATION_ADDED:
|
||||
print_color = "green"
|
||||
if notification.event_flags & EventFlags.PRE_EXISTING:
|
||||
prefix = " Existing "
|
||||
else:
|
||||
prefix = " New "
|
||||
elif notification.event_id == EventId.NOTIFICATION_REMOVED:
|
||||
print_color = "red"
|
||||
elif notification.event_id == EventId.NOTIFICATION_MODIFIED:
|
||||
print_color = "yellow"
|
||||
else:
|
||||
print_color = "white"
|
||||
|
||||
print(
|
||||
color(
|
||||
(
|
||||
f"[{notification.event_id.name}]{prefix}Notification "
|
||||
f"({notification.notification_uid}):"
|
||||
),
|
||||
print_color,
|
||||
)
|
||||
)
|
||||
print(color(" Event ID: ", "yellow"), notification.event_id.name)
|
||||
print(color(" Event Flags: ", "yellow"), notification.event_flags.name)
|
||||
print(color(" Category ID: ", "yellow"), notification.category_id.name)
|
||||
print(color(" Category Count:", "yellow"), notification.category_count)
|
||||
|
||||
if notification.event_id not in (
|
||||
EventId.NOTIFICATION_ADDED,
|
||||
EventId.NOTIFICATION_MODIFIED,
|
||||
):
|
||||
continue
|
||||
|
||||
requested_attributes = [
|
||||
NotificationAttributeId.APP_IDENTIFIER,
|
||||
NotificationAttributeId.TITLE,
|
||||
NotificationAttributeId.SUBTITLE,
|
||||
NotificationAttributeId.MESSAGE,
|
||||
NotificationAttributeId.DATE,
|
||||
]
|
||||
if notification.event_flags & EventFlags.NEGATIVE_ACTION:
|
||||
requested_attributes.append(NotificationAttributeId.NEGATIVE_ACTION_LABEL)
|
||||
if notification.event_flags & EventFlags.POSITIVE_ACTION:
|
||||
requested_attributes.append(NotificationAttributeId.POSITIVE_ACTION_LABEL)
|
||||
|
||||
attributes = await ancs_client.get_notification_attributes(
|
||||
notification.notification_uid, requested_attributes
|
||||
)
|
||||
max_attribute_name_width = max(
|
||||
(len(attribute.attribute_id.name) for attribute in attributes)
|
||||
)
|
||||
app_identifier = str(
|
||||
next(
|
||||
(
|
||||
attribute.value
|
||||
for attribute in attributes
|
||||
if attribute.attribute_id == NotificationAttributeId.APP_IDENTIFIER
|
||||
)
|
||||
)
|
||||
)
|
||||
if app_identifier not in _cached_app_names:
|
||||
app_attributes = await ancs_client.get_app_attributes(
|
||||
app_identifier, [AppAttributeId.DISPLAY_NAME]
|
||||
)
|
||||
_cached_app_names[app_identifier] = app_attributes[0].value
|
||||
app_name = _cached_app_names[app_identifier]
|
||||
|
||||
for attribute in attributes:
|
||||
padding = ' ' * (
|
||||
max_attribute_name_width - len(attribute.attribute_id.name)
|
||||
)
|
||||
suffix = (
|
||||
f" ({app_name})"
|
||||
if attribute.attribute_id == NotificationAttributeId.APP_IDENTIFIER
|
||||
else ""
|
||||
)
|
||||
print(
|
||||
color(f" {attribute.attribute_id.name}:{padding}", "blue"),
|
||||
f"{attribute.value}{suffix}",
|
||||
)
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def on_ancs_notification(notification: Notification) -> None:
|
||||
_notification_queue.put_nowait(notification)
|
||||
|
||||
|
||||
async def handle_command_client(
|
||||
ancs_client: AncsClient, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
||||
) -> None:
|
||||
while True:
|
||||
command = (await reader.readline()).decode("utf-8").strip()
|
||||
|
||||
try:
|
||||
command_name, command_args = command.split(" ", 1)
|
||||
if command_name == "+":
|
||||
notification_uid = int(command_args)
|
||||
await ancs_client.perform_positive_action(notification_uid)
|
||||
elif command_name == "-":
|
||||
notification_uid = int(command_args)
|
||||
await ancs_client.perform_negative_action(notification_uid)
|
||||
else:
|
||||
writer.write(f"unknown command {command_name}".encode("utf-8"))
|
||||
except Exception as error:
|
||||
writer.write(f"ERROR: {error}\n".encode("utf-8"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def main() -> None:
|
||||
if len(sys.argv) < 3:
|
||||
print(
|
||||
'Usage: run_ancs_client.py <device-config> <transport-spec> '
|
||||
'<bluetooth-address> <mtu>'
|
||||
)
|
||||
print('example: run_ancs_client.py device1.json usb:0 E1:CA:72:48:C4:E8 512')
|
||||
return
|
||||
device_config, transport_spec, bluetooth_address, mtu = sys.argv[1:]
|
||||
|
||||
print('<<< connecting to HCI...')
|
||||
async with await open_transport(transport_spec) as hci_transport:
|
||||
print('<<< connected')
|
||||
|
||||
# Create a device to manage the host, with a custom listener
|
||||
device = Device.from_config_file_with_hci(
|
||||
device_config, hci_transport.source, hci_transport.sink
|
||||
)
|
||||
await device.power_on()
|
||||
|
||||
# Connect to the peer
|
||||
print(f'=== Connecting to {bluetooth_address}...')
|
||||
connection = await device.connect(bluetooth_address)
|
||||
print(f'=== Connected: {connection}')
|
||||
|
||||
await connection.encrypt()
|
||||
|
||||
peer = Peer(connection)
|
||||
mtu_int = int(mtu)
|
||||
if mtu_int:
|
||||
new_mtu = await peer.request_mtu(mtu_int)
|
||||
print(f'ATT MTU = {new_mtu}')
|
||||
ancs_client = await AncsClient.for_peer(peer)
|
||||
if ancs_client is None:
|
||||
print("!!! no ANCS service found")
|
||||
return
|
||||
await ancs_client.start()
|
||||
|
||||
print('Subscribing to updates')
|
||||
ancs_client.on("notification", on_ancs_notification)
|
||||
|
||||
# Process all notifications in a task.
|
||||
notification_processing_task = asyncio.create_task(
|
||||
process_notifications(ancs_client)
|
||||
)
|
||||
|
||||
# Accept a TCP connection to handle commands.
|
||||
tcp_server = await asyncio.start_server(
|
||||
lambda reader, writer: handle_command_client(ancs_client, reader, writer),
|
||||
'127.0.0.1',
|
||||
9000,
|
||||
)
|
||||
print("Accepting command client on port 9000")
|
||||
async with tcp_server:
|
||||
await tcp_server.serve_forever()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
asyncio.run(main())
|
||||
@@ -25,7 +25,7 @@ import websockets
|
||||
|
||||
from bumble.device import Device
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.core import BT_BR_EDR_TRANSPORT
|
||||
from bumble.core import PhysicalTransport
|
||||
from bumble import avc
|
||||
from bumble import avrcp
|
||||
from bumble import avdtp
|
||||
@@ -379,7 +379,7 @@ async def main() -> None:
|
||||
target_address = sys.argv[4]
|
||||
print(f'=== Connecting to {target_address}...')
|
||||
connection = await device.connect(
|
||||
target_address, transport=BT_BR_EDR_TRANSPORT
|
||||
target_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
print(f'=== Connected to {connection.peer_address}!')
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ async def main() -> None:
|
||||
|
||||
print(f'<<< Connecting to {target_address}')
|
||||
connection = await device.connect(
|
||||
target_address, transport=core.BT_LE_TRANSPORT
|
||||
target_address, transport=core.PhysicalTransport.LE
|
||||
)
|
||||
print('<<< ACL Connected')
|
||||
if not (await device.get_long_term_key(connection.handle, b'', 0)):
|
||||
|
||||
@@ -19,12 +19,8 @@ import asyncio
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
from bumble.device import (
|
||||
Device,
|
||||
Connection,
|
||||
AdvertisingParameters,
|
||||
AdvertisingEventProperties,
|
||||
)
|
||||
from bumble import utils
|
||||
from bumble.device import Device, Connection
|
||||
from bumble.hci import (
|
||||
OwnAddressType,
|
||||
)
|
||||
@@ -79,7 +75,9 @@ async def main() -> None:
|
||||
def on_cis_request(
|
||||
connection: Connection, cis_handle: int, _cig_id: int, _cis_id: int
|
||||
):
|
||||
connection.abort_on('disconnection', devices[0].accept_cis_request(cis_handle))
|
||||
utils.cancel_on_event(
|
||||
connection, 'disconnection', devices[0].accept_cis_request(cis_handle)
|
||||
)
|
||||
|
||||
devices[0].on('cis_request', on_cis_request)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ from bumble.colors import color
|
||||
|
||||
from bumble.device import Device
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.core import BT_BR_EDR_TRANSPORT, BT_L2CAP_PROTOCOL_ID, CommandTimeoutError
|
||||
from bumble.core import PhysicalTransport, BT_L2CAP_PROTOCOL_ID, CommandTimeoutError
|
||||
from bumble.sdp import (
|
||||
Client as SDP_Client,
|
||||
SDP_PUBLIC_BROWSE_ROOT,
|
||||
@@ -57,7 +57,7 @@ async def main() -> None:
|
||||
print(f'=== Connecting to {target_address}...')
|
||||
try:
|
||||
connection = await device.connect(
|
||||
target_address, transport=BT_BR_EDR_TRANSPORT
|
||||
target_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
except CommandTimeoutError:
|
||||
print('!!! Connection timed out')
|
||||
|
||||
@@ -16,11 +16,10 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
from bumble.core import BT_BR_EDR_TRANSPORT
|
||||
from bumble.core import PhysicalTransport
|
||||
from bumble.device import Device, ScoLink
|
||||
from bumble.hci import HCI_Enhanced_Setup_Synchronous_Connection_Command
|
||||
from bumble.hfp import DefaultCodecParameters, ESCO_PARAMETERS
|
||||
@@ -61,7 +60,9 @@ async def main() -> None:
|
||||
|
||||
connections = await asyncio.gather(
|
||||
devices[0].accept(devices[1].public_address),
|
||||
devices[1].connect(devices[0].public_address, transport=BT_BR_EDR_TRANSPORT),
|
||||
devices[1].connect(
|
||||
devices[0].public_address, transport=PhysicalTransport.BR_EDR
|
||||
),
|
||||
)
|
||||
|
||||
def on_sco(sco_link: ScoLink):
|
||||
|
||||
@@ -28,9 +28,7 @@ import websockets
|
||||
import bumble.core
|
||||
from bumble.device import Device, ScoLink
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.core import (
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
)
|
||||
from bumble.core import PhysicalTransport
|
||||
from bumble import hci, rfcomm, hfp
|
||||
|
||||
|
||||
@@ -234,7 +232,7 @@ async def main() -> None:
|
||||
target_address = sys.argv[3]
|
||||
print(f'=== Connecting to {target_address}...')
|
||||
connection = await device.connect(
|
||||
target_address, transport=BT_BR_EDR_TRANSPORT
|
||||
target_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
print(f'=== Connected to {connection.peer_address}!')
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import websockets
|
||||
import functools
|
||||
from typing import Optional
|
||||
|
||||
from bumble import utils
|
||||
from bumble import rfcomm
|
||||
from bumble import hci
|
||||
from bumble.device import Device, Connection
|
||||
@@ -60,7 +61,8 @@ def on_dlc(dlc: rfcomm.DLC, configuration: hfp.HfConfiguration):
|
||||
else:
|
||||
raise RuntimeError("unknown active codec")
|
||||
|
||||
connection.abort_on(
|
||||
utils.cancel_on_event(
|
||||
connection,
|
||||
'disconnection',
|
||||
connection.device.send_command(
|
||||
hci.HCI_Enhanced_Accept_Synchronous_Connection_Request_Command(
|
||||
|
||||
@@ -26,7 +26,7 @@ import struct
|
||||
from bumble.device import Device
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.core import (
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
PhysicalTransport,
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
BT_HUMAN_INTERFACE_DEVICE_SERVICE,
|
||||
BT_HIDP_PROTOCOL_ID,
|
||||
@@ -721,7 +721,7 @@ async def main() -> None:
|
||||
elif choice == '9':
|
||||
hid_host_bd_addr = str(hid_device.remote_device_bd_address)
|
||||
connection = await device.connect(
|
||||
hid_host_bd_addr, transport=BT_BR_EDR_TRANSPORT
|
||||
hid_host_bd_addr, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
await connection.authenticate()
|
||||
await connection.encrypt()
|
||||
|
||||
@@ -26,7 +26,7 @@ from bumble.device import Device
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.core import (
|
||||
BT_HUMAN_INTERFACE_DEVICE_SERVICE,
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
PhysicalTransport,
|
||||
)
|
||||
from bumble.hci import Address
|
||||
from bumble.hid import Host, Message
|
||||
@@ -349,7 +349,9 @@ async def main() -> None:
|
||||
# Connect to a peer
|
||||
target_address = sys.argv[3]
|
||||
print(f'=== Connecting to {target_address}...')
|
||||
connection = await device.connect(target_address, transport=BT_BR_EDR_TRANSPORT)
|
||||
connection = await device.connect(
|
||||
target_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
print(f'=== Connected to {connection.peer_address}!')
|
||||
|
||||
# Request authentication
|
||||
@@ -519,10 +521,10 @@ async def main() -> None:
|
||||
|
||||
elif choice == '13':
|
||||
peer_address = Address.from_string_for_transport(
|
||||
target_address, transport=BT_BR_EDR_TRANSPORT
|
||||
target_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
connection = device.find_connection_by_bd_addr(
|
||||
peer_address, transport=BT_BR_EDR_TRANSPORT
|
||||
peer_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
if connection is not None:
|
||||
await connection.disconnect()
|
||||
@@ -538,7 +540,7 @@ async def main() -> None:
|
||||
|
||||
elif choice == '15':
|
||||
connection = await device.connect(
|
||||
target_address, transport=BT_BR_EDR_TRANSPORT
|
||||
target_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
await connection.authenticate()
|
||||
await connection.encrypt()
|
||||
|
||||
@@ -22,6 +22,7 @@ import os
|
||||
import websockets
|
||||
import json
|
||||
|
||||
from bumble import utils
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import (
|
||||
Device,
|
||||
@@ -169,7 +170,7 @@ async def main() -> None:
|
||||
mcp.on('track_position', on_track_position)
|
||||
await mcp.subscribe_characteristics()
|
||||
|
||||
connection.abort_on('disconnection', on_connection_async())
|
||||
utils.cancel_on_event(connection, 'disconnection', on_connection_async())
|
||||
|
||||
device.on('connection', on_connection)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ from bumble.transport import open_transport_or_link
|
||||
from bumble.core import (
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
BT_RFCOMM_PROTOCOL_ID,
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
PhysicalTransport,
|
||||
)
|
||||
from bumble.rfcomm import Client
|
||||
from bumble.sdp import (
|
||||
@@ -191,7 +191,9 @@ async def main() -> None:
|
||||
# Connect to a peer
|
||||
target_address = sys.argv[3]
|
||||
print(f'=== Connecting to {target_address}...')
|
||||
connection = await device.connect(target_address, transport=BT_BR_EDR_TRANSPORT)
|
||||
connection = await device.connect(
|
||||
target_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
print(f'=== Connected to {connection.peer_address}!')
|
||||
|
||||
channel_str = sys.argv[4]
|
||||
|
||||
+1
@@ -26,6 +26,7 @@ class Advertiser(private val bluetoothAdapter: BluetoothAdapter) : AdvertiseCall
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun stop() {
|
||||
Log.info("stopping advertiser")
|
||||
bluetoothAdapter.bluetoothLeAdvertiser.stopAdvertising(this)
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -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
|
||||
|
||||
+3
-3
@@ -21,7 +21,7 @@ import os
|
||||
import pytest
|
||||
|
||||
from bumble.controller import Controller
|
||||
from bumble.core import BT_BR_EDR_TRANSPORT
|
||||
from bumble.core import PhysicalTransport
|
||||
from bumble.link import LocalLink
|
||||
from bumble.device import Device
|
||||
from bumble.host import Host
|
||||
@@ -106,7 +106,7 @@ async def test_self_connection():
|
||||
# Connect the two devices
|
||||
await asyncio.gather(
|
||||
two_devices.devices[0].connect(
|
||||
two_devices.devices[1].public_address, transport=BT_BR_EDR_TRANSPORT
|
||||
two_devices.devices[1].public_address, transport=PhysicalTransport.BR_EDR
|
||||
),
|
||||
two_devices.devices[1].accept(two_devices.devices[0].public_address),
|
||||
)
|
||||
@@ -190,7 +190,7 @@ async def test_source_sink_1():
|
||||
async def make_connection():
|
||||
connections = await asyncio.gather(
|
||||
two_devices.devices[0].connect(
|
||||
two_devices.devices[1].public_address, BT_BR_EDR_TRANSPORT
|
||||
two_devices.devices[1].public_address, PhysicalTransport.BR_EDR
|
||||
),
|
||||
two_devices.devices[1].accept(two_devices.devices[0].public_address),
|
||||
)
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ class TwoDevices:
|
||||
|
||||
self.connections = await asyncio.gather(
|
||||
self.devices[0].connect(
|
||||
self.devices[1].public_address, core.BT_BR_EDR_TRANSPORT
|
||||
self.devices[1].public_address, core.PhysicalTransport.BR_EDR
|
||||
),
|
||||
self.devices[1].accept(self.devices[0].public_address),
|
||||
)
|
||||
|
||||
+10
-10
@@ -22,8 +22,7 @@ import os
|
||||
import pytest
|
||||
|
||||
from bumble.core import (
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
BT_LE_TRANSPORT,
|
||||
PhysicalTransport,
|
||||
ConnectionParameters,
|
||||
)
|
||||
from bumble.device import (
|
||||
@@ -50,6 +49,7 @@ from bumble.hci import (
|
||||
HCI_Error,
|
||||
HCI_Packet,
|
||||
)
|
||||
from bumble import utils
|
||||
from bumble import gatt
|
||||
|
||||
from .test_utils import TwoDevices, async_barrier
|
||||
@@ -229,10 +229,10 @@ async def test_device_connect_parallel():
|
||||
[c01, c02, a10, a20] = await asyncio.gather(
|
||||
*[
|
||||
asyncio.create_task(
|
||||
d0.connect(d1.public_address, transport=BT_BR_EDR_TRANSPORT)
|
||||
d0.connect(d1.public_address, transport=PhysicalTransport.BR_EDR)
|
||||
),
|
||||
asyncio.create_task(
|
||||
d0.connect(d2.public_address, transport=BT_BR_EDR_TRANSPORT)
|
||||
d0.connect(d2.public_address, transport=PhysicalTransport.BR_EDR)
|
||||
),
|
||||
d1_accept_task,
|
||||
d2_accept_task,
|
||||
@@ -252,7 +252,7 @@ async def test_device_connect_parallel():
|
||||
@pytest.mark.asyncio
|
||||
async def test_flush():
|
||||
d0 = Device(host=Host(None, None))
|
||||
task = d0.abort_on('flush', asyncio.sleep(10000))
|
||||
task = utils.cancel_on_event(d0, 'flush', asyncio.sleep(10000))
|
||||
await d0.host.flush()
|
||||
try:
|
||||
await task
|
||||
@@ -291,7 +291,7 @@ async def test_legacy_advertising_disconnection(auto_restart):
|
||||
await device.start_advertising(auto_restart=auto_restart)
|
||||
device.on_connection(
|
||||
0x0001,
|
||||
BT_LE_TRANSPORT,
|
||||
PhysicalTransport.LE,
|
||||
peer_address,
|
||||
None,
|
||||
None,
|
||||
@@ -349,7 +349,7 @@ async def test_extended_advertising_connection(own_address_type):
|
||||
)
|
||||
device.on_connection(
|
||||
0x0001,
|
||||
BT_LE_TRANSPORT,
|
||||
PhysicalTransport.LE,
|
||||
peer_address,
|
||||
None,
|
||||
None,
|
||||
@@ -393,7 +393,7 @@ async def test_extended_advertising_connection_out_of_order(own_address_type):
|
||||
)
|
||||
device.on_connection(
|
||||
0x0001,
|
||||
BT_LE_TRANSPORT,
|
||||
PhysicalTransport.LE,
|
||||
Address('F0:F1:F2:F3:F4:F5'),
|
||||
None,
|
||||
None,
|
||||
@@ -483,8 +483,8 @@ async def test_cis():
|
||||
_cig_id: int,
|
||||
_cis_id: int,
|
||||
):
|
||||
acl_connection.abort_on(
|
||||
'disconnection', devices[1].accept_cis_request(cis_handle)
|
||||
utils.cancel_on_event(
|
||||
acl_connection, 'disconnection', devices[1].accept_cis_request(cis_handle)
|
||||
)
|
||||
peripheral_cis_futures[cis_handle] = asyncio.get_running_loop().create_future()
|
||||
|
||||
|
||||
+1
-1
@@ -522,7 +522,7 @@ async def test_sco_setup():
|
||||
|
||||
connections = await asyncio.gather(
|
||||
devices[0].connect(
|
||||
devices[1].public_address, transport=core.BT_BR_EDR_TRANSPORT
|
||||
devices[1].public_address, transport=core.PhysicalTransport.BR_EDR
|
||||
),
|
||||
devices[1].accept(devices[0].public_address),
|
||||
)
|
||||
|
||||
+3
-3
@@ -24,7 +24,7 @@ import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from bumble.controller import Controller
|
||||
from bumble.core import BT_BR_EDR_TRANSPORT, BT_LE_TRANSPORT
|
||||
from bumble.core import PhysicalTransport
|
||||
from bumble.link import LocalLink
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.host import Host
|
||||
@@ -137,7 +137,7 @@ async def test_self_classic_connection(responder_role):
|
||||
# Connect the two devices
|
||||
await asyncio.gather(
|
||||
two_devices.devices[0].connect(
|
||||
two_devices.devices[1].public_address, transport=BT_BR_EDR_TRANSPORT
|
||||
two_devices.devices[1].public_address, transport=PhysicalTransport.BR_EDR
|
||||
),
|
||||
two_devices.devices[1].accept(
|
||||
two_devices.devices[0].public_address, responder_role
|
||||
@@ -507,7 +507,7 @@ async def test_self_smp_over_classic():
|
||||
# Connect the two devices
|
||||
await asyncio.gather(
|
||||
two_devices.devices[0].connect(
|
||||
two_devices.devices[1].public_address, transport=BT_BR_EDR_TRANSPORT
|
||||
two_devices.devices[1].public_address, transport=PhysicalTransport.BR_EDR
|
||||
),
|
||||
two_devices.devices[1].accept(two_devices.devices[0].public_address),
|
||||
)
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import pyee
|
||||
|
||||
from bumble import utils
|
||||
from bumble.device import Device
|
||||
from bumble.hci import HCI_Reset_Command
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Scanner(pyee.EventEmitter):
|
||||
class Scanner(utils.EventEmitter):
|
||||
"""
|
||||
Scanner web app
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import enum
|
||||
import logging
|
||||
from typing import Dict, List
|
||||
|
||||
from bumble.core import BT_BR_EDR_TRANSPORT, CommandTimeoutError
|
||||
from bumble.core import PhysicalTransport, CommandTimeoutError
|
||||
from bumble.device import Device, DeviceConfiguration
|
||||
from bumble.pairing import PairingConfig
|
||||
from bumble.sdp import ServiceAttribute
|
||||
@@ -229,7 +229,9 @@ class Speaker:
|
||||
async def connect(self, address):
|
||||
# Connect to the source
|
||||
print(f'=== Connecting to {address}...')
|
||||
connection = await self.device.connect(address, transport=BT_BR_EDR_TRANSPORT)
|
||||
connection = await self.device.connect(
|
||||
address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
print(f'=== Connected to {connection.peer_address}')
|
||||
|
||||
# Request authentication
|
||||
|
||||
Reference in New Issue
Block a user