Merge branch 'main' into update

This commit is contained in:
khsiao-google
2025-09-01 14:51:58 +08:00
29 changed files with 2427 additions and 1231 deletions

View File

@@ -104,5 +104,8 @@
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"python-envs.defaultEnvManager": "ms-python.python:system",
"python-envs.pythonProjects": []
"python-envs.pythonProjects": [],
"nrf-connect.applications": [
"${workspaceFolder}/extras/zephyr/hci_usb"
]
}

View File

@@ -39,7 +39,7 @@ import bumble.device
import bumble.logging
import bumble.transport
import bumble.utils
from bumble import company_ids, core, gatt, hci
from bumble import company_ids, core, data_types, gatt, hci
from bumble.audio import io as audio_io
from bumble.colors import color
from bumble.profiles import bap, bass, le_audio, pbp
@@ -859,20 +859,12 @@ async def run_transmit(
)
broadcast_audio_announcement = bap.BroadcastAudioAnnouncement(broadcast_id)
advertising_manufacturer_data = (
b''
if manufacturer_data is None
else bytes(
core.AdvertisingData(
[
(
core.AdvertisingData.MANUFACTURER_SPECIFIC_DATA,
struct.pack('<H', manufacturer_data[0])
+ manufacturer_data[1],
)
advertising_data_types: list[core.DataType] = [
data_types.BroadcastName(broadcast_name)
]
)
)
if manufacturer_data is not None:
advertising_data_types.append(
data_types.ManufacturerSpecificData(*manufacturer_data)
)
advertising_set = await device.create_advertising_set(
@@ -885,12 +877,7 @@ async def run_transmit(
),
advertising_data=(
broadcast_audio_announcement.get_advertising_data()
+ bytes(
core.AdvertisingData(
[(core.AdvertisingData.BROADCAST_NAME, broadcast_name.encode())]
)
)
+ advertising_manufacturer_data
+ bytes(core.AdvertisingData(advertising_data_types))
),
periodic_advertising_parameters=bumble.device.PeriodicAdvertisingParameters(
periodic_advertising_interval_min=80,

View File

@@ -37,7 +37,7 @@ import click
import bumble
import bumble.logging
from bumble import utils
from bumble import data_types, utils
from bumble.colors import color
from bumble.core import AdvertisingData
from bumble.device import AdvertisingParameters, CisLink, Device, DeviceConfiguration
@@ -330,22 +330,13 @@ class Speaker:
advertising_data = bytes(
AdvertisingData(
[
(
AdvertisingData.COMPLETE_LOCAL_NAME,
bytes(device_config.name, 'utf-8'),
data_types.CompleteLocalName(device_config.name),
data_types.Flags(
AdvertisingData.Flags.LE_GENERAL_DISCOVERABLE_MODE
| AdvertisingData.Flags.BR_EDR_NOT_SUPPORTED
),
(
AdvertisingData.FLAGS,
bytes(
[
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
| AdvertisingData.BR_EDR_NOT_SUPPORTED_FLAG
]
),
),
(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
bytes(pacs.PublishedAudioCapabilitiesService.UUID),
data_types.IncompleteListOf16BitServiceUUIDs(
[pacs.PublishedAudioCapabilitiesService.UUID]
),
]
)

View File

@@ -23,6 +23,7 @@ import struct
import click
from prompt_toolkit.shortcuts import PromptSession
from bumble import data_types
from bumble.a2dp import make_audio_sink_service_sdp_records
from bumble.att import (
ATT_INSUFFICIENT_AUTHENTICATION_ERROR,
@@ -34,6 +35,7 @@ from bumble.core import (
UUID,
AdvertisingData,
Appearance,
DataType,
PhysicalTransport,
ProtocolError,
)
@@ -506,33 +508,21 @@ async def pair(
if mode == 'dual':
flags |= AdvertisingData.Flags.SIMULTANEOUS_LE_BR_EDR_CAPABLE
ad_structs = [
(
AdvertisingData.FLAGS,
bytes([flags]),
),
(AdvertisingData.COMPLETE_LOCAL_NAME, 'Bumble'.encode()),
advertising_data_types: list[DataType] = [
data_types.Flags(flags),
data_types.CompleteLocalName('Bumble'),
]
if service_uuids_16:
ad_structs.append(
(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
b"".join(bytes(uuid) for uuid in service_uuids_16),
)
advertising_data_types.append(
data_types.IncompleteListOf16BitServiceUUIDs(service_uuids_16)
)
if service_uuids_32:
ad_structs.append(
(
AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS,
b"".join(bytes(uuid) for uuid in service_uuids_32),
)
advertising_data_types.append(
data_types.IncompleteListOf32BitServiceUUIDs(service_uuids_32)
)
if service_uuids_128:
ad_structs.append(
(
AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS,
b"".join(bytes(uuid) for uuid in service_uuids_128),
)
advertising_data_types.append(
data_types.IncompleteListOf128BitServiceUUIDs(service_uuids_128)
)
if advertise_appearance:
@@ -559,13 +549,10 @@ async def pair(
advertise_appearance_int = int(
Appearance(category_enum, subcategory_enum)
)
ad_structs.append(
(
AdvertisingData.APPEARANCE,
struct.pack('<H', advertise_appearance_int),
advertising_data_types.append(
data_types.Appearance(category_enum, subcategory_enum)
)
)
device.advertising_data = bytes(AdvertisingData(ad_structs))
device.advertising_data = bytes(AdvertisingData(advertising_data_types))
await device.start_advertising(
auto_restart=True,
own_address_type=(

View File

@@ -20,6 +20,7 @@ import asyncio
import click
import bumble.logging
from bumble import data_types
from bumble.colors import color
from bumble.device import Advertisement, Device
from bumble.hci import HCI_LE_1M_PHY, HCI_LE_CODED_PHY, Address, HCI_Constant
@@ -94,13 +95,22 @@ class AdvertisementPrinter:
else:
phy_info = ''
details = separator.join(
[
data_type.to_string(use_label=True)
for data_type in data_types.data_types_from_advertising_data(
advertisement.data
)
]
)
print(
f'>>> {color(address, address_color)} '
f'[{color(address_type_string, type_color)}]{address_qualifier}'
f'{resolution_qualifier}:{separator}'
f'{phy_info}'
f'RSSI:{advertisement.rssi:4} {rssi_bar}{separator}'
f'{advertisement.data.to_string(separator)}\n'
f'{details}\n'
)
def on_advertisement(self, advertisement):

File diff suppressed because it is too large Load Diff

View File

@@ -17,9 +17,21 @@
# -----------------------------------------------------------------------------
from __future__ import annotations
import dataclasses
import enum
import struct
from typing import Literal, Optional, Union, cast, overload
from typing import (
TYPE_CHECKING,
Any,
ClassVar,
Iterable,
Literal,
Optional,
Type,
Union,
cast,
overload,
)
from typing_extensions import Self
@@ -331,6 +343,9 @@ class UUID:
result += f' ({self.name})'
return result
def __repr__(self) -> str:
return self.to_hex_str()
# -----------------------------------------------------------------------------
# Common UUID constants
@@ -447,26 +462,25 @@ BT_HDP_SINK_SERVICE = UUID.from_16_bits(0x1402,
# -----------------------------------------------------------------------------
# DeviceClass
# ClassOfDevice
# See Bluetooth - Assigned Numbers - 2.8 Class of Device
# -----------------------------------------------------------------------------
class DeviceClass:
@dataclasses.dataclass
class ClassOfDevice:
# fmt: off
# pylint: disable=line-too-long
class MajorServiceClasses(utils.CompatibleIntFlag):
LIMITED_DISCOVERABLE_MODE = (1 << 0)
LE_AUDIO = (1 << 1)
POSITIONING = (1 << 3)
NETWORKING = (1 << 4)
RENDERING = (1 << 5)
CAPTURING = (1 << 6)
OBJECT_TRANSFER = (1 << 7)
AUDIO = (1 << 8)
TELEPHONY = (1 << 9)
INFORMATION = (1 << 10)
# Major Service Classes (flags combined with OR)
LIMITED_DISCOVERABLE_MODE_SERVICE_CLASS = (1 << 0)
LE_AUDIO_SERVICE_CLASS = (1 << 1)
RESERVED = (1 << 2)
POSITIONING_SERVICE_CLASS = (1 << 3)
NETWORKING_SERVICE_CLASS = (1 << 4)
RENDERING_SERVICE_CLASS = (1 << 5)
CAPTURING_SERVICE_CLASS = (1 << 6)
OBJECT_TRANSFER_SERVICE_CLASS = (1 << 7)
AUDIO_SERVICE_CLASS = (1 << 8)
TELEPHONY_SERVICE_CLASS = (1 << 9)
INFORMATION_SERVICE_CLASS = (1 << 10)
SERVICE_CLASS_LABELS = [
MAJOR_SERVICE_CLASS_LABELS: ClassVar[list[str]] = [
'Limited Discoverable Mode',
'LE audio',
'(reserved)',
@@ -477,220 +491,440 @@ class DeviceClass:
'Object Transfer',
'Audio',
'Telephony',
'Information'
'Information',
]
# Major Device Classes
MISCELLANEOUS_MAJOR_DEVICE_CLASS = 0x00
COMPUTER_MAJOR_DEVICE_CLASS = 0x01
PHONE_MAJOR_DEVICE_CLASS = 0x02
LAN_NETWORK_ACCESS_POINT_MAJOR_DEVICE_CLASS = 0x03
AUDIO_VIDEO_MAJOR_DEVICE_CLASS = 0x04
PERIPHERAL_MAJOR_DEVICE_CLASS = 0x05
IMAGING_MAJOR_DEVICE_CLASS = 0x06
WEARABLE_MAJOR_DEVICE_CLASS = 0x07
TOY_MAJOR_DEVICE_CLASS = 0x08
HEALTH_MAJOR_DEVICE_CLASS = 0x09
UNCATEGORIZED_MAJOR_DEVICE_CLASS = 0x1F
class MajorDeviceClass(utils.OpenIntEnum):
MISCELLANEOUS = 0x00
COMPUTER = 0x01
PHONE = 0x02
LAN_NETWORK_ACCESS_POINT = 0x03
AUDIO_VIDEO = 0x04
PERIPHERAL = 0x05
IMAGING = 0x06
WEARABLE = 0x07
TOY = 0x08
HEALTH = 0x09
UNCATEGORIZED = 0x1F
MAJOR_DEVICE_CLASS_NAMES = {
MISCELLANEOUS_MAJOR_DEVICE_CLASS: 'Miscellaneous',
COMPUTER_MAJOR_DEVICE_CLASS: 'Computer',
PHONE_MAJOR_DEVICE_CLASS: 'Phone',
LAN_NETWORK_ACCESS_POINT_MAJOR_DEVICE_CLASS: 'LAN/Network Access Point',
AUDIO_VIDEO_MAJOR_DEVICE_CLASS: 'Audio/Video',
PERIPHERAL_MAJOR_DEVICE_CLASS: 'Peripheral',
IMAGING_MAJOR_DEVICE_CLASS: 'Imaging',
WEARABLE_MAJOR_DEVICE_CLASS: 'Wearable',
TOY_MAJOR_DEVICE_CLASS: 'Toy',
HEALTH_MAJOR_DEVICE_CLASS: 'Health',
UNCATEGORIZED_MAJOR_DEVICE_CLASS: 'Uncategorized'
MAJOR_DEVICE_CLASS_LABELS: ClassVar[dict[MajorDeviceClass, str]] = {
MajorDeviceClass.MISCELLANEOUS: 'Miscellaneous',
MajorDeviceClass.COMPUTER: 'Computer',
MajorDeviceClass.PHONE: 'Phone',
MajorDeviceClass.LAN_NETWORK_ACCESS_POINT: 'LAN/Network Access Point',
MajorDeviceClass.AUDIO_VIDEO: 'Audio/Video',
MajorDeviceClass.PERIPHERAL: 'Peripheral',
MajorDeviceClass.IMAGING: 'Imaging',
MajorDeviceClass.WEARABLE: 'Wearable',
MajorDeviceClass.TOY: 'Toy',
MajorDeviceClass.HEALTH: 'Health',
MajorDeviceClass.UNCATEGORIZED: 'Uncategorized',
}
COMPUTER_UNCATEGORIZED_MINOR_DEVICE_CLASS = 0x00
COMPUTER_DESKTOP_WORKSTATION_MINOR_DEVICE_CLASS = 0x01
COMPUTER_SERVER_CLASS_COMPUTER_MINOR_DEVICE_CLASS = 0x02
COMPUTER_LAPTOP_COMPUTER_MINOR_DEVICE_CLASS = 0x03
COMPUTER_HANDHELD_PC_PDA_MINOR_DEVICE_CLASS = 0x04
COMPUTER_PALM_SIZE_PC_PDA_MINOR_DEVICE_CLASS = 0x05
COMPUTER_WEARABLE_COMPUTER_MINOR_DEVICE_CLASS = 0x06
COMPUTER_TABLET_MINOR_DEVICE_CLASS = 0x07
class ComputerMinorDeviceClass(utils.OpenIntEnum):
UNCATEGORIZED = 0x00
DESKTOP_WORKSTATION = 0x01
SERVER_CLASS_COMPUTER = 0x02
LAPTOP_COMPUTER = 0x03
HANDHELD_PC_PDA = 0x04
PALM_SIZE_PC_PDA = 0x05
WEARABLE_COMPUTER = 0x06
TABLET = 0x07
COMPUTER_MINOR_DEVICE_CLASS_NAMES = {
COMPUTER_UNCATEGORIZED_MINOR_DEVICE_CLASS: 'Uncategorized',
COMPUTER_DESKTOP_WORKSTATION_MINOR_DEVICE_CLASS: 'Desktop workstation',
COMPUTER_SERVER_CLASS_COMPUTER_MINOR_DEVICE_CLASS: 'Server-class computer',
COMPUTER_LAPTOP_COMPUTER_MINOR_DEVICE_CLASS: 'Laptop',
COMPUTER_HANDHELD_PC_PDA_MINOR_DEVICE_CLASS: 'Handheld PC/PDA',
COMPUTER_PALM_SIZE_PC_PDA_MINOR_DEVICE_CLASS: 'Palm-size PC/PDA',
COMPUTER_WEARABLE_COMPUTER_MINOR_DEVICE_CLASS: 'Wearable computer',
COMPUTER_TABLET_MINOR_DEVICE_CLASS: 'Tablet'
COMPUTER_MINOR_DEVICE_CLASS_LABELS: ClassVar[dict[ComputerMinorDeviceClass, str]] = {
ComputerMinorDeviceClass.UNCATEGORIZED: 'Uncategorized',
ComputerMinorDeviceClass.DESKTOP_WORKSTATION: 'Desktop workstation',
ComputerMinorDeviceClass.SERVER_CLASS_COMPUTER: 'Server-class computer',
ComputerMinorDeviceClass.LAPTOP_COMPUTER: 'Laptop',
ComputerMinorDeviceClass.HANDHELD_PC_PDA: 'Handheld PC/PDA',
ComputerMinorDeviceClass.PALM_SIZE_PC_PDA: 'Palm-size PC/PDA',
ComputerMinorDeviceClass.WEARABLE_COMPUTER: 'Wearable computer',
ComputerMinorDeviceClass.TABLET: 'Tablet',
}
PHONE_UNCATEGORIZED_MINOR_DEVICE_CLASS = 0x00
PHONE_CELLULAR_MINOR_DEVICE_CLASS = 0x01
PHONE_CORDLESS_MINOR_DEVICE_CLASS = 0x02
PHONE_SMARTPHONE_MINOR_DEVICE_CLASS = 0x03
PHONE_WIRED_MODEM_OR_VOICE_GATEWAY_MINOR_DEVICE_CLASS = 0x04
PHONE_COMMON_ISDN_MINOR_DEVICE_CLASS = 0x05
class PhoneMinorDeviceClass(utils.OpenIntEnum):
UNCATEGORIZED = 0x00
CELLULAR = 0x01
CORDLESS = 0x02
SMARTPHONE = 0x03
WIRED_MODEM_OR_VOICE_GATEWAY = 0x04
COMMON_ISDN = 0x05
PHONE_MINOR_DEVICE_CLASS_NAMES = {
PHONE_UNCATEGORIZED_MINOR_DEVICE_CLASS: 'Uncategorized',
PHONE_CELLULAR_MINOR_DEVICE_CLASS: 'Cellular',
PHONE_CORDLESS_MINOR_DEVICE_CLASS: 'Cordless',
PHONE_SMARTPHONE_MINOR_DEVICE_CLASS: 'Smartphone',
PHONE_WIRED_MODEM_OR_VOICE_GATEWAY_MINOR_DEVICE_CLASS: 'Wired modem or voice gateway',
PHONE_COMMON_ISDN_MINOR_DEVICE_CLASS: 'Common ISDN access'
PHONE_MINOR_DEVICE_CLASS_LABELS: ClassVar[dict[PhoneMinorDeviceClass, str]] = {
PhoneMinorDeviceClass.UNCATEGORIZED: 'Uncategorized',
PhoneMinorDeviceClass.CELLULAR: 'Cellular',
PhoneMinorDeviceClass.CORDLESS: 'Cordless',
PhoneMinorDeviceClass.SMARTPHONE: 'Smartphone',
PhoneMinorDeviceClass.WIRED_MODEM_OR_VOICE_GATEWAY: 'Wired modem or voice gateway',
PhoneMinorDeviceClass.COMMON_ISDN: 'Common ISDN access',
}
AUDIO_VIDEO_UNCATEGORIZED_MINOR_DEVICE_CLASS = 0x00
AUDIO_VIDEO_WEARABLE_HEADSET_DEVICE_MINOR_DEVICE_CLASS = 0x01
AUDIO_VIDEO_HANDS_FREE_DEVICE_MINOR_DEVICE_CLASS = 0x02
class LanNetworkMinorDeviceClass(utils.OpenIntEnum):
FULLY_AVAILABLE = 0x00
_1_TO_17_PERCENT_UTILIZED = 0x01
_17_TO_33_PERCENT_UTILIZED = 0x02
_33_TO_50_PERCENT_UTILIZED = 0x03
_50_TO_67_PERCENT_UTILIZED = 0x04
_67_TO_83_PERCENT_UTILIZED = 0x05
_83_TO_99_PERCENT_UTILIZED = 0x06
_NO_SERVICE_AVAILABLE = 0x07
LAN_NETWORK_MINOR_DEVICE_CLASS_LABELS: ClassVar[dict[LanNetworkMinorDeviceClass, str]] = {
LanNetworkMinorDeviceClass.FULLY_AVAILABLE: 'Fully availbable',
LanNetworkMinorDeviceClass._1_TO_17_PERCENT_UTILIZED: '1% to 17% utilized',
LanNetworkMinorDeviceClass._17_TO_33_PERCENT_UTILIZED: '17% to 33% utilized',
LanNetworkMinorDeviceClass._33_TO_50_PERCENT_UTILIZED: '33% to 50% utilized',
LanNetworkMinorDeviceClass._50_TO_67_PERCENT_UTILIZED: '50% to 67% utilized',
LanNetworkMinorDeviceClass._67_TO_83_PERCENT_UTILIZED: '67% to 83% utilized',
LanNetworkMinorDeviceClass._83_TO_99_PERCENT_UTILIZED: '83% to 99% utilized',
LanNetworkMinorDeviceClass._NO_SERVICE_AVAILABLE: 'No service available',
}
class AudioVideoMinorDeviceClass(utils.OpenIntEnum):
UNCATEGORIZED = 0x00
WEARABLE_HEADSET_DEVICE = 0x01
HANDS_FREE_DEVICE = 0x02
# (RESERVED) = 0x03
AUDIO_VIDEO_MICROPHONE_MINOR_DEVICE_CLASS = 0x04
AUDIO_VIDEO_LOUDSPEAKER_MINOR_DEVICE_CLASS = 0x05
AUDIO_VIDEO_HEADPHONES_MINOR_DEVICE_CLASS = 0x06
AUDIO_VIDEO_PORTABLE_AUDIO_MINOR_DEVICE_CLASS = 0x07
AUDIO_VIDEO_CAR_AUDIO_MINOR_DEVICE_CLASS = 0x08
AUDIO_VIDEO_SET_TOP_BOX_MINOR_DEVICE_CLASS = 0x09
AUDIO_VIDEO_HIFI_AUDIO_DEVICE_MINOR_DEVICE_CLASS = 0x0A
AUDIO_VIDEO_VCR_MINOR_DEVICE_CLASS = 0x0B
AUDIO_VIDEO_VIDEO_CAMERA_MINOR_DEVICE_CLASS = 0x0C
AUDIO_VIDEO_CAMCORDER_MINOR_DEVICE_CLASS = 0x0D
AUDIO_VIDEO_VIDEO_MONITOR_MINOR_DEVICE_CLASS = 0x0E
AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER_MINOR_DEVICE_CLASS = 0x0F
AUDIO_VIDEO_VIDEO_CONFERENCING_MINOR_DEVICE_CLASS = 0x10
MICROPHONE = 0x04
LOUDSPEAKER = 0x05
HEADPHONES = 0x06
PORTABLE_AUDIO = 0x07
CAR_AUDIO = 0x08
SET_TOP_BOX = 0x09
HIFI_AUDIO_DEVICE = 0x0A
VCR = 0x0B
VIDEO_CAMERA = 0x0C
CAMCORDER = 0x0D
VIDEO_MONITOR = 0x0E
VIDEO_DISPLAY_AND_LOUDSPEAKER = 0x0F
VIDEO_CONFERENCING = 0x10
# (RESERVED) = 0x11
AUDIO_VIDEO_GAMING_OR_TOY_MINOR_DEVICE_CLASS = 0x12
GAMING_OR_TOY = 0x12
AUDIO_VIDEO_MINOR_DEVICE_CLASS_NAMES = {
AUDIO_VIDEO_UNCATEGORIZED_MINOR_DEVICE_CLASS: 'Uncategorized',
AUDIO_VIDEO_WEARABLE_HEADSET_DEVICE_MINOR_DEVICE_CLASS: 'Wearable Headset Device',
AUDIO_VIDEO_HANDS_FREE_DEVICE_MINOR_DEVICE_CLASS: 'Hands-free Device',
AUDIO_VIDEO_MICROPHONE_MINOR_DEVICE_CLASS: 'Microphone',
AUDIO_VIDEO_LOUDSPEAKER_MINOR_DEVICE_CLASS: 'Loudspeaker',
AUDIO_VIDEO_HEADPHONES_MINOR_DEVICE_CLASS: 'Headphones',
AUDIO_VIDEO_PORTABLE_AUDIO_MINOR_DEVICE_CLASS: 'Portable Audio',
AUDIO_VIDEO_CAR_AUDIO_MINOR_DEVICE_CLASS: 'Car audio',
AUDIO_VIDEO_SET_TOP_BOX_MINOR_DEVICE_CLASS: 'Set-top box',
AUDIO_VIDEO_HIFI_AUDIO_DEVICE_MINOR_DEVICE_CLASS: 'HiFi Audio Device',
AUDIO_VIDEO_VCR_MINOR_DEVICE_CLASS: 'VCR',
AUDIO_VIDEO_VIDEO_CAMERA_MINOR_DEVICE_CLASS: 'Video Camera',
AUDIO_VIDEO_CAMCORDER_MINOR_DEVICE_CLASS: 'Camcorder',
AUDIO_VIDEO_VIDEO_MONITOR_MINOR_DEVICE_CLASS: 'Video Monitor',
AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER_MINOR_DEVICE_CLASS: 'Video Display and Loudspeaker',
AUDIO_VIDEO_VIDEO_CONFERENCING_MINOR_DEVICE_CLASS: 'Video Conferencing',
AUDIO_VIDEO_GAMING_OR_TOY_MINOR_DEVICE_CLASS: 'Gaming/Toy'
AUDIO_VIDEO_MINOR_DEVICE_CLASS_LABELS: ClassVar[dict[AudioVideoMinorDeviceClass, str]] = {
AudioVideoMinorDeviceClass.UNCATEGORIZED: 'Uncategorized',
AudioVideoMinorDeviceClass.WEARABLE_HEADSET_DEVICE: 'Wearable Headset Device',
AudioVideoMinorDeviceClass.HANDS_FREE_DEVICE: 'Hands-free Device',
AudioVideoMinorDeviceClass.MICROPHONE: 'Microphone',
AudioVideoMinorDeviceClass.LOUDSPEAKER: 'Loudspeaker',
AudioVideoMinorDeviceClass.HEADPHONES: 'Headphones',
AudioVideoMinorDeviceClass.PORTABLE_AUDIO: 'Portable Audio',
AudioVideoMinorDeviceClass.CAR_AUDIO: 'Car audio',
AudioVideoMinorDeviceClass.SET_TOP_BOX: 'Set-top box',
AudioVideoMinorDeviceClass.HIFI_AUDIO_DEVICE: 'HiFi Audio Device',
AudioVideoMinorDeviceClass.VCR: 'VCR',
AudioVideoMinorDeviceClass.VIDEO_CAMERA: 'Video Camera',
AudioVideoMinorDeviceClass.CAMCORDER: 'Camcorder',
AudioVideoMinorDeviceClass.VIDEO_MONITOR: 'Video Monitor',
AudioVideoMinorDeviceClass.VIDEO_DISPLAY_AND_LOUDSPEAKER: 'Video Display and Loudspeaker',
AudioVideoMinorDeviceClass.VIDEO_CONFERENCING: 'Video Conferencing',
AudioVideoMinorDeviceClass.GAMING_OR_TOY: 'Gaming/Toy',
}
PERIPHERAL_UNCATEGORIZED_MINOR_DEVICE_CLASS = 0x00
PERIPHERAL_KEYBOARD_MINOR_DEVICE_CLASS = 0x10
PERIPHERAL_POINTING_DEVICE_MINOR_DEVICE_CLASS = 0x20
PERIPHERAL_COMBO_KEYBOARD_POINTING_DEVICE_MINOR_DEVICE_CLASS = 0x30
PERIPHERAL_JOYSTICK_MINOR_DEVICE_CLASS = 0x01
PERIPHERAL_GAMEPAD_MINOR_DEVICE_CLASS = 0x02
PERIPHERAL_REMOTE_CONTROL_MINOR_DEVICE_CLASS = 0x03
PERIPHERAL_SENSING_DEVICE_MINOR_DEVICE_CLASS = 0x04
PERIPHERAL_DIGITIZER_TABLET_MINOR_DEVICE_CLASS = 0x05
PERIPHERAL_CARD_READER_MINOR_DEVICE_CLASS = 0x06
PERIPHERAL_DIGITAL_PEN_MINOR_DEVICE_CLASS = 0x07
PERIPHERAL_HANDHELD_SCANNER_MINOR_DEVICE_CLASS = 0x08
PERIPHERAL_HANDHELD_GESTURAL_INPUT_DEVICE_MINOR_DEVICE_CLASS = 0x09
class PeripheralMinorDeviceClass(utils.OpenIntEnum):
UNCATEGORIZED = 0x00
KEYBOARD = 0x10
POINTING_DEVICE = 0x20
COMBO_KEYBOARD_POINTING_DEVICE = 0x30
JOYSTICK = 0x01
GAMEPAD = 0x02
REMOTE_CONTROL = 0x03
SENSING_DEVICE = 0x04
DIGITIZER_TABLET = 0x05
CARD_READER = 0x06
DIGITAL_PEN = 0x07
HANDHELD_SCANNER = 0x08
HANDHELD_GESTURAL_INPUT_DEVICE = 0x09
PERIPHERAL_MINOR_DEVICE_CLASS_NAMES = {
PERIPHERAL_UNCATEGORIZED_MINOR_DEVICE_CLASS: 'Uncategorized',
PERIPHERAL_KEYBOARD_MINOR_DEVICE_CLASS: 'Keyboard',
PERIPHERAL_POINTING_DEVICE_MINOR_DEVICE_CLASS: 'Pointing device',
PERIPHERAL_COMBO_KEYBOARD_POINTING_DEVICE_MINOR_DEVICE_CLASS: 'Combo keyboard/pointing device',
PERIPHERAL_JOYSTICK_MINOR_DEVICE_CLASS: 'Joystick',
PERIPHERAL_GAMEPAD_MINOR_DEVICE_CLASS: 'Gamepad',
PERIPHERAL_REMOTE_CONTROL_MINOR_DEVICE_CLASS: 'Remote control',
PERIPHERAL_SENSING_DEVICE_MINOR_DEVICE_CLASS: 'Sensing device',
PERIPHERAL_DIGITIZER_TABLET_MINOR_DEVICE_CLASS: 'Digitizer tablet',
PERIPHERAL_CARD_READER_MINOR_DEVICE_CLASS: 'Card Reader',
PERIPHERAL_DIGITAL_PEN_MINOR_DEVICE_CLASS: 'Digital Pen',
PERIPHERAL_HANDHELD_SCANNER_MINOR_DEVICE_CLASS: 'Handheld scanner',
PERIPHERAL_HANDHELD_GESTURAL_INPUT_DEVICE_MINOR_DEVICE_CLASS: 'Handheld gestural input device'
PERIPHERAL_MINOR_DEVICE_CLASS_LABELS: ClassVar[dict[PeripheralMinorDeviceClass, str]] = {
PeripheralMinorDeviceClass.UNCATEGORIZED: 'Uncategorized',
PeripheralMinorDeviceClass.KEYBOARD: 'Keyboard',
PeripheralMinorDeviceClass.POINTING_DEVICE: 'Pointing device',
PeripheralMinorDeviceClass.COMBO_KEYBOARD_POINTING_DEVICE: 'Combo keyboard/pointing device',
PeripheralMinorDeviceClass.JOYSTICK: 'Joystick',
PeripheralMinorDeviceClass.GAMEPAD: 'Gamepad',
PeripheralMinorDeviceClass.REMOTE_CONTROL: 'Remote control',
PeripheralMinorDeviceClass.SENSING_DEVICE: 'Sensing device',
PeripheralMinorDeviceClass.DIGITIZER_TABLET: 'Digitizer tablet',
PeripheralMinorDeviceClass.CARD_READER: 'Card Reader',
PeripheralMinorDeviceClass.DIGITAL_PEN: 'Digital Pen',
PeripheralMinorDeviceClass.HANDHELD_SCANNER: 'Handheld scanner',
PeripheralMinorDeviceClass.HANDHELD_GESTURAL_INPUT_DEVICE: 'Handheld gestural input device',
}
WEARABLE_UNCATEGORIZED_MINOR_DEVICE_CLASS = 0x00
WEARABLE_WRISTWATCH_MINOR_DEVICE_CLASS = 0x01
WEARABLE_PAGER_MINOR_DEVICE_CLASS = 0x02
WEARABLE_JACKET_MINOR_DEVICE_CLASS = 0x03
WEARABLE_HELMET_MINOR_DEVICE_CLASS = 0x04
WEARABLE_GLASSES_MINOR_DEVICE_CLASS = 0x05
class WearableMinorDeviceClass(utils.OpenIntEnum):
UNCATEGORIZED = 0x00
WRISTWATCH = 0x01
PAGER = 0x02
JACKET = 0x03
HELMET = 0x04
GLASSES = 0x05
WEARABLE_MINOR_DEVICE_CLASS_NAMES = {
WEARABLE_UNCATEGORIZED_MINOR_DEVICE_CLASS: 'Uncategorized',
WEARABLE_WRISTWATCH_MINOR_DEVICE_CLASS: 'Wristwatch',
WEARABLE_PAGER_MINOR_DEVICE_CLASS: 'Pager',
WEARABLE_JACKET_MINOR_DEVICE_CLASS: 'Jacket',
WEARABLE_HELMET_MINOR_DEVICE_CLASS: 'Helmet',
WEARABLE_GLASSES_MINOR_DEVICE_CLASS: 'Glasses',
WEARABLE_MINOR_DEVICE_CLASS_LABELS: ClassVar[dict[WearableMinorDeviceClass, str]] = {
WearableMinorDeviceClass.UNCATEGORIZED: 'Uncategorized',
WearableMinorDeviceClass.WRISTWATCH: 'Wristwatch',
WearableMinorDeviceClass.PAGER: 'Pager',
WearableMinorDeviceClass.JACKET: 'Jacket',
WearableMinorDeviceClass.HELMET: 'Helmet',
WearableMinorDeviceClass.GLASSES: 'Glasses',
}
TOY_UNCATEGORIZED_MINOR_DEVICE_CLASS = 0x00
TOY_ROBOT_MINOR_DEVICE_CLASS = 0x01
TOY_VEHICLE_MINOR_DEVICE_CLASS = 0x02
TOY_DOLL_ACTION_FIGURE_MINOR_DEVICE_CLASS = 0x03
TOY_CONTROLLER_MINOR_DEVICE_CLASS = 0x04
TOY_GAME_MINOR_DEVICE_CLASS = 0x05
class ToyMinorDeviceClass(utils.OpenIntEnum):
UNCATEGORIZED = 0x00
ROBOT = 0x01
VEHICLE = 0x02
DOLL_ACTION_FIGURE = 0x03
CONTROLLER = 0x04
GAME = 0x05
TOY_MINOR_DEVICE_CLASS_NAMES = {
TOY_UNCATEGORIZED_MINOR_DEVICE_CLASS: 'Uncategorized',
TOY_ROBOT_MINOR_DEVICE_CLASS: 'Robot',
TOY_VEHICLE_MINOR_DEVICE_CLASS: 'Vehicle',
TOY_DOLL_ACTION_FIGURE_MINOR_DEVICE_CLASS: 'Doll/Action figure',
TOY_CONTROLLER_MINOR_DEVICE_CLASS: 'Controller',
TOY_GAME_MINOR_DEVICE_CLASS: 'Game',
TOY_MINOR_DEVICE_CLASS_LABELS: ClassVar[dict[ToyMinorDeviceClass, str]] = {
ToyMinorDeviceClass.UNCATEGORIZED: 'Uncategorized',
ToyMinorDeviceClass.ROBOT: 'Robot',
ToyMinorDeviceClass.VEHICLE: 'Vehicle',
ToyMinorDeviceClass.DOLL_ACTION_FIGURE: 'Doll/Action figure',
ToyMinorDeviceClass.CONTROLLER: 'Controller',
ToyMinorDeviceClass.GAME: 'Game',
}
HEALTH_UNDEFINED_MINOR_DEVICE_CLASS = 0x00
HEALTH_BLOOD_PRESSURE_MONITOR_MINOR_DEVICE_CLASS = 0x01
HEALTH_THERMOMETER_MINOR_DEVICE_CLASS = 0x02
HEALTH_WEIGHING_SCALE_MINOR_DEVICE_CLASS = 0x03
HEALTH_GLUCOSE_METER_MINOR_DEVICE_CLASS = 0x04
HEALTH_PULSE_OXIMETER_MINOR_DEVICE_CLASS = 0x05
HEALTH_HEART_PULSE_RATE_MONITOR_MINOR_DEVICE_CLASS = 0x06
HEALTH_HEALTH_DATA_DISPLAY_MINOR_DEVICE_CLASS = 0x07
HEALTH_STEP_COUNTER_MINOR_DEVICE_CLASS = 0x08
HEALTH_BODY_COMPOSITION_ANALYZER_MINOR_DEVICE_CLASS = 0x09
HEALTH_PEAK_FLOW_MONITOR_MINOR_DEVICE_CLASS = 0x0A
HEALTH_MEDICATION_MONITOR_MINOR_DEVICE_CLASS = 0x0B
HEALTH_KNEE_PROSTHESIS_MINOR_DEVICE_CLASS = 0x0C
HEALTH_ANKLE_PROSTHESIS_MINOR_DEVICE_CLASS = 0x0D
HEALTH_GENERIC_HEALTH_MANAGER_MINOR_DEVICE_CLASS = 0x0E
HEALTH_PERSONAL_MOBILITY_DEVICE_MINOR_DEVICE_CLASS = 0x0F
class HealthMinorDeviceClass(utils.OpenIntEnum):
UNDEFINED = 0x00
BLOOD_PRESSURE_MONITOR = 0x01
THERMOMETER = 0x02
WEIGHING_SCALE = 0x03
GLUCOSE_METER = 0x04
PULSE_OXIMETER = 0x05
HEART_PULSE_RATE_MONITOR = 0x06
HEALTH_DATA_DISPLAY = 0x07
STEP_COUNTER = 0x08
BODY_COMPOSITION_ANALYZER = 0x09
PEAK_FLOW_MONITOR = 0x0A
MEDICATION_MONITOR = 0x0B
KNEE_PROSTHESIS = 0x0C
ANKLE_PROSTHESIS = 0x0D
GENERIC_HEALTH_MANAGER = 0x0E
PERSONAL_MOBILITY_DEVICE = 0x0F
HEALTH_MINOR_DEVICE_CLASS_NAMES = {
HEALTH_UNDEFINED_MINOR_DEVICE_CLASS: 'Undefined',
HEALTH_BLOOD_PRESSURE_MONITOR_MINOR_DEVICE_CLASS: 'Blood Pressure Monitor',
HEALTH_THERMOMETER_MINOR_DEVICE_CLASS: 'Thermometer',
HEALTH_WEIGHING_SCALE_MINOR_DEVICE_CLASS: 'Weighing Scale',
HEALTH_GLUCOSE_METER_MINOR_DEVICE_CLASS: 'Glucose Meter',
HEALTH_PULSE_OXIMETER_MINOR_DEVICE_CLASS: 'Pulse Oximeter',
HEALTH_HEART_PULSE_RATE_MONITOR_MINOR_DEVICE_CLASS: 'Heart/Pulse Rate Monitor',
HEALTH_HEALTH_DATA_DISPLAY_MINOR_DEVICE_CLASS: 'Health Data Display',
HEALTH_STEP_COUNTER_MINOR_DEVICE_CLASS: 'Step Counter',
HEALTH_BODY_COMPOSITION_ANALYZER_MINOR_DEVICE_CLASS: 'Body Composition Analyzer',
HEALTH_PEAK_FLOW_MONITOR_MINOR_DEVICE_CLASS: 'Peak Flow Monitor',
HEALTH_MEDICATION_MONITOR_MINOR_DEVICE_CLASS: 'Medication Monitor',
HEALTH_KNEE_PROSTHESIS_MINOR_DEVICE_CLASS: 'Knee Prosthesis',
HEALTH_ANKLE_PROSTHESIS_MINOR_DEVICE_CLASS: 'Ankle Prosthesis',
HEALTH_GENERIC_HEALTH_MANAGER_MINOR_DEVICE_CLASS: 'Generic Health Manager',
HEALTH_PERSONAL_MOBILITY_DEVICE_MINOR_DEVICE_CLASS: 'Personal Mobility Device',
HEALTH_MINOR_DEVICE_CLASS_LABELS: ClassVar[dict[HealthMinorDeviceClass, str]] = {
HealthMinorDeviceClass.UNDEFINED: 'Undefined',
HealthMinorDeviceClass.BLOOD_PRESSURE_MONITOR: 'Blood Pressure Monitor',
HealthMinorDeviceClass.THERMOMETER: 'Thermometer',
HealthMinorDeviceClass.WEIGHING_SCALE: 'Weighing Scale',
HealthMinorDeviceClass.GLUCOSE_METER: 'Glucose Meter',
HealthMinorDeviceClass.PULSE_OXIMETER: 'Pulse Oximeter',
HealthMinorDeviceClass.HEART_PULSE_RATE_MONITOR: 'Heart/Pulse Rate Monitor',
HealthMinorDeviceClass.HEALTH_DATA_DISPLAY: 'Health Data Display',
HealthMinorDeviceClass.STEP_COUNTER: 'Step Counter',
HealthMinorDeviceClass.BODY_COMPOSITION_ANALYZER: 'Body Composition Analyzer',
HealthMinorDeviceClass.PEAK_FLOW_MONITOR: 'Peak Flow Monitor',
HealthMinorDeviceClass.MEDICATION_MONITOR: 'Medication Monitor',
HealthMinorDeviceClass.KNEE_PROSTHESIS: 'Knee Prosthesis',
HealthMinorDeviceClass.ANKLE_PROSTHESIS: 'Ankle Prosthesis',
HealthMinorDeviceClass.GENERIC_HEALTH_MANAGER: 'Generic Health Manager',
HealthMinorDeviceClass.PERSONAL_MOBILITY_DEVICE: 'Personal Mobility Device',
}
MINOR_DEVICE_CLASS_NAMES = {
COMPUTER_MAJOR_DEVICE_CLASS: COMPUTER_MINOR_DEVICE_CLASS_NAMES,
PHONE_MAJOR_DEVICE_CLASS: PHONE_MINOR_DEVICE_CLASS_NAMES,
AUDIO_VIDEO_MAJOR_DEVICE_CLASS: AUDIO_VIDEO_MINOR_DEVICE_CLASS_NAMES,
PERIPHERAL_MAJOR_DEVICE_CLASS: PERIPHERAL_MINOR_DEVICE_CLASS_NAMES,
WEARABLE_MAJOR_DEVICE_CLASS: WEARABLE_MINOR_DEVICE_CLASS_NAMES,
TOY_MAJOR_DEVICE_CLASS: TOY_MINOR_DEVICE_CLASS_NAMES,
HEALTH_MAJOR_DEVICE_CLASS: HEALTH_MINOR_DEVICE_CLASS_NAMES,
MINOR_DEVICE_CLASS_LABELS: ClassVar[dict[MajorDeviceClass, dict[Any, str]]] = {
MajorDeviceClass.COMPUTER: COMPUTER_MINOR_DEVICE_CLASS_LABELS,
MajorDeviceClass.PHONE: PHONE_MINOR_DEVICE_CLASS_LABELS,
MajorDeviceClass.LAN_NETWORK_ACCESS_POINT: LAN_NETWORK_MINOR_DEVICE_CLASS_LABELS,
MajorDeviceClass.AUDIO_VIDEO: AUDIO_VIDEO_MINOR_DEVICE_CLASS_LABELS,
MajorDeviceClass.PERIPHERAL: PERIPHERAL_MINOR_DEVICE_CLASS_LABELS,
MajorDeviceClass.WEARABLE: WEARABLE_MINOR_DEVICE_CLASS_LABELS,
MajorDeviceClass.TOY: TOY_MINOR_DEVICE_CLASS_LABELS,
MajorDeviceClass.HEALTH: HEALTH_MINOR_DEVICE_CLASS_LABELS,
}
_MINOR_DEVICE_CLASSES: ClassVar[dict[MajorDeviceClass, Type]] = {
MajorDeviceClass.COMPUTER: ComputerMinorDeviceClass,
MajorDeviceClass.PHONE: PhoneMinorDeviceClass,
MajorDeviceClass.LAN_NETWORK_ACCESS_POINT: LanNetworkMinorDeviceClass,
MajorDeviceClass.AUDIO_VIDEO: AudioVideoMinorDeviceClass,
MajorDeviceClass.PERIPHERAL: PeripheralMinorDeviceClass,
MajorDeviceClass.WEARABLE: WearableMinorDeviceClass,
MajorDeviceClass.TOY: ToyMinorDeviceClass,
MajorDeviceClass.HEALTH: HealthMinorDeviceClass,
}
# fmt: on
major_service_classes: MajorServiceClasses
major_device_class: MajorDeviceClass
minor_device_class: Union[
ComputerMinorDeviceClass,
PhoneMinorDeviceClass,
LanNetworkMinorDeviceClass,
AudioVideoMinorDeviceClass,
PeripheralMinorDeviceClass,
WearableMinorDeviceClass,
ToyMinorDeviceClass,
HealthMinorDeviceClass,
int,
]
@classmethod
def from_int(cls, class_of_device: int) -> Self:
major_service_classes = cls.MajorServiceClasses(class_of_device >> 13 & 0x7FF)
major_device_class = cls.MajorDeviceClass(class_of_device >> 8 & 0x1F)
minor_device_class_int = class_of_device >> 2 & 0x3F
if minor_device_class_object := cls._MINOR_DEVICE_CLASSES.get(
major_device_class
):
minor_device_class = minor_device_class_object(minor_device_class_int)
else:
minor_device_class = minor_device_class_int
return cls(major_service_classes, major_device_class, minor_device_class)
def __int__(self) -> int:
return (
self.major_service_classes << 13
| self.major_device_class << 8
| self.minor_device_class << 2
)
def __str__(self) -> str:
minor_device_class_name = (
self.minor_device_class.name
if hasattr(self.minor_device_class, 'name')
else hex(self.minor_device_class)
)
return (
f"ClassOfDevice({self.major_service_classes.composite_name},"
f"{self.major_device_class.name}/{minor_device_class_name})"
)
def major_service_classes_labels(self) -> str:
return "|".join(
bit_flags_to_strings(
self.major_service_classes, self.MAJOR_SERVICE_CLASS_LABELS
)
)
def major_device_class_label(self) -> str:
return name_or_number(
cast(dict[int, str], self.MAJOR_DEVICE_CLASS_LABELS),
self.major_device_class,
)
def minor_device_class_label(self) -> str:
class_names = self.MINOR_DEVICE_CLASS_LABELS.get(self.major_device_class)
if class_names is None:
return f'#{self.minor_device_class:02X}'
return name_or_number(class_names, self.minor_device_class)
# -----------------------------------------------------------------------------
# DeviceClass
# -----------------------------------------------------------------------------
class DeviceClass:
"""Legacy only. Use ClassOfDevice instead"""
# fmt: off
# pylint: disable=line-too-long
# Major Service Classes (flags combined with OR)
LIMITED_DISCOVERABLE_MODE_SERVICE_CLASS = ClassOfDevice.MajorServiceClasses.LIMITED_DISCOVERABLE_MODE
LE_AUDIO_SERVICE_CLASS = ClassOfDevice.MajorServiceClasses.LE_AUDIO
POSITIONING_SERVICE_CLASS = ClassOfDevice.MajorServiceClasses.POSITIONING
NETWORKING_SERVICE_CLASS = ClassOfDevice.MajorServiceClasses.NETWORKING
RENDERING_SERVICE_CLASS = ClassOfDevice.MajorServiceClasses.RENDERING
CAPTURING_SERVICE_CLASS = ClassOfDevice.MajorServiceClasses.CAPTURING
OBJECT_TRANSFER_SERVICE_CLASS = ClassOfDevice.MajorServiceClasses.OBJECT_TRANSFER
AUDIO_SERVICE_CLASS = ClassOfDevice.MajorServiceClasses.AUDIO
TELEPHONY_SERVICE_CLASS = ClassOfDevice.MajorServiceClasses.TELEPHONY
INFORMATION_SERVICE_CLASS = ClassOfDevice.MajorServiceClasses.INFORMATION
# Major Device Classes
MISCELLANEOUS_MAJOR_DEVICE_CLASS = ClassOfDevice.MajorDeviceClass.MISCELLANEOUS
COMPUTER_MAJOR_DEVICE_CLASS = ClassOfDevice.MajorDeviceClass.COMPUTER
PHONE_MAJOR_DEVICE_CLASS = ClassOfDevice.MajorDeviceClass.PHONE
LAN_NETWORK_ACCESS_POINT_MAJOR_DEVICE_CLASS = ClassOfDevice.MajorDeviceClass.LAN_NETWORK_ACCESS_POINT
AUDIO_VIDEO_MAJOR_DEVICE_CLASS = ClassOfDevice.MajorDeviceClass.AUDIO_VIDEO
PERIPHERAL_MAJOR_DEVICE_CLASS = ClassOfDevice.MajorDeviceClass.PERIPHERAL
IMAGING_MAJOR_DEVICE_CLASS = ClassOfDevice.MajorDeviceClass.IMAGING
WEARABLE_MAJOR_DEVICE_CLASS = ClassOfDevice.MajorDeviceClass.WEARABLE
TOY_MAJOR_DEVICE_CLASS = ClassOfDevice.MajorDeviceClass.TOY
HEALTH_MAJOR_DEVICE_CLASS = ClassOfDevice.MajorDeviceClass.HEALTH
UNCATEGORIZED_MAJOR_DEVICE_CLASS = ClassOfDevice.MajorDeviceClass.UNCATEGORIZED
COMPUTER_UNCATEGORIZED_MINOR_DEVICE_CLASS = ClassOfDevice.ComputerMinorDeviceClass.UNCATEGORIZED
COMPUTER_DESKTOP_WORKSTATION_MINOR_DEVICE_CLASS = ClassOfDevice.ComputerMinorDeviceClass.DESKTOP_WORKSTATION
COMPUTER_SERVER_CLASS_COMPUTER_MINOR_DEVICE_CLASS = ClassOfDevice.ComputerMinorDeviceClass.SERVER_CLASS_COMPUTER
COMPUTER_LAPTOP_COMPUTER_MINOR_DEVICE_CLASS = ClassOfDevice.ComputerMinorDeviceClass.LAPTOP_COMPUTER
COMPUTER_HANDHELD_PC_PDA_MINOR_DEVICE_CLASS = ClassOfDevice.ComputerMinorDeviceClass.HANDHELD_PC_PDA
COMPUTER_PALM_SIZE_PC_PDA_MINOR_DEVICE_CLASS = ClassOfDevice.ComputerMinorDeviceClass.PALM_SIZE_PC_PDA
COMPUTER_WEARABLE_COMPUTER_MINOR_DEVICE_CLASS = ClassOfDevice.ComputerMinorDeviceClass.WEARABLE_COMPUTER
COMPUTER_TABLET_MINOR_DEVICE_CLASS = ClassOfDevice.ComputerMinorDeviceClass.TABLET
PHONE_UNCATEGORIZED_MINOR_DEVICE_CLASS = ClassOfDevice.PhoneMinorDeviceClass.UNCATEGORIZED
PHONE_CELLULAR_MINOR_DEVICE_CLASS = ClassOfDevice.PhoneMinorDeviceClass.CELLULAR
PHONE_CORDLESS_MINOR_DEVICE_CLASS = ClassOfDevice.PhoneMinorDeviceClass.CORDLESS
PHONE_SMARTPHONE_MINOR_DEVICE_CLASS = ClassOfDevice.PhoneMinorDeviceClass.SMARTPHONE
PHONE_WIRED_MODEM_OR_VOICE_GATEWAY_MINOR_DEVICE_CLASS = ClassOfDevice.PhoneMinorDeviceClass.WIRED_MODEM_OR_VOICE_GATEWAY
PHONE_COMMON_ISDN_MINOR_DEVICE_CLASS = ClassOfDevice.PhoneMinorDeviceClass.COMMON_ISDN
AUDIO_VIDEO_UNCATEGORIZED_MINOR_DEVICE_CLASS = ClassOfDevice.AudioVideoMinorDeviceClass.UNCATEGORIZED
AUDIO_VIDEO_WEARABLE_HEADSET_DEVICE_MINOR_DEVICE_CLASS = ClassOfDevice.AudioVideoMinorDeviceClass.WEARABLE_HEADSET_DEVICE
AUDIO_VIDEO_HANDS_FREE_DEVICE_MINOR_DEVICE_CLASS = ClassOfDevice.AudioVideoMinorDeviceClass.HANDS_FREE_DEVICE
AUDIO_VIDEO_MICROPHONE_MINOR_DEVICE_CLASS = ClassOfDevice.AudioVideoMinorDeviceClass.MICROPHONE
AUDIO_VIDEO_LOUDSPEAKER_MINOR_DEVICE_CLASS = ClassOfDevice.AudioVideoMinorDeviceClass.LOUDSPEAKER
AUDIO_VIDEO_HEADPHONES_MINOR_DEVICE_CLASS = ClassOfDevice.AudioVideoMinorDeviceClass.HEADPHONES
AUDIO_VIDEO_PORTABLE_AUDIO_MINOR_DEVICE_CLASS = ClassOfDevice.AudioVideoMinorDeviceClass.PORTABLE_AUDIO
AUDIO_VIDEO_CAR_AUDIO_MINOR_DEVICE_CLASS = ClassOfDevice.AudioVideoMinorDeviceClass.CAR_AUDIO
AUDIO_VIDEO_SET_TOP_BOX_MINOR_DEVICE_CLASS = ClassOfDevice.AudioVideoMinorDeviceClass.SET_TOP_BOX
AUDIO_VIDEO_HIFI_AUDIO_DEVICE_MINOR_DEVICE_CLASS = ClassOfDevice.AudioVideoMinorDeviceClass.HIFI_AUDIO_DEVICE
AUDIO_VIDEO_VCR_MINOR_DEVICE_CLASS = ClassOfDevice.AudioVideoMinorDeviceClass.VCR
AUDIO_VIDEO_VIDEO_CAMERA_MINOR_DEVICE_CLASS = ClassOfDevice.AudioVideoMinorDeviceClass.VIDEO_CAMERA
AUDIO_VIDEO_CAMCORDER_MINOR_DEVICE_CLASS = ClassOfDevice.AudioVideoMinorDeviceClass.CAMCORDER
AUDIO_VIDEO_VIDEO_MONITOR_MINOR_DEVICE_CLASS = ClassOfDevice.AudioVideoMinorDeviceClass.VIDEO_MONITOR
AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER_MINOR_DEVICE_CLASS = ClassOfDevice.AudioVideoMinorDeviceClass.VIDEO_DISPLAY_AND_LOUDSPEAKER
AUDIO_VIDEO_VIDEO_CONFERENCING_MINOR_DEVICE_CLASS = ClassOfDevice.AudioVideoMinorDeviceClass.VIDEO_CONFERENCING
AUDIO_VIDEO_GAMING_OR_TOY_MINOR_DEVICE_CLASS = ClassOfDevice.AudioVideoMinorDeviceClass.GAMING_OR_TOY
PERIPHERAL_UNCATEGORIZED_MINOR_DEVICE_CLASS = ClassOfDevice.PeripheralMinorDeviceClass.UNCATEGORIZED
PERIPHERAL_KEYBOARD_MINOR_DEVICE_CLASS = ClassOfDevice.PeripheralMinorDeviceClass.KEYBOARD
PERIPHERAL_POINTING_DEVICE_MINOR_DEVICE_CLASS = ClassOfDevice.PeripheralMinorDeviceClass.POINTING_DEVICE
PERIPHERAL_COMBO_KEYBOARD_POINTING_DEVICE_MINOR_DEVICE_CLASS = ClassOfDevice.PeripheralMinorDeviceClass.COMBO_KEYBOARD_POINTING_DEVICE
PERIPHERAL_JOYSTICK_MINOR_DEVICE_CLASS = ClassOfDevice.PeripheralMinorDeviceClass.JOYSTICK
PERIPHERAL_GAMEPAD_MINOR_DEVICE_CLASS = ClassOfDevice.PeripheralMinorDeviceClass.GAMEPAD
PERIPHERAL_REMOTE_CONTROL_MINOR_DEVICE_CLASS = ClassOfDevice.PeripheralMinorDeviceClass.REMOTE_CONTROL
PERIPHERAL_SENSING_DEVICE_MINOR_DEVICE_CLASS = ClassOfDevice.PeripheralMinorDeviceClass.SENSING_DEVICE
PERIPHERAL_DIGITIZER_TABLET_MINOR_DEVICE_CLASS = ClassOfDevice.PeripheralMinorDeviceClass.DIGITIZER_TABLET
PERIPHERAL_CARD_READER_MINOR_DEVICE_CLASS = ClassOfDevice.PeripheralMinorDeviceClass.CARD_READER
PERIPHERAL_DIGITAL_PEN_MINOR_DEVICE_CLASS = ClassOfDevice.PeripheralMinorDeviceClass.DIGITAL_PEN
PERIPHERAL_HANDHELD_SCANNER_MINOR_DEVICE_CLASS = ClassOfDevice.PeripheralMinorDeviceClass.HANDHELD_SCANNER
PERIPHERAL_HANDHELD_GESTURAL_INPUT_DEVICE_MINOR_DEVICE_CLASS = ClassOfDevice.PeripheralMinorDeviceClass.HANDHELD_GESTURAL_INPUT_DEVICE
WEARABLE_UNCATEGORIZED_MINOR_DEVICE_CLASS = ClassOfDevice.WearableMinorDeviceClass.UNCATEGORIZED
WEARABLE_WRISTWATCH_MINOR_DEVICE_CLASS = ClassOfDevice.WearableMinorDeviceClass.WRISTWATCH
WEARABLE_PAGER_MINOR_DEVICE_CLASS = ClassOfDevice.WearableMinorDeviceClass.PAGER
WEARABLE_JACKET_MINOR_DEVICE_CLASS = ClassOfDevice.WearableMinorDeviceClass.JACKET
WEARABLE_HELMET_MINOR_DEVICE_CLASS = ClassOfDevice.WearableMinorDeviceClass.HELMET
WEARABLE_GLASSES_MINOR_DEVICE_CLASS = ClassOfDevice.WearableMinorDeviceClass.GLASSES
TOY_UNCATEGORIZED_MINOR_DEVICE_CLASS = ClassOfDevice.ToyMinorDeviceClass.UNCATEGORIZED
TOY_ROBOT_MINOR_DEVICE_CLASS = ClassOfDevice.ToyMinorDeviceClass.ROBOT
TOY_VEHICLE_MINOR_DEVICE_CLASS = ClassOfDevice.ToyMinorDeviceClass.VEHICLE
TOY_DOLL_ACTION_FIGURE_MINOR_DEVICE_CLASS = ClassOfDevice.ToyMinorDeviceClass.DOLL_ACTION_FIGURE
TOY_CONTROLLER_MINOR_DEVICE_CLASS = ClassOfDevice.ToyMinorDeviceClass.CONTROLLER
TOY_GAME_MINOR_DEVICE_CLASS = ClassOfDevice.ToyMinorDeviceClass.GAME
HEALTH_UNDEFINED_MINOR_DEVICE_CLASS = ClassOfDevice.HealthMinorDeviceClass.UNDEFINED
HEALTH_BLOOD_PRESSURE_MONITOR_MINOR_DEVICE_CLASS = ClassOfDevice.HealthMinorDeviceClass.BLOOD_PRESSURE_MONITOR
HEALTH_THERMOMETER_MINOR_DEVICE_CLASS = ClassOfDevice.HealthMinorDeviceClass.THERMOMETER
HEALTH_WEIGHING_SCALE_MINOR_DEVICE_CLASS = ClassOfDevice.HealthMinorDeviceClass.WEIGHING_SCALE
HEALTH_GLUCOSE_METER_MINOR_DEVICE_CLASS = ClassOfDevice.HealthMinorDeviceClass.GLUCOSE_METER
HEALTH_PULSE_OXIMETER_MINOR_DEVICE_CLASS = ClassOfDevice.HealthMinorDeviceClass.PULSE_OXIMETER
HEALTH_HEART_PULSE_RATE_MONITOR_MINOR_DEVICE_CLASS = ClassOfDevice.HealthMinorDeviceClass.HEART_PULSE_RATE_MONITOR
HEALTH_HEALTH_DATA_DISPLAY_MINOR_DEVICE_CLASS = ClassOfDevice.HealthMinorDeviceClass.HEALTH_DATA_DISPLAY
HEALTH_STEP_COUNTER_MINOR_DEVICE_CLASS = ClassOfDevice.HealthMinorDeviceClass.STEP_COUNTER
HEALTH_BODY_COMPOSITION_ANALYZER_MINOR_DEVICE_CLASS = ClassOfDevice.HealthMinorDeviceClass.BODY_COMPOSITION_ANALYZER
HEALTH_PEAK_FLOW_MONITOR_MINOR_DEVICE_CLASS = ClassOfDevice.HealthMinorDeviceClass.PEAK_FLOW_MONITOR
HEALTH_MEDICATION_MONITOR_MINOR_DEVICE_CLASS = ClassOfDevice.HealthMinorDeviceClass.MEDICATION_MONITOR
HEALTH_KNEE_PROSTHESIS_MINOR_DEVICE_CLASS = ClassOfDevice.HealthMinorDeviceClass.KNEE_PROSTHESIS
HEALTH_ANKLE_PROSTHESIS_MINOR_DEVICE_CLASS = ClassOfDevice.HealthMinorDeviceClass.ANKLE_PROSTHESIS
HEALTH_GENERIC_HEALTH_MANAGER_MINOR_DEVICE_CLASS = ClassOfDevice.HealthMinorDeviceClass.GENERIC_HEALTH_MANAGER
HEALTH_PERSONAL_MOBILITY_DEVICE_MINOR_DEVICE_CLASS = ClassOfDevice.HealthMinorDeviceClass.PERSONAL_MOBILITY_DEVICE
# fmt: on
# pylint: enable=line-too-long
@@ -711,16 +945,16 @@ class DeviceClass:
@staticmethod
def service_class_labels(service_class_flags):
return bit_flags_to_strings(
service_class_flags, DeviceClass.SERVICE_CLASS_LABELS
service_class_flags, ClassOfDevice.MAJOR_SERVICE_CLASS_LABELS
)
@staticmethod
def major_device_class_name(device_class):
return name_or_number(DeviceClass.MAJOR_DEVICE_CLASS_NAMES, device_class)
return name_or_number(ClassOfDevice.MAJOR_DEVICE_CLASS_LABELS, device_class)
@staticmethod
def minor_device_class_name(major_device_class, minor_device_class):
class_names = DeviceClass.MINOR_DEVICE_CLASS_NAMES.get(major_device_class)
class_names = ClassOfDevice.MINOR_DEVICE_CLASS_LABELS.get(major_device_class)
if class_names is None:
return f'#{minor_device_class:02X}'
return name_or_number(class_names, minor_device_class)
@@ -1255,6 +1489,10 @@ class Appearance:
category = cls.Category(appearance >> 6)
return cls(category, appearance & 0x3F)
@classmethod
def from_bytes(cls, data: bytes):
return cls.from_int(int.from_bytes(data, byteorder="little"))
def __init__(self, category: Category, subcategory: int) -> None:
self.category = category
if subcategory_class := self.SUBCATEGORY_CLASSES.get(category):
@@ -1265,6 +1503,9 @@ class Appearance:
def __int__(self) -> int:
return self.category << 6 | self.subcategory
def __bytes__(self) -> bytes:
return int(self).to_bytes(2, byteorder="little")
def __repr__(self) -> str:
return (
'Appearance('
@@ -1276,6 +1517,61 @@ class Appearance:
def __str__(self) -> str:
return f'{self.category.name}/{self.subcategory.name}'
def __eq__(self, value: Any) -> bool:
return (
isinstance(value, Appearance)
and self.category == value.category
and self.subcategory == value.subcategory
)
# -----------------------------------------------------------------------------
# Classes representing "Data Types" defined in
# "Supplement to the Bluetooth Core Specification", Part A
# -----------------------------------------------------------------------------
# TODO: use ABC, figure out multiple base classes with metaclasses
class DataType:
# Human-reable label/name for the type
label = ""
# Advertising Data type ID for this data type.
ad_type: AdvertisingData.Type = 0 # type: ignore
def value_string(self) -> str:
"""Human-reable string representation of the value."""
raise NotImplementedError()
def to_string(self, use_label: bool = False) -> str:
if use_label:
return f"[{self.label}]: {self.value_string()}"
return f"{self.__class__.__name__}({self.value_string()})"
@classmethod
def from_advertising_data(cls, advertising_data: AdvertisingData) -> Optional[Self]:
if (data := advertising_data.get(cls.ad_type, raw=True)) is None:
return None
return cls.from_bytes(data)
@classmethod
def all_from_advertising_data(cls, advertising_data: AdvertisingData) -> list[Self]:
return [
cls.from_bytes(data)
for data in advertising_data.get_all(cls.ad_type, raw=True)
]
@classmethod
def from_bytes(cls, data: bytes) -> Self:
"""Create an instance from a serialized form."""
raise NotImplementedError()
def __bytes__(self) -> bytes:
raise NotImplementedError()
def __str__(self) -> str:
return self.to_string()
# -----------------------------------------------------------------------------
# Advertising Data
@@ -1351,7 +1647,7 @@ class AdvertisingData:
THREE_D_INFORMATION_DATA = 0x3D
MANUFACTURER_SPECIFIC_DATA = 0xFF
class Flags(enum.IntFlag):
class Flags(utils.CompatibleIntFlag):
LE_LIMITED_DISCOVERABLE_MODE = 1 << 0
LE_GENERAL_DISCOVERABLE_MODE = 1 << 1
BR_EDR_NOT_SUPPORTED = 1 << 2
@@ -1419,15 +1715,25 @@ class AdvertisingData:
BR_EDR_CONTROLLER_FLAG = Flags.SIMULTANEOUS_LE_BR_EDR_CAPABLE
BR_EDR_HOST_FLAG = 0x10 # Deprecated
ad_structures: list[tuple[int, bytes]]
ad_structures: list[tuple[AdvertisingData.Type, bytes]]
# fmt: on
# pylint: enable=line-too-long
def __init__(self, ad_structures: Optional[list[tuple[int, bytes]]] = None) -> None:
def __init__(
self,
ad_structures: Optional[Iterable[Union[tuple[int, bytes], DataType]]] = None,
) -> None:
if ad_structures is None:
ad_structures = []
self.ad_structures = ad_structures[:]
self.ad_structures = [
(
(element.ad_type, bytes(element))
if isinstance(element, DataType)
else (AdvertisingData.Type(element[0]), element[1])
)
for element in ad_structures
]
@classmethod
def from_bytes(cls, data: bytes) -> AdvertisingData:
@@ -1444,11 +1750,10 @@ class AdvertisingData:
'LE Limited Discoverable Mode',
'LE General Discoverable Mode',
'BR/EDR Not Supported',
'Simultaneous LE and BR/EDR (Controller)',
'Simultaneous LE and BR/EDR (Host)',
'Simultaneous LE and BR/EDR',
]
)
return ','.join(bit_flags_to_strings(flags, flag_names))
return ', '.join(bit_flags_to_strings(flags, flag_names))
@staticmethod
def uuid_list_to_objects(ad_data: bytes, uuid_size: int) -> list[UUID]:
@@ -1604,7 +1909,7 @@ class AdvertisingData:
if length > 0:
ad_type = data[offset]
ad_data = data[offset + 1 : offset + length]
self.ad_structures.append((ad_type, ad_data))
self.ad_structures.append((AdvertisingData.Type(ad_type), ad_data))
offset += length
@overload
@@ -1623,6 +1928,7 @@ class AdvertisingData:
],
raw: Literal[False] = False,
) -> list[list[UUID]]: ...
@overload
def get_all(
self,
@@ -1633,6 +1939,7 @@ class AdvertisingData:
],
raw: Literal[False] = False,
) -> list[tuple[UUID, bytes]]: ...
@overload
def get_all(
self,
@@ -1644,6 +1951,7 @@ class AdvertisingData:
],
raw: Literal[False] = False,
) -> list[str]: ...
@overload
def get_all(
self,
@@ -1655,26 +1963,31 @@ class AdvertisingData:
],
raw: Literal[False] = False,
) -> list[int]: ...
@overload
def get_all(
self,
type_id: Literal[AdvertisingData.Type.PERIPHERAL_CONNECTION_INTERVAL_RANGE,],
raw: Literal[False] = False,
) -> list[tuple[int, int]]: ...
@overload
def get_all(
self,
type_id: Literal[AdvertisingData.Type.MANUFACTURER_SPECIFIC_DATA,],
raw: Literal[False] = False,
) -> list[tuple[int, bytes]]: ...
@overload
def get_all(
self,
type_id: Literal[AdvertisingData.Type.APPEARANCE,],
raw: Literal[False] = False,
) -> list[Appearance]: ...
@overload
def get_all(self, type_id: int, raw: Literal[True]) -> list[bytes]: ...
@overload
def get_all(
self, type_id: int, raw: bool = False
@@ -1682,7 +1995,7 @@ class AdvertisingData:
def get_all(self, type_id: int, raw: bool = False) -> list[AdvertisingDataObject]: # type: ignore[misc]
'''
Get Advertising Data Structure(s) with a given type
Get all advertising data elements as simple AdvertisingDataObject objects.
Returns a (possibly empty) list of matches.
'''
@@ -1708,6 +2021,7 @@ class AdvertisingData:
],
raw: Literal[False] = False,
) -> Optional[list[UUID]]: ...
@overload
def get(
self,
@@ -1718,6 +2032,7 @@ class AdvertisingData:
],
raw: Literal[False] = False,
) -> Optional[tuple[UUID, bytes]]: ...
@overload
def get(
self,
@@ -1729,6 +2044,7 @@ class AdvertisingData:
],
raw: Literal[False] = False,
) -> Optional[Optional[str]]: ...
@overload
def get(
self,
@@ -1740,26 +2056,31 @@ class AdvertisingData:
],
raw: Literal[False] = False,
) -> Optional[int]: ...
@overload
def get(
self,
type_id: Literal[AdvertisingData.Type.PERIPHERAL_CONNECTION_INTERVAL_RANGE,],
raw: Literal[False] = False,
) -> Optional[tuple[int, int]]: ...
@overload
def get(
self,
type_id: Literal[AdvertisingData.Type.MANUFACTURER_SPECIFIC_DATA,],
raw: Literal[False] = False,
) -> Optional[tuple[int, bytes]]: ...
@overload
def get(
self,
type_id: Literal[AdvertisingData.Type.APPEARANCE,],
raw: Literal[False] = False,
) -> Optional[Appearance]: ...
@overload
def get(self, type_id: int, raw: Literal[True]) -> Optional[bytes]: ...
@overload
def get(
self, type_id: int, raw: bool = False
@@ -1767,7 +2088,7 @@ class AdvertisingData:
def get(self, type_id: int, raw: bool = False) -> Optional[AdvertisingDataObject]:
'''
Get Advertising Data Structure(s) with a given type
Get advertising data as a simple AdvertisingDataObject object.
Returns the first entry, or None if no structure matches.
'''
@@ -1822,7 +2143,22 @@ class ConnectionPHY:
# LE Role
# -----------------------------------------------------------------------------
class LeRole(enum.IntEnum):
# fmt: off
PERIPHERAL_ONLY = 0x00
CENTRAL_ONLY = 0x01
BOTH_PERIPHERAL_PREFERRED = 0x02
BOTH_CENTRAL_PREFERRED = 0x03
# -----------------------------------------------------------------------------
# Security Manager OOB Flag
# -----------------------------------------------------------------------------
class SecurityManagerOutOfBandFlag(utils.CompatibleIntFlag):
"""
See Supplement to the Bluetooth Core Specification, Part A
1.7 SECURITY MANAGER OUT OF BAND (OOB)
"""
OOB_FLAGS_FIELD = 1 << 0
LE_SUPPORTED = 1 << 1
ADDRESS_TYPE = 1 << 3

1025
bumble/data_types.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -47,6 +47,7 @@ from typing_extensions import Self
from bumble import (
core,
data_types,
gatt,
gatt_client,
gatt_server,
@@ -1881,16 +1882,6 @@ class Connection(utils.CompositeEventEmitter):
def send_l2cap_pdu(self, cid: int, pdu: bytes) -> None:
self.device.send_l2cap_pdu(self.handle, cid, pdu)
@utils.deprecated("Please use create_l2cap_channel()")
async def open_l2cap_channel(
self,
psm,
max_credits=DEVICE_DEFAULT_L2CAP_COC_MAX_CREDITS,
mtu=DEVICE_DEFAULT_L2CAP_COC_MTU,
mps=DEVICE_DEFAULT_L2CAP_COC_MPS,
):
return await self.device.open_l2cap_channel(self, psm, max_credits, mtu, mps)
@overload
async def create_l2cap_channel(
self, spec: l2cap.ClassicChannelSpec
@@ -2076,9 +2067,7 @@ class DeviceConfiguration:
connectable: bool = True
discoverable: bool = True
advertising_data: bytes = bytes(
AdvertisingData(
[(AdvertisingData.COMPLETE_LOCAL_NAME, bytes(DEVICE_DEFAULT_NAME, 'utf-8'))]
)
AdvertisingData([data_types.CompleteLocalName(DEVICE_DEFAULT_NAME)])
)
irk: bytes = bytes(16) # This really must be changed for any level of security
keystore: Optional[str] = None
@@ -2122,9 +2111,7 @@ class DeviceConfiguration:
self.advertising_data = bytes.fromhex(advertising_data)
elif name is not None:
self.advertising_data = bytes(
AdvertisingData(
[(AdvertisingData.COMPLETE_LOCAL_NAME, bytes(self.name, 'utf-8'))]
)
AdvertisingData([data_types.CompleteLocalName(self.name)])
)
# Load scan response data
@@ -2608,36 +2595,6 @@ class Device(utils.CompositeEventEmitter):
None,
)
@utils.deprecated("Please use create_l2cap_server()")
def register_l2cap_server(self, psm, server) -> int:
return self.l2cap_channel_manager.register_server(psm, server)
@utils.deprecated("Please use create_l2cap_server()")
def register_l2cap_channel_server(
self,
psm,
server,
max_credits=DEVICE_DEFAULT_L2CAP_COC_MAX_CREDITS,
mtu=DEVICE_DEFAULT_L2CAP_COC_MTU,
mps=DEVICE_DEFAULT_L2CAP_COC_MPS,
):
return self.l2cap_channel_manager.register_le_coc_server(
psm, server, max_credits, mtu, mps
)
@utils.deprecated("Please use create_l2cap_channel()")
async def open_l2cap_channel(
self,
connection,
psm,
max_credits=DEVICE_DEFAULT_L2CAP_COC_MAX_CREDITS,
mtu=DEVICE_DEFAULT_L2CAP_COC_MTU,
mps=DEVICE_DEFAULT_L2CAP_COC_MPS,
):
return await self.l2cap_channel_manager.open_le_coc(
connection, psm, max_credits, mtu, mps
)
@overload
async def create_l2cap_channel(
self,
@@ -3605,14 +3562,7 @@ class Device(utils.CompositeEventEmitter):
# Synthesize an inquiry response if none is set already
if self.inquiry_response is None:
self.inquiry_response = bytes(
AdvertisingData(
[
(
AdvertisingData.COMPLETE_LOCAL_NAME,
bytes(self.name, 'utf-8'),
)
]
)
AdvertisingData([data_types.CompleteLocalName(self.name)])
)
# Update the controller

View File

@@ -112,7 +112,14 @@ class SpecableEnum(utils.OpenIntEnum):
@classmethod
def type_spec(cls, size: int):
return {'size': size, 'mapper': lambda x: cls(x).name}
return {
'serializer': lambda x: x.to_bytes(size, 'little'),
'parser': lambda data, offset: (
offset + size,
cls(int.from_bytes(data[offset : offset + size], 'little')),
),
'mapper': lambda x: cls(x).name,
}
@classmethod
def type_metadata(cls, size: int, list_begin: bool = False, list_end: bool = False):
@@ -123,7 +130,14 @@ class SpecableFlag(enum.IntFlag):
@classmethod
def type_spec(cls, size: int):
return {'size': size, 'mapper': lambda x: cls(x).name}
return {
'serializer': lambda x: x.to_bytes(size, 'little'),
'parser': lambda data, offset: (
offset + size,
cls(int.from_bytes(data[offset : offset + size], 'little')),
),
'mapper': lambda x: cls(x).name,
}
@classmethod
def type_metadata(cls, size: int, list_begin: bool = False, list_end: bool = False):
@@ -1322,7 +1336,7 @@ class LeFeature(SpecableEnum):
MONITORING_ADVERTISERS = 64
FRAME_SPACE_UPDATE = 65
class LeFeatureMask(enum.IntFlag):
class LeFeatureMask(utils.CompatibleIntFlag):
LE_ENCRYPTION = 1 << LeFeature.LE_ENCRYPTION
CONNECTION_PARAMETERS_REQUEST_PROCEDURE = 1 << LeFeature.CONNECTION_PARAMETERS_REQUEST_PROCEDURE
EXTENDED_REJECT_INDICATION = 1 << LeFeature.EXTENDED_REJECT_INDICATION
@@ -1463,7 +1477,7 @@ class LmpFeature(SpecableEnum):
SLOT_AVAILABILITY_MASK = 138
TRAIN_NUDGING = 139
class LmpFeatureMask(enum.IntFlag):
class LmpFeatureMask(utils.CompatibleIntFlag):
# Page 0 (Legacy LMP features)
LMP_3_SLOT_PACKETS = (1 << LmpFeature.LMP_3_SLOT_PACKETS)
LMP_5_SLOT_PACKETS = (1 << LmpFeature.LMP_5_SLOT_PACKETS)
@@ -2135,6 +2149,7 @@ class Address:
if len(address) == 12 + 5:
# Form with ':' separators
address = address.replace(':', '')
self.address_bytes = bytes(reversed(bytes.fromhex(address)))
if len(self.address_bytes) != 6:

View File

@@ -217,33 +217,41 @@ class HID(ABC, utils.EventEmitter):
self.role = role
# Register ourselves with the L2CAP channel manager
device.register_l2cap_server(HID_CONTROL_PSM, self.on_l2cap_connection)
device.register_l2cap_server(HID_INTERRUPT_PSM, self.on_l2cap_connection)
device.create_l2cap_server(
l2cap.ClassicChannelSpec(HID_CONTROL_PSM), self.on_l2cap_connection
)
device.create_l2cap_server(
l2cap.ClassicChannelSpec(HID_INTERRUPT_PSM), self.on_l2cap_connection
)
device.on(device.EVENT_CONNECTION, self.on_device_connection)
async def connect_control_channel(self) -> None:
if not self.connection:
raise InvalidStateError("Connection is not established!")
# Create a new L2CAP connection - control channel
try:
channel = await self.device.l2cap_channel_manager.connect(
self.connection, HID_CONTROL_PSM
channel = await self.connection.create_l2cap_channel(
l2cap.ClassicChannelSpec(HID_CONTROL_PSM)
)
channel.sink = self.on_ctrl_pdu
self.l2cap_ctrl_channel = channel
except ProtocolError:
logging.exception(f'L2CAP connection failed.')
logging.exception('L2CAP connection failed.')
raise
async def connect_interrupt_channel(self) -> None:
if not self.connection:
raise InvalidStateError("Connection is not established!")
# Create a new L2CAP connection - interrupt channel
try:
channel = await self.device.l2cap_channel_manager.connect(
self.connection, HID_INTERRUPT_PSM
channel = await self.connection.create_l2cap_channel(
l2cap.ClassicChannelSpec(HID_CONTROL_PSM)
)
channel.sink = self.on_intr_pdu
self.l2cap_intr_channel = channel
except ProtocolError:
logging.exception(f'L2CAP connection failed.')
logging.exception('L2CAP connection failed.')
raise
async def disconnect_interrupt_channel(self) -> None:

View File

@@ -1531,16 +1531,6 @@ class ChannelManager:
if cid in self.fixed_channels:
del self.fixed_channels[cid]
@utils.deprecated("Please use create_classic_server")
def register_server(
self,
psm: int,
server: Callable[[ClassicChannel], Any],
) -> int:
return self.create_classic_server(
handler=server, spec=ClassicChannelSpec(psm=psm)
).psm
def create_classic_server(
self,
spec: ClassicChannelSpec,
@@ -1577,22 +1567,6 @@ class ChannelManager:
return self.servers[spec.psm]
@utils.deprecated("Please use create_le_credit_based_server()")
def register_le_coc_server(
self,
psm: int,
server: Callable[[LeCreditBasedChannel], Any],
max_credits: int,
mtu: int,
mps: int,
) -> int:
return self.create_le_credit_based_server(
spec=LeCreditBasedChannelSpec(
psm=None if psm == 0 else psm, mtu=mtu, mps=mps, max_credits=max_credits
),
handler=server,
).psm
def create_le_credit_based_server(
self,
spec: LeCreditBasedChannelSpec,
@@ -2145,17 +2119,6 @@ class ChannelManager:
if channel.source_cid in connection_channels:
del connection_channels[channel.source_cid]
@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:
return await self.create_le_credit_based_channel(
connection=connection,
spec=LeCreditBasedChannelSpec(
psm=psm, max_credits=max_credits, mtu=mtu, mps=mps
),
)
async def create_le_credit_based_channel(
self,
connection: Connection,
@@ -2202,12 +2165,6 @@ class ChannelManager:
return 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)
)
async def create_classic_channel(
self, connection: Connection, spec: ClassicChannelSpec
) -> ClassicChannel:
@@ -2244,20 +2201,3 @@ class ChannelManager:
raise e
return channel
# -----------------------------------------------------------------------------
# Deprecated Classes
# -----------------------------------------------------------------------------
class Channel(ClassicChannel):
@utils.deprecated("Please use ClassicChannel")
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
class LeConnectionOrientedChannel(LeCreditBasedChannel):
@utils.deprecated("Please use LeCreditBasedChannel")
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)

View File

@@ -21,7 +21,7 @@ import logging
import struct
from typing import Any, Callable, Optional, Union
from bumble import gatt, gatt_client, l2cap, utils
from bumble import data_types, gatt, gatt_client, l2cap, utils
from bumble.core import AdvertisingData
from bumble.device import Connection, Device
@@ -185,12 +185,11 @@ class AshaService(gatt.TemplateService):
return bytes(
AdvertisingData(
[
(
AdvertisingData.SERVICE_DATA_16_BIT_UUID,
bytes(gatt.GATT_ASHA_SERVICE)
+ bytes([self.protocol_version, self.capability])
data_types.ServiceData16BitUUID(
gatt.GATT_ASHA_SERVICE,
bytes([self.protocol_version, self.capability])
+ self.hisyncid[:4],
),
)
]
)
)

View File

@@ -27,7 +27,7 @@ from collections.abc import Sequence
from typing_extensions import Self
from bumble import core, gatt, hci, utils
from bumble import core, data_types, gatt, hci, utils
from bumble.profiles import le_audio
# -----------------------------------------------------------------------------
@@ -257,11 +257,10 @@ class UnicastServerAdvertisingData:
return bytes(
core.AdvertisingData(
[
(
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID,
data_types.ServiceData16BitUUID(
gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE,
struct.pack(
'<2sBIB',
bytes(gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE),
'<BIB',
self.announcement_type,
self.available_audio_contexts,
len(self.metadata),
@@ -490,12 +489,8 @@ class BroadcastAudioAnnouncement:
return bytes(
core.AdvertisingData(
[
(
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID,
(
bytes(gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE)
+ bytes(self)
),
data_types.ServiceData16BitUUID(
gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE, bytes(self)
)
]
)
@@ -607,12 +602,8 @@ class BasicAudioAnnouncement:
return bytes(
core.AdvertisingData(
[
(
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID,
(
bytes(gatt.GATT_BASIC_AUDIO_ANNOUNCEMENT_SERVICE)
+ bytes(self)
),
data_types.ServiceData16BitUUID(
gatt.GATT_BASIC_AUDIO_ANNOUNCEMENT_SERVICE, bytes(self)
)
]
)

View File

@@ -500,6 +500,22 @@ class OpenIntEnum(enum.IntEnum):
return obj
# -----------------------------------------------------------------------------
class CompatibleIntFlag(enum.IntFlag):
"""
Subclass of `enum.IntFlag` with a `composite_name` property that behaves like the
`name` property of the `enum.IntFlag` implementation for python vesions >= 3.11
"""
@property
def composite_name(self) -> str:
return '|'.join(
name
for flag in self.__class__
if self.value & flag.value and (name := flag.name) is not None
)
# -----------------------------------------------------------------------------
class ByteSerializable(Protocol):
"""

View File

@@ -21,6 +21,7 @@ import struct
import sys
import bumble.logging
from bumble import data_types
from bumble.core import AdvertisingData
from bumble.device import Device
from bumble.profiles.battery_service import BatteryService
@@ -47,15 +48,14 @@ async def main() -> None:
device.advertising_data = bytes(
AdvertisingData(
[
(
AdvertisingData.COMPLETE_LOCAL_NAME,
bytes('Bumble Battery', 'utf-8'),
data_types.CompleteLocalName('Bumble Battery'),
data_types.IncompleteListOf16BitServiceUUIDs(
[battery_service.uuid]
),
(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
bytes(battery_service.uuid),
data_types.Appearance(
data_types.Appearance.Category.WEARABLE_AUDIO_DEVICE,
data_types.Appearance.WearableAudioDeviceSubcategory.EARBUD,
),
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x0340)),
]
)
)

View File

@@ -20,6 +20,7 @@ import struct
import sys
import bumble.logging
from bumble import data_types
from bumble.core import AdvertisingData
from bumble.device import Device
from bumble.profiles.device_information_service import DeviceInformationService
@@ -53,11 +54,11 @@ async def main() -> None:
device.advertising_data = bytes(
AdvertisingData(
[
(
AdvertisingData.COMPLETE_LOCAL_NAME,
bytes('Bumble Device', 'utf-8'),
data_types.CompleteLocalName('Bumble Device'),
data_types.Appearance(
data_types.Appearance.Category.HEART_RATE_SENSOR,
data_types.Appearance.HeartRateSensorSubcategory.GENERIC_HEART_RATE_SENSOR,
),
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x0340)),
]
)
)

View File

@@ -24,6 +24,7 @@ import sys
import time
import bumble.logging
from bumble import data_types
from bumble.core import AdvertisingData
from bumble.device import Device
from bumble.profiles.device_information_service import DeviceInformationService
@@ -88,15 +89,14 @@ async def main() -> None:
device.advertising_data = bytes(
AdvertisingData(
[
(
AdvertisingData.COMPLETE_LOCAL_NAME,
bytes('Bumble Heart', 'utf-8'),
data_types.CompleteLocalName('Bumble Heart'),
data_types.IncompleteListOf16BitServiceUUIDs(
[heart_rate_service.uuid]
),
(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
bytes(heart_rate_service.uuid),
data_types.Appearance(
data_types.Appearance.Category.HEART_RATE_SENSOR,
data_types.Appearance.HeartRateSensorSubcategory.GENERIC_HEART_RATE_SENSOR,
),
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x0340)),
]
)
)

View File

@@ -23,6 +23,7 @@ import sys
import websockets
import bumble.logging
from bumble import data_types
from bumble.colors import color
from bumble.core import AdvertisingData
from bumble.device import Connection, Device, Peer
@@ -341,16 +342,18 @@ async def keyboard_device(device, command):
device.advertising_data = bytes(
AdvertisingData(
[
(
AdvertisingData.COMPLETE_LOCAL_NAME,
bytes('Bumble Keyboard', 'utf-8'),
data_types.CompleteLocalName('Bumble Keyboard'),
data_types.IncompleteListOf16BitServiceUUIDs(
[GATT_HUMAN_INTERFACE_DEVICE_SERVICE]
),
(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
bytes(GATT_HUMAN_INTERFACE_DEVICE_SERVICE),
data_types.Appearance(
data_types.Appearance.Category.HUMAN_INTERFACE_DEVICE,
data_types.Appearance.HumanInterfaceDeviceSubcategory.KEYBOARD,
),
data_types.Flags(
AdvertisingData.Flags.LE_LIMITED_DISCOVERABLE_MODE
| AdvertisingData.Flags.BR_EDR_NOT_SUPPORTED
),
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x03C1)),
(AdvertisingData.FLAGS, bytes([0x05])),
]
)
)

View File

@@ -20,6 +20,7 @@ import struct
import sys
import bumble.logging
from bumble import data_types
from bumble.core import AdvertisingData
from bumble.device import AdvertisingType, Device
from bumble.hci import Address
@@ -60,7 +61,10 @@ async def main() -> None:
device.scan_response_data = bytes(
AdvertisingData(
[
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x0340)),
data_types.Appearance(
data_types.Appearance.Category.HEART_RATE_SENSOR,
data_types.Appearance.HeartRateSensorSubcategory.GENERIC_HEART_RATE_SENSOR,
)
]
)
)

View File

@@ -23,7 +23,7 @@ from typing import Optional
import websockets
import bumble.logging
from bumble import decoder, gatt
from bumble import data_types, decoder, gatt
from bumble.core import AdvertisingData
from bumble.device import AdvertisingParameters, Device
from bumble.profiles import asha
@@ -78,14 +78,10 @@ async def main() -> None:
bytes(
AdvertisingData(
[
(
AdvertisingData.COMPLETE_LOCAL_NAME,
bytes(device.name, 'utf-8'),
),
(AdvertisingData.FLAGS, bytes([0x06])),
(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
bytes(gatt.GATT_ASHA_SERVICE),
data_types.CompleteLocalName(device.name),
data_types.Flags(AdvertisingData.Flags(0x06)),
data_types.IncompleteListOf16BitServiceUUIDs(
[gatt.GATT_ASHA_SERVICE]
),
]
)

View File

@@ -20,6 +20,7 @@ import secrets
import sys
import bumble.logging
from bumble import data_types
from bumble.core import AdvertisingData
from bumble.device import Device
from bumble.hci import Address
@@ -66,23 +67,14 @@ async def main() -> None:
bytes(
AdvertisingData(
[
(
AdvertisingData.COMPLETE_LOCAL_NAME,
bytes(f'Bumble LE Audio-{i}', 'utf-8'),
),
(
AdvertisingData.FLAGS,
bytes(
[
data_types.CompleteLocalName(f'Bumble LE Audio-{i}'),
data_types.Flags(
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
| AdvertisingData.BR_EDR_HOST_FLAG
| AdvertisingData.BR_EDR_CONTROLLER_FLAG
]
),
),
(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
bytes(CoordinatedSetIdentificationService.UUID),
data_types.IncompleteListOf16BitServiceUUIDs(
[CoordinatedSetIdentificationService.UUID]
),
]
)

View File

@@ -19,6 +19,7 @@ import asyncio
import sys
import bumble.logging
from bumble import data_types
from bumble.core import AdvertisingData
from bumble.device import Device
from bumble.profiles.hap import (
@@ -71,23 +72,14 @@ async def main() -> None:
advertising_data = bytes(
AdvertisingData(
[
(
AdvertisingData.COMPLETE_LOCAL_NAME,
bytes('Bumble HearingAccessService', 'utf-8'),
),
(
AdvertisingData.FLAGS,
bytes(
[
data_types.CompleteLocalName('Bumble HearingAccessService'),
data_types.Flags(
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
| AdvertisingData.BR_EDR_HOST_FLAG
| AdvertisingData.BR_EDR_CONTROLLER_FLAG
]
),
),
(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
bytes(HearingAccessService.UUID),
data_types.IncompleteListOf16BitServiceUUIDs(
[HearingAccessService.UUID]
),
]
)

View File

@@ -23,6 +23,7 @@ from typing import Optional
import websockets
import bumble.logging
from bumble import data_types
from bumble.core import AdvertisingData
from bumble.device import (
AdvertisingEventProperties,
@@ -106,17 +107,10 @@ async def main() -> None:
advertising_data = bytes(
AdvertisingData(
[
(
AdvertisingData.COMPLETE_LOCAL_NAME,
bytes('Bumble LE Audio', 'utf-8'),
),
(
AdvertisingData.FLAGS,
bytes([AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG]),
),
(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
bytes(PublishedAudioCapabilitiesService.UUID),
data_types.CompleteLocalName('Bumble LE Audio'),
data_types.Flags(AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG),
data_types.IncompleteListOf16BitServiceUUIDs(
[PublishedAudioCapabilitiesService.UUID]
),
]
)

View File

@@ -24,6 +24,7 @@ import struct
import sys
import bumble.logging
from bumble import data_types
from bumble.core import AdvertisingData
from bumble.device import Device
from bumble.hci import CodecID, CodingFormat, HCI_IsoDataPacket
@@ -111,23 +112,14 @@ async def main() -> None:
bytes(
AdvertisingData(
[
(
AdvertisingData.COMPLETE_LOCAL_NAME,
bytes('Bumble LE Audio', 'utf-8'),
),
(
AdvertisingData.FLAGS,
bytes(
[
data_types.CompleteLocalName('Bumble LE Audio'),
data_types.Flags(
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
| AdvertisingData.BR_EDR_HOST_FLAG
| AdvertisingData.BR_EDR_CONTROLLER_FLAG
]
),
),
(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
bytes(PublishedAudioCapabilitiesService.UUID),
data_types.IncompleteListOf16BitServiceUUIDs(
[PublishedAudioCapabilitiesService.UUID]
),
]
)

View File

@@ -24,6 +24,7 @@ from typing import Optional
import websockets
import bumble.logging
from bumble import data_types
from bumble.core import AdvertisingData
from bumble.device import AdvertisingEventProperties, AdvertisingParameters, Device
from bumble.hci import CodecID, CodingFormat, OwnAddressType
@@ -127,23 +128,14 @@ async def main() -> None:
bytes(
AdvertisingData(
[
(
AdvertisingData.COMPLETE_LOCAL_NAME,
bytes('Bumble LE Audio', 'utf-8'),
),
(
AdvertisingData.FLAGS,
bytes(
[
data_types.CompleteLocalName('Bumble LE Audio'),
data_types.Flags(
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
| AdvertisingData.BR_EDR_HOST_FLAG
| AdvertisingData.BR_EDR_CONTROLLER_FLAG
]
),
),
(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
bytes(PublishedAudioCapabilitiesService.UUID),
data_types.IncompleteListOf16BitServiceUUIDs(
[PublishedAudioCapabilitiesService.UUID]
),
]
)

View File

@@ -15,67 +15,210 @@
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
import asyncio
from __future__ import annotations
import struct
from collections.abc import Sequence
import pytest
from bumble import avc, avctp, avrcp, controller, core, device, host, link
from bumble.transport import common
from bumble import avc, avctp, avrcp
from . import test_utils
# -----------------------------------------------------------------------------
class TwoDevices:
def __init__(self):
self.connections = [None, None]
addresses = ['F0:F1:F2:F3:F4:F5', 'F5:F4:F3:F2:F1:F0']
self.link = link.LocalLink()
self.controllers = [
controller.Controller('C1', link=self.link, public_address=addresses[0]),
controller.Controller('C2', link=self.link, public_address=addresses[1]),
]
self.devices = [
device.Device(
address=addresses[0],
host=host.Host(
self.controllers[0], common.AsyncPipeSink(self.controllers[0])
),
),
device.Device(
address=addresses[1],
host=host.Host(
self.controllers[1], common.AsyncPipeSink(self.controllers[1])
),
),
]
self.devices[0].classic_enabled = True
self.devices[1].classic_enabled = True
self.connections = [None, None]
self.protocols = [None, None]
def on_connection(self, which, connection):
self.connections[which] = connection
async def setup_connections(self):
await self.devices[0].power_on()
await self.devices[1].power_on()
self.connections = await asyncio.gather(
self.devices[0].connect(
self.devices[1].public_address, core.PhysicalTransport.BR_EDR
),
self.devices[1].accept(self.devices[0].public_address),
)
class TwoDevices(test_utils.TwoDevices):
protocols: Sequence[avrcp.Protocol] = ()
async def setup_avdtp_connections(self):
self.protocols = [avrcp.Protocol(), avrcp.Protocol()]
self.protocols[0].listen(self.devices[1])
await self.protocols[1].connect(self.connections[0])
@classmethod
async def create_with_avdtp(cls) -> TwoDevices:
devices = await cls.create_with_connection()
await devices.setup_avdtp_connections()
return devices
# -----------------------------------------------------------------------------
def test_GetPlayStatusCommand():
command = avrcp.GetPlayStatusCommand()
assert avrcp.Command.from_bytes(command.pdu_id, bytes(command)) == command
# -----------------------------------------------------------------------------
def test_GetCapabilitiesCommand():
command = avrcp.GetCapabilitiesCommand(
capability_id=avrcp.GetCapabilitiesCommand.CapabilityId.COMPANY_ID
)
assert avrcp.Command.from_bytes(command.pdu_id, bytes(command)) == command
# -----------------------------------------------------------------------------
def test_SetAbsoluteVolumeCommand():
command = avrcp.SetAbsoluteVolumeCommand(volume=5)
assert avrcp.Command.from_bytes(command.pdu_id, bytes(command)) == command
# -----------------------------------------------------------------------------
def test_GetElementAttributesCommand():
command = avrcp.GetElementAttributesCommand(
identifier=999,
attribute_ids=[
avrcp.MediaAttributeId.ALBUM_NAME,
avrcp.MediaAttributeId.ARTIST_NAME,
],
)
assert avrcp.Command.from_bytes(command.pdu_id, bytes(command)) == command
# -----------------------------------------------------------------------------
def test_RegisterNotificationCommand():
command = avrcp.RegisterNotificationCommand(
event_id=avrcp.EventId.ADDRESSED_PLAYER_CHANGED, playback_interval=123
)
assert avrcp.Command.from_bytes(command.pdu_id, bytes(command)) == command
# -----------------------------------------------------------------------------
def test_UidsChangedEvent():
event = avrcp.UidsChangedEvent(uid_counter=7)
assert avrcp.Event.from_bytes(bytes(event)) == event
# -----------------------------------------------------------------------------
def test_TrackChangedEvent():
event = avrcp.TrackChangedEvent(identifier=b'12356')
assert avrcp.Event.from_bytes(bytes(event)) == event
# -----------------------------------------------------------------------------
def test_VolumeChangedEvent():
event = avrcp.VolumeChangedEvent(volume=9)
assert avrcp.Event.from_bytes(bytes(event)) == event
# -----------------------------------------------------------------------------
def test_PlaybackStatusChangedEvent():
event = avrcp.PlaybackStatusChangedEvent(play_status=avrcp.PlayStatus.PLAYING)
assert avrcp.Event.from_bytes(bytes(event)) == event
# -----------------------------------------------------------------------------
def test_AddressedPlayerChangedEvent():
event = avrcp.AddressedPlayerChangedEvent(
player=avrcp.AddressedPlayerChangedEvent.Player(player_id=9, uid_counter=10)
)
assert avrcp.Event.from_bytes(bytes(event)) == event
# -----------------------------------------------------------------------------
def test_AvailablePlayersChangedEvent():
event = avrcp.AvailablePlayersChangedEvent()
assert avrcp.Event.from_bytes(bytes(event)) == event
# -----------------------------------------------------------------------------
def test_PlaybackPositionChangedEvent():
event = avrcp.PlaybackPositionChangedEvent(playback_position=1314)
assert avrcp.Event.from_bytes(bytes(event)) == event
# -----------------------------------------------------------------------------
def test_NowPlayingContentChangedEvent():
event = avrcp.NowPlayingContentChangedEvent()
assert avrcp.Event.from_bytes(bytes(event)) == event
# -----------------------------------------------------------------------------
def test_PlayerApplicationSettingChangedEvent():
event = avrcp.PlayerApplicationSettingChangedEvent(
player_application_settings=[
avrcp.PlayerApplicationSettingChangedEvent.Setting(
avrcp.ApplicationSetting.AttributeId.REPEAT_MODE,
avrcp.ApplicationSetting.RepeatModeStatus.ALL_TRACK_REPEAT,
)
]
)
assert avrcp.Event.from_bytes(bytes(event)) == event
# -----------------------------------------------------------------------------
def test_RejectedResponse():
pdu_id = avrcp.PduId.GET_ELEMENT_ATTRIBUTES
response = avrcp.RejectedResponse(
pdu_id=pdu_id,
status_code=avrcp.StatusCode.DOES_NOT_EXIST,
)
assert (
avrcp.RejectedResponse.from_bytes(pdu=bytes(response), pdu_id=pdu_id)
== response
)
# -----------------------------------------------------------------------------
def test_GetPlayStatusResponse():
response = avrcp.GetPlayStatusResponse(
song_length=1010, song_position=13, play_status=avrcp.PlayStatus.PAUSED
)
assert avrcp.GetPlayStatusResponse.from_bytes(bytes(response)) == response
# -----------------------------------------------------------------------------
def test_NotImplementedResponse():
pdu_id = avrcp.PduId.GET_ELEMENT_ATTRIBUTES
response = avrcp.NotImplementedResponse(pdu_id=pdu_id, parameters=b'koasd')
assert (
avrcp.NotImplementedResponse.from_bytes(bytes(response), pdu_id=pdu_id)
== response
)
# -----------------------------------------------------------------------------
def test_GetCapabilitiesResponse():
response = avrcp.GetCapabilitiesResponse(
capability_id=avrcp.GetCapabilitiesCommand.CapabilityId.EVENTS_SUPPORTED,
capabilities=[
avrcp.EventId.ADDRESSED_PLAYER_CHANGED,
avrcp.EventId.BATT_STATUS_CHANGED,
],
)
assert avrcp.GetCapabilitiesResponse.from_bytes(bytes(response)) == response
# -----------------------------------------------------------------------------
def test_RegisterNotificationResponse():
response = avrcp.RegisterNotificationResponse(
event=avrcp.PlaybackPositionChangedEvent(playback_position=38)
)
assert avrcp.RegisterNotificationResponse.from_bytes(bytes(response)) == response
# -----------------------------------------------------------------------------
def test_SetAbsoluteVolumeResponse():
response = avrcp.SetAbsoluteVolumeResponse(volume=99)
assert avrcp.SetAbsoluteVolumeResponse.from_bytes(bytes(response)) == response
# -----------------------------------------------------------------------------
def test_GetElementAttributesResponse():
response = avrcp.GetElementAttributesResponse(
attributes=[
avrcp.MediaAttribute(
attribute_id=avrcp.MediaAttributeId.ALBUM_NAME,
attribute_value="White Album",
)
]
)
assert avrcp.GetElementAttributesResponse.from_bytes(bytes(response)) == response
# -----------------------------------------------------------------------------
def test_frame_parser():
with pytest.raises(ValueError) as error:
with pytest.raises(ValueError):
avc.Frame.from_bytes(bytes.fromhex("11480000"))
x = bytes.fromhex("014D0208")
@@ -217,8 +360,7 @@ def test_passthrough_commands():
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_get_supported_events():
two_devices = TwoDevices()
await two_devices.setup_connections()
two_devices = await TwoDevices.create_with_avdtp()
supported_events = await two_devices.protocols[0].get_supported_events()
assert supported_events == []

View File

@@ -16,7 +16,13 @@
# Imports
# -----------------------------------------------------------------------------
from bumble.core import UUID, AdvertisingData, Appearance, get_dict_key_by_value
from bumble.core import (
UUID,
AdvertisingData,
Appearance,
ClassOfDevice,
get_dict_key_by_value,
)
# -----------------------------------------------------------------------------
@@ -93,6 +99,24 @@ def test_appearance() -> None:
assert int(a) == 0x3333
# -----------------------------------------------------------------------------
def test_class_of_device() -> None:
c1 = ClassOfDevice(
ClassOfDevice.MajorServiceClasses.AUDIO
| ClassOfDevice.MajorServiceClasses.RENDERING,
ClassOfDevice.MajorDeviceClass.AUDIO_VIDEO,
ClassOfDevice.AudioVideoMinorDeviceClass.CAMCORDER,
)
assert str(c1) == "ClassOfDevice(RENDERING|AUDIO,AUDIO_VIDEO/CAMCORDER)"
c2 = ClassOfDevice(
ClassOfDevice.MajorServiceClasses.AUDIO,
ClassOfDevice.MajorDeviceClass.AUDIO_VIDEO,
0x123,
)
assert str(c2) == "ClassOfDevice(AUDIO,AUDIO_VIDEO/0x123)"
# -----------------------------------------------------------------------------
if __name__ == '__main__':
test_ad_data()

View File

@@ -17,6 +17,7 @@
# -----------------------------------------------------------------------------
import struct
from bumble import data_types
from bumble.core import AdvertisingData
from bumble.device import Device
from bumble.hci import HCI_Reset_Command
@@ -65,24 +66,18 @@ class HeartRateMonitor:
self.device.advertising_data = bytes(
AdvertisingData(
[
(
AdvertisingData.FLAGS,
bytes(
[
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
| AdvertisingData.BR_EDR_NOT_SUPPORTED_FLAG
]
data_types.Flags(
AdvertisingData.Flags.LE_GENERAL_DISCOVERABLE_MODE
| AdvertisingData.Flags.BR_EDR_NOT_SUPPORTED
),
data_types.CompleteLocalName('Bumble Heart'),
data_types.IncompleteListOf16BitServiceUUIDs(
[self.heart_rate_service.uuid]
),
(
AdvertisingData.COMPLETE_LOCAL_NAME,
bytes('Bumble Heart', 'utf-8'),
data_types.Appearance(
data_types.Appearance.Category.HEART_RATE_SENSOR,
data_types.Appearance.HeartRateSensorSubcategory.GENERIC_HEART_RATE_SENSOR,
),
(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
bytes(self.heart_rate_service.uuid),
),
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x0340)),
]
)
)