Make all event emitters async

* Also remove AbortableEventEmitter
This commit is contained in:
Josh Wu
2025-04-12 22:53:32 +08:00
parent 6cecc16519
commit 55801bc2ca
32 changed files with 432 additions and 395 deletions

View File

@@ -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:

View File

@@ -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,

View File

@@ -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
@@ -798,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
@@ -845,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):

View File

@@ -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

View File

@@ -37,7 +37,6 @@ from typing import (
cast,
)
from pyee import EventEmitter
from bumble.core import (
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
@@ -57,7 +56,7 @@ from bumble.a2dp import (
VendorSpecificMediaCodecInformation,
)
from bumble.rtp import MediaPacket
from bumble import sdp, device, l2cap
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

View File

@@ -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)

View File

@@ -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

View File

@@ -49,7 +49,6 @@ from typing import (
)
from typing_extensions import Self
from pyee import EventEmitter
from bumble.colors import color
from bumble.att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU
@@ -71,15 +70,7 @@ from bumble.core import (
OutOfResourcesError,
UnreachableError,
)
from bumble.utils import (
AsyncRunner,
CompositeEventEmitter,
EventWatcher,
setup_event_forwarding,
composite_listener,
deprecated,
experimental,
)
from bumble import utils
from bumble.keys import (
KeyStore,
PairingKeys,
@@ -576,7 +567,7 @@ class PeriodicAdvertisingParameters:
# -----------------------------------------------------------------------------
@dataclass
class AdvertisingSet(EventEmitter):
class AdvertisingSet(utils.EventEmitter):
device: Device
advertising_handle: int
auto_restart: bool
@@ -810,7 +801,7 @@ class AdvertisingSet(EventEmitter):
# -----------------------------------------------------------------------------
class PeriodicAdvertisingSync(EventEmitter):
class PeriodicAdvertisingSync(utils.EventEmitter):
class State(Enum):
INIT = 0
PENDING = 1
@@ -939,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:
@@ -1025,7 +1016,7 @@ class BigParameters:
# -----------------------------------------------------------------------------
@dataclass
class Big(EventEmitter):
class Big(utils.EventEmitter):
class State(IntEnum):
PENDING = 0
ACTIVE = 1
@@ -1065,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(
@@ -1088,7 +1079,7 @@ class BigSyncParameters:
# -----------------------------------------------------------------------------
@dataclass
class BigSync(EventEmitter):
class BigSync(utils.EventEmitter):
class State(IntEnum):
PENDING = 0
ACTIVE = 1
@@ -1123,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(
@@ -1392,7 +1383,7 @@ ConnectionParametersPreferences.default = ConnectionParametersPreferences()
# -----------------------------------------------------------------------------
@dataclass
class ScoLink(CompositeEventEmitter):
class ScoLink(utils.CompositeEventEmitter):
device: Device
acl_connection: Connection
handle: int
@@ -1483,7 +1474,7 @@ class _IsoLink:
# -----------------------------------------------------------------------------
@dataclass
class CisLink(CompositeEventEmitter, _IsoLink):
class CisLink(utils.EventEmitter, _IsoLink):
class State(IntEnum):
PENDING = 0
ESTABLISHED = 1
@@ -1560,7 +1551,7 @@ class IsoPacketStream:
# -----------------------------------------------------------------------------
class Connection(CompositeEventEmitter):
class Connection(utils.CompositeEventEmitter):
device: Device
handle: int
transport: core.PhysicalTransport
@@ -1580,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
@@ -1699,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,
@@ -1753,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)
@@ -2037,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: (
@@ -2069,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
@@ -2290,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:
@@ -2379,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,
@@ -2396,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,
@@ -3237,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
@@ -3667,7 +3662,7 @@ class Device(CompositeEventEmitter):
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(
@@ -3684,7 +3679,9 @@ 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:
@@ -3740,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
@@ -3802,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)
@@ -3876,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
@@ -4075,7 +4072,7 @@ 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)
@@ -4125,7 +4122,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)
@@ -4229,7 +4226,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(
@@ -4309,7 +4308,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
@@ -4353,7 +4352,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)
@@ -4402,13 +4403,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,
@@ -4466,7 +4467,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]:
@@ -4482,7 +4483,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
@@ -4509,7 +4510,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.
@@ -4531,7 +4532,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:
@@ -4555,7 +4556,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,
@@ -4569,14 +4570,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,
@@ -4619,7 +4620,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:
@@ -4629,7 +4630,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,
@@ -4677,7 +4678,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()
)
@@ -4700,7 +4701,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:
@@ -4708,7 +4709,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
)
@@ -4725,7 +4726,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,
@@ -4745,7 +4746,7 @@ class Device(CompositeEventEmitter):
check_result=True,
)
@experimental('Only for testing.')
@utils.experimental('Only for testing.')
async def create_cs_config(
self,
connection: Connection,
@@ -4782,7 +4783,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
)
@@ -4816,12 +4817,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:
@@ -4840,7 +4841,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,
@@ -4878,7 +4879,7 @@ class Device(CompositeEventEmitter):
check_result=True,
)
@experimental('Only for testing.')
@utils.experimental('Only for testing.')
async def enable_cs_procedure(
self,
connection: Connection,
@@ -4888,7 +4889,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
)
@@ -4928,7 +4929,9 @@ 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=PhysicalTransport.BR_EDR
@@ -5197,7 +5200,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)
@@ -5306,7 +5309,7 @@ class Device(CompositeEventEmitter):
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
@@ -5433,7 +5436,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
@@ -5565,7 +5568,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
@@ -5581,7 +5584,7 @@ class Device(CompositeEventEmitter):
)
)
AsyncRunner.spawn(reply())
utils.AsyncRunner.spawn(reply())
# [Classic only]
@host_event_handler
@@ -5592,8 +5595,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(
@@ -5611,7 +5614,7 @@ class Device(CompositeEventEmitter):
)
)
AsyncRunner.spawn(reply())
utils.AsyncRunner.spawn(reply())
# [Classic only]
@host_event_handler
@@ -5626,8 +5629,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:
@@ -5665,8 +5668,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]
@@ -5698,7 +5701,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:
@@ -5718,7 +5721,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:
@@ -5727,7 +5730,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:
@@ -5737,7 +5740,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,
@@ -5764,7 +5767,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
@@ -5784,7 +5787,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):
@@ -5793,7 +5796,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)

View File

@@ -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)
# -----------------------------------------------------------------------------

View File

@@ -44,7 +44,6 @@ from typing import (
TYPE_CHECKING,
)
from pyee import EventEmitter
from bumble.colors import color
from bumble.hci import HCI_Constant
@@ -69,6 +68,7 @@ from bumble.att import (
ATT_Write_Request,
ATT_Error,
)
from bumble import utils
from bumble import core
from bumble.core import UUID, InvalidStateError
from bumble.gatt import (
@@ -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

View File

@@ -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

View File

@@ -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
@@ -5370,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
@@ -6203,13 +6203,13 @@ class HCI_LE_Periodic_Advertising_Report_Event(HCI_LE_Meta_Event):
TX_POWER_INFORMATION_NOT_AVAILABLE = 0x7F
RSSI_NOT_AVAILABLE = 0x7F
class CteType(OpenIntEnum):
class CteType(utils.OpenIntEnum):
AOA_CONSTANT_TONE_EXTENSION = 0x00
AOD_CONSTANT_TONE_EXTENSION_1US = 0x01
AOD_CONSTANT_TONE_EXTENSION_2US = 0x02
NO_CONSTANT_TONE_EXTENSION = 0xFF
class DataStatus(OpenIntEnum):
class DataStatus(utils.OpenIntEnum):
DATA_COMPLETE = 0x00
DATA_INCOMPLETE_MORE_TO_COME = 0x01
DATA_INCOMPLETE_TRUNCATED_NO_MORE_TO_COME = 0x02
@@ -6590,7 +6590,7 @@ class HCI_LE_CS_Config_Complete_Event(HCI_LE_Meta_Event):
See Bluetooth spec @ 7.7.65.42 LE CS Config Complete event
'''
class Action(OpenIntEnum):
class Action(utils.OpenIntEnum):
REMOVED = 0
CREATED = 1
@@ -6642,7 +6642,7 @@ class HCI_LE_CS_Procedure_Enable_Complete_Event(HCI_LE_Meta_Event):
See Bluetooth spec @ 7.7.65.43 LE CS Procedure Enable Complete event
'''
class State(OpenIntEnum):
class State(utils.OpenIntEnum):
DISABLED = 0
ENABLED = 1
@@ -6984,7 +6984,7 @@ class HCI_QOS_Setup_Complete_Event(HCI_Event):
See Bluetooth spec @ 7.7.13 QoS Setup Complete Event
'''
class ServiceType(OpenIntEnum):
class ServiceType(utils.OpenIntEnum):
NO_TRAFFIC_AVAILABLE = 0x00
BEST_EFFORT_AVAILABLE = 0x01
GUARANTEED_AVAILABLE = 0x02

View File

@@ -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.

View File

@@ -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

View File

@@ -34,7 +34,6 @@ from typing import (
TYPE_CHECKING,
)
import pyee
from bumble.colors import color
from bumble.l2cap import L2CAP_PDU
@@ -47,7 +46,7 @@ from bumble.core import (
ConnectionPHY,
ConnectionParameters,
)
from bumble.utils import AbortableEventEmitter
from bumble import utils
from bumble.transport.common import TransportLostError
if TYPE_CHECKING:
@@ -61,7 +60,7 @@ logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
class DataPacketQueue(pyee.EventEmitter):
class DataPacketQueue(utils.EventEmitter):
"""
Flow-control queue for host->controller data packets (ACL, ISO).
@@ -234,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]
@@ -1289,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(
@@ -1447,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),

View File

@@ -23,7 +23,6 @@ import logging
import struct
from collections import deque
from pyee import EventEmitter
from typing import (
Dict,
Type,
@@ -39,7 +38,7 @@ from typing import (
TYPE_CHECKING,
)
from bumble.utils import deprecated
from bumble import utils
from bumble.colors import color
from bumble.core import (
InvalidStateError,
@@ -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,
@@ -1536,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,
@@ -1582,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,
@@ -2126,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:
@@ -2183,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)
@@ -2233,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)

View File

@@ -20,6 +20,7 @@ import grpc.aio
import logging
import struct
import bumble.utils
from bumble.pandora import utils
from bumble.pandora.config import Config
from bumble.core import (
@@ -534,7 +535,9 @@ class HostService(HostServicer):
try:
self.log.debug('Stop advertising')
await self.device.abort_on('flush', self.device.stop_advertising())
await bumble.utils.cancel_on_event(
self.device, 'flush', self.device.stop_advertising()
)
except:
pass
@@ -602,7 +605,9 @@ class HostService(HostServicer):
self.device.remove_listener('advertisement', handler) # type: ignore
try:
self.log.debug('Stop scanning')
await self.device.abort_on('flush', self.device.stop_scanning())
await bumble.utils.cancel_on_event(
self.device, 'flush', self.device.stop_scanning()
)
except:
pass
@@ -642,7 +647,9 @@ class HostService(HostServicer):
self.device.remove_listener('inquiry_result', result_handler) # type: ignore
try:
self.log.debug('Stop inquiry')
await self.device.abort_on('flush', self.device.stop_discovery())
await bumble.utils.cancel_on_event(
self.device, 'flush', self.device.stop_discovery()
)
except:
pass

View File

@@ -25,9 +25,9 @@ from bumble.core import (
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
@@ -300,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:
@@ -449,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)

View File

@@ -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
'''

View File

@@ -28,7 +28,6 @@ import logging
import struct
from typing import Optional, Sequence, Union
from pyee import EventEmitter
from bumble.att import ATT_Error
from bumble.device import Peer
@@ -42,7 +41,7 @@ from bumble.gatt import (
)
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
from bumble.gatt_adapters import SerializableCharacteristicProxyAdapter
from bumble.utils import OpenIntEnum
from bumble import utils
# -----------------------------------------------------------------------------
@@ -60,16 +59,16 @@ logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
# Protocol
# -----------------------------------------------------------------------------
class ActionId(OpenIntEnum):
class ActionId(utils.OpenIntEnum):
POSITIVE = 0
NEGATIVE = 1
class AppAttributeId(OpenIntEnum):
class AppAttributeId(utils.OpenIntEnum):
DISPLAY_NAME = 0
class CategoryId(OpenIntEnum):
class CategoryId(utils.OpenIntEnum):
OTHER = 0
INCOMING_CALL = 1
MISSED_CALL = 2
@@ -84,13 +83,13 @@ class CategoryId(OpenIntEnum):
ENTERTAINMENT = 11
class CommandId(OpenIntEnum):
class CommandId(utils.OpenIntEnum):
GET_NOTIFICATION_ATTRIBUTES = 0
GET_APP_ATTRIBUTES = 1
PERFORM_NOTIFICATION_ACTION = 2
class EventId(OpenIntEnum):
class EventId(utils.OpenIntEnum):
NOTIFICATION_ADDED = 0
NOTIFICATION_MODIFIED = 1
NOTIFICATION_REMOVED = 2
@@ -104,7 +103,7 @@ class EventFlags(enum.IntFlag):
NEGATIVE_ACTION = 1 << 4
class NotificationAttributeId(OpenIntEnum):
class NotificationAttributeId(utils.OpenIntEnum):
APP_IDENTIFIER = 0
TITLE = 1
SUBTITLE = 2
@@ -156,7 +155,7 @@ class Notification:
)
class ErrorCode(OpenIntEnum):
class ErrorCode(utils.OpenIntEnum):
UNKNOWN_COMMAND = 0xA0
INVALID_COMMAND = 0xA1
INVALID_PARAMETER = 0xA2
@@ -243,7 +242,7 @@ class AncsProxy(ProfileServiceProxy):
)
class AncsClient(EventEmitter):
class AncsClient(utils.EventEmitter):
_expected_response_command_id: Optional[CommandId]
_expected_response_notification_uid: Optional[int]
_expected_response_app_identifier: Optional[str]

View File

@@ -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
@@ -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),
)

View File

@@ -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
@@ -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]

View File

@@ -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
@@ -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),
)

View File

@@ -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.
"""

View File

@@ -25,11 +25,11 @@ 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 bumble import utils
from bumble.colors import color
from bumble.core import (
UUID,
@@ -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
@@ -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:

View File

@@ -41,7 +41,6 @@ from typing import (
cast,
)
from pyee import EventEmitter
from bumble.colors import color
from bumble.hci import (
@@ -60,6 +59,7 @@ from bumble.core import (
)
from bumble.keys import PairingKeys
from bumble import crypto
from bumble import utils
if TYPE_CHECKING:
from bumble.device import Connection, Device
@@ -899,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]
@@ -918,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:
@@ -935,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
@@ -950,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),
)
@@ -1049,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,
@@ -1172,8 +1173,8 @@ class Session:
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
@@ -1211,8 +1212,8 @@ class Session:
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:
@@ -1304,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)
@@ -1322,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:
@@ -1431,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(
@@ -1877,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:
@@ -1921,7 +1926,7 @@ class Session:
# -----------------------------------------------------------------------------
class Manager(EventEmitter):
class Manager(utils.EventEmitter):
'''
Implements the Initiator and Responder roles of the Security Manager Protocol
'''

View File

@@ -38,7 +38,8 @@ from typing import (
)
from typing_extensions import Self
from pyee import EventEmitter
import pyee
import pyee.asyncio
from bumble.colors import color
@@ -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):
"""

View File

@@ -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)

View File

@@ -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(

View File

@@ -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)

View File

@@ -49,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
@@ -251,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
@@ -482,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()

View File

@@ -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