add support for data type classes

This commit is contained in:
Gilles Boccon-Gibod
2025-08-29 13:16:07 -07:00
parent 6f73b736d7
commit 116dc9b319
24 changed files with 1775 additions and 467 deletions

View File

@@ -104,5 +104,8 @@
"python.testing.unittestEnabled": false, "python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true, "python.testing.pytestEnabled": true,
"python-envs.defaultEnvManager": "ms-python.python:system", "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.logging
import bumble.transport import bumble.transport
import bumble.utils 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.audio import io as audio_io
from bumble.colors import color from bumble.colors import color
from bumble.profiles import bap, bass, le_audio, pbp from bumble.profiles import bap, bass, le_audio, pbp
@@ -859,20 +859,12 @@ async def run_transmit(
) )
broadcast_audio_announcement = bap.BroadcastAudioAnnouncement(broadcast_id) broadcast_audio_announcement = bap.BroadcastAudioAnnouncement(broadcast_id)
advertising_manufacturer_data = ( advertising_data_types: list[core.DataType] = [
b'' data_types.BroadcastName(broadcast_name)
if manufacturer_data is None
else bytes(
core.AdvertisingData(
[
(
core.AdvertisingData.MANUFACTURER_SPECIFIC_DATA,
struct.pack('<H', manufacturer_data[0])
+ manufacturer_data[1],
)
] ]
) if manufacturer_data is not None:
) advertising_data_types.append(
data_types.ManufacturerSpecificData(*manufacturer_data)
) )
advertising_set = await device.create_advertising_set( advertising_set = await device.create_advertising_set(
@@ -885,12 +877,7 @@ async def run_transmit(
), ),
advertising_data=( advertising_data=(
broadcast_audio_announcement.get_advertising_data() broadcast_audio_announcement.get_advertising_data()
+ bytes( + bytes(core.AdvertisingData(advertising_data_types))
core.AdvertisingData(
[(core.AdvertisingData.BROADCAST_NAME, broadcast_name.encode())]
)
)
+ advertising_manufacturer_data
), ),
periodic_advertising_parameters=bumble.device.PeriodicAdvertisingParameters( periodic_advertising_parameters=bumble.device.PeriodicAdvertisingParameters(
periodic_advertising_interval_min=80, periodic_advertising_interval_min=80,

View File

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

View File

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

View File

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

View File

@@ -17,9 +17,21 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import dataclasses
import enum import enum
import struct 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 from typing_extensions import Self
@@ -331,6 +343,9 @@ class UUID:
result += f' ({self.name})' result += f' ({self.name})'
return result return result
def __repr__(self) -> str:
return self.to_hex_str()
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Common UUID constants # 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 # fmt: off
# pylint: disable=line-too-long class MajorServiceClasses(enum.IntFlag):
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) MAJOR_SERVICE_CLASS_LABELS: ClassVar[list[str]] = [
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 = [
'Limited Discoverable Mode', 'Limited Discoverable Mode',
'LE audio', 'LE audio',
'(reserved)', '(reserved)',
@@ -477,220 +491,440 @@ class DeviceClass:
'Object Transfer', 'Object Transfer',
'Audio', 'Audio',
'Telephony', 'Telephony',
'Information' 'Information',
] ]
# Major Device Classes class MajorDeviceClass(utils.OpenIntEnum):
MISCELLANEOUS_MAJOR_DEVICE_CLASS = 0x00 MISCELLANEOUS = 0x00
COMPUTER_MAJOR_DEVICE_CLASS = 0x01 COMPUTER = 0x01
PHONE_MAJOR_DEVICE_CLASS = 0x02 PHONE = 0x02
LAN_NETWORK_ACCESS_POINT_MAJOR_DEVICE_CLASS = 0x03 LAN_NETWORK_ACCESS_POINT = 0x03
AUDIO_VIDEO_MAJOR_DEVICE_CLASS = 0x04 AUDIO_VIDEO = 0x04
PERIPHERAL_MAJOR_DEVICE_CLASS = 0x05 PERIPHERAL = 0x05
IMAGING_MAJOR_DEVICE_CLASS = 0x06 IMAGING = 0x06
WEARABLE_MAJOR_DEVICE_CLASS = 0x07 WEARABLE = 0x07
TOY_MAJOR_DEVICE_CLASS = 0x08 TOY = 0x08
HEALTH_MAJOR_DEVICE_CLASS = 0x09 HEALTH = 0x09
UNCATEGORIZED_MAJOR_DEVICE_CLASS = 0x1F UNCATEGORIZED = 0x1F
MAJOR_DEVICE_CLASS_NAMES = { MAJOR_DEVICE_CLASS_LABELS: ClassVar[dict[MajorDeviceClass, str]] = {
MISCELLANEOUS_MAJOR_DEVICE_CLASS: 'Miscellaneous', MajorDeviceClass.MISCELLANEOUS: 'Miscellaneous',
COMPUTER_MAJOR_DEVICE_CLASS: 'Computer', MajorDeviceClass.COMPUTER: 'Computer',
PHONE_MAJOR_DEVICE_CLASS: 'Phone', MajorDeviceClass.PHONE: 'Phone',
LAN_NETWORK_ACCESS_POINT_MAJOR_DEVICE_CLASS: 'LAN/Network Access Point', MajorDeviceClass.LAN_NETWORK_ACCESS_POINT: 'LAN/Network Access Point',
AUDIO_VIDEO_MAJOR_DEVICE_CLASS: 'Audio/Video', MajorDeviceClass.AUDIO_VIDEO: 'Audio/Video',
PERIPHERAL_MAJOR_DEVICE_CLASS: 'Peripheral', MajorDeviceClass.PERIPHERAL: 'Peripheral',
IMAGING_MAJOR_DEVICE_CLASS: 'Imaging', MajorDeviceClass.IMAGING: 'Imaging',
WEARABLE_MAJOR_DEVICE_CLASS: 'Wearable', MajorDeviceClass.WEARABLE: 'Wearable',
TOY_MAJOR_DEVICE_CLASS: 'Toy', MajorDeviceClass.TOY: 'Toy',
HEALTH_MAJOR_DEVICE_CLASS: 'Health', MajorDeviceClass.HEALTH: 'Health',
UNCATEGORIZED_MAJOR_DEVICE_CLASS: 'Uncategorized' MajorDeviceClass.UNCATEGORIZED: 'Uncategorized',
} }
COMPUTER_UNCATEGORIZED_MINOR_DEVICE_CLASS = 0x00 class ComputerMinorDeviceClass(utils.OpenIntEnum):
COMPUTER_DESKTOP_WORKSTATION_MINOR_DEVICE_CLASS = 0x01 UNCATEGORIZED = 0x00
COMPUTER_SERVER_CLASS_COMPUTER_MINOR_DEVICE_CLASS = 0x02 DESKTOP_WORKSTATION = 0x01
COMPUTER_LAPTOP_COMPUTER_MINOR_DEVICE_CLASS = 0x03 SERVER_CLASS_COMPUTER = 0x02
COMPUTER_HANDHELD_PC_PDA_MINOR_DEVICE_CLASS = 0x04 LAPTOP_COMPUTER = 0x03
COMPUTER_PALM_SIZE_PC_PDA_MINOR_DEVICE_CLASS = 0x05 HANDHELD_PC_PDA = 0x04
COMPUTER_WEARABLE_COMPUTER_MINOR_DEVICE_CLASS = 0x06 PALM_SIZE_PC_PDA = 0x05
COMPUTER_TABLET_MINOR_DEVICE_CLASS = 0x07 WEARABLE_COMPUTER = 0x06
TABLET = 0x07
COMPUTER_MINOR_DEVICE_CLASS_NAMES = { COMPUTER_MINOR_DEVICE_CLASS_LABELS: ClassVar[dict[ComputerMinorDeviceClass, str]] = {
COMPUTER_UNCATEGORIZED_MINOR_DEVICE_CLASS: 'Uncategorized', ComputerMinorDeviceClass.UNCATEGORIZED: 'Uncategorized',
COMPUTER_DESKTOP_WORKSTATION_MINOR_DEVICE_CLASS: 'Desktop workstation', ComputerMinorDeviceClass.DESKTOP_WORKSTATION: 'Desktop workstation',
COMPUTER_SERVER_CLASS_COMPUTER_MINOR_DEVICE_CLASS: 'Server-class computer', ComputerMinorDeviceClass.SERVER_CLASS_COMPUTER: 'Server-class computer',
COMPUTER_LAPTOP_COMPUTER_MINOR_DEVICE_CLASS: 'Laptop', ComputerMinorDeviceClass.LAPTOP_COMPUTER: 'Laptop',
COMPUTER_HANDHELD_PC_PDA_MINOR_DEVICE_CLASS: 'Handheld PC/PDA', ComputerMinorDeviceClass.HANDHELD_PC_PDA: 'Handheld PC/PDA',
COMPUTER_PALM_SIZE_PC_PDA_MINOR_DEVICE_CLASS: 'Palm-size PC/PDA', ComputerMinorDeviceClass.PALM_SIZE_PC_PDA: 'Palm-size PC/PDA',
COMPUTER_WEARABLE_COMPUTER_MINOR_DEVICE_CLASS: 'Wearable computer', ComputerMinorDeviceClass.WEARABLE_COMPUTER: 'Wearable computer',
COMPUTER_TABLET_MINOR_DEVICE_CLASS: 'Tablet' ComputerMinorDeviceClass.TABLET: 'Tablet',
} }
PHONE_UNCATEGORIZED_MINOR_DEVICE_CLASS = 0x00 class PhoneMinorDeviceClass(utils.OpenIntEnum):
PHONE_CELLULAR_MINOR_DEVICE_CLASS = 0x01 UNCATEGORIZED = 0x00
PHONE_CORDLESS_MINOR_DEVICE_CLASS = 0x02 CELLULAR = 0x01
PHONE_SMARTPHONE_MINOR_DEVICE_CLASS = 0x03 CORDLESS = 0x02
PHONE_WIRED_MODEM_OR_VOICE_GATEWAY_MINOR_DEVICE_CLASS = 0x04 SMARTPHONE = 0x03
PHONE_COMMON_ISDN_MINOR_DEVICE_CLASS = 0x05 WIRED_MODEM_OR_VOICE_GATEWAY = 0x04
COMMON_ISDN = 0x05
PHONE_MINOR_DEVICE_CLASS_NAMES = { PHONE_MINOR_DEVICE_CLASS_LABELS: ClassVar[dict[PhoneMinorDeviceClass, str]] = {
PHONE_UNCATEGORIZED_MINOR_DEVICE_CLASS: 'Uncategorized', PhoneMinorDeviceClass.UNCATEGORIZED: 'Uncategorized',
PHONE_CELLULAR_MINOR_DEVICE_CLASS: 'Cellular', PhoneMinorDeviceClass.CELLULAR: 'Cellular',
PHONE_CORDLESS_MINOR_DEVICE_CLASS: 'Cordless', PhoneMinorDeviceClass.CORDLESS: 'Cordless',
PHONE_SMARTPHONE_MINOR_DEVICE_CLASS: 'Smartphone', PhoneMinorDeviceClass.SMARTPHONE: 'Smartphone',
PHONE_WIRED_MODEM_OR_VOICE_GATEWAY_MINOR_DEVICE_CLASS: 'Wired modem or voice gateway', PhoneMinorDeviceClass.WIRED_MODEM_OR_VOICE_GATEWAY: 'Wired modem or voice gateway',
PHONE_COMMON_ISDN_MINOR_DEVICE_CLASS: 'Common ISDN access' PhoneMinorDeviceClass.COMMON_ISDN: 'Common ISDN access',
} }
AUDIO_VIDEO_UNCATEGORIZED_MINOR_DEVICE_CLASS = 0x00 class LanNetworkMinorDeviceClass(utils.OpenIntEnum):
AUDIO_VIDEO_WEARABLE_HEADSET_DEVICE_MINOR_DEVICE_CLASS = 0x01 FULLY_AVAILABLE = 0x00
AUDIO_VIDEO_HANDS_FREE_DEVICE_MINOR_DEVICE_CLASS = 0x02 _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 # (RESERVED) = 0x03
AUDIO_VIDEO_MICROPHONE_MINOR_DEVICE_CLASS = 0x04 MICROPHONE = 0x04
AUDIO_VIDEO_LOUDSPEAKER_MINOR_DEVICE_CLASS = 0x05 LOUDSPEAKER = 0x05
AUDIO_VIDEO_HEADPHONES_MINOR_DEVICE_CLASS = 0x06 HEADPHONES = 0x06
AUDIO_VIDEO_PORTABLE_AUDIO_MINOR_DEVICE_CLASS = 0x07 PORTABLE_AUDIO = 0x07
AUDIO_VIDEO_CAR_AUDIO_MINOR_DEVICE_CLASS = 0x08 CAR_AUDIO = 0x08
AUDIO_VIDEO_SET_TOP_BOX_MINOR_DEVICE_CLASS = 0x09 SET_TOP_BOX = 0x09
AUDIO_VIDEO_HIFI_AUDIO_DEVICE_MINOR_DEVICE_CLASS = 0x0A HIFI_AUDIO_DEVICE = 0x0A
AUDIO_VIDEO_VCR_MINOR_DEVICE_CLASS = 0x0B VCR = 0x0B
AUDIO_VIDEO_VIDEO_CAMERA_MINOR_DEVICE_CLASS = 0x0C VIDEO_CAMERA = 0x0C
AUDIO_VIDEO_CAMCORDER_MINOR_DEVICE_CLASS = 0x0D CAMCORDER = 0x0D
AUDIO_VIDEO_VIDEO_MONITOR_MINOR_DEVICE_CLASS = 0x0E VIDEO_MONITOR = 0x0E
AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER_MINOR_DEVICE_CLASS = 0x0F VIDEO_DISPLAY_AND_LOUDSPEAKER = 0x0F
AUDIO_VIDEO_VIDEO_CONFERENCING_MINOR_DEVICE_CLASS = 0x10 VIDEO_CONFERENCING = 0x10
# (RESERVED) = 0x11 # (RESERVED) = 0x11
AUDIO_VIDEO_GAMING_OR_TOY_MINOR_DEVICE_CLASS = 0x12 GAMING_OR_TOY = 0x12
AUDIO_VIDEO_MINOR_DEVICE_CLASS_NAMES = { AUDIO_VIDEO_MINOR_DEVICE_CLASS_LABELS: ClassVar[dict[AudioVideoMinorDeviceClass, str]] = {
AUDIO_VIDEO_UNCATEGORIZED_MINOR_DEVICE_CLASS: 'Uncategorized', AudioVideoMinorDeviceClass.UNCATEGORIZED: 'Uncategorized',
AUDIO_VIDEO_WEARABLE_HEADSET_DEVICE_MINOR_DEVICE_CLASS: 'Wearable Headset Device', AudioVideoMinorDeviceClass.WEARABLE_HEADSET_DEVICE: 'Wearable Headset Device',
AUDIO_VIDEO_HANDS_FREE_DEVICE_MINOR_DEVICE_CLASS: 'Hands-free Device', AudioVideoMinorDeviceClass.HANDS_FREE_DEVICE: 'Hands-free Device',
AUDIO_VIDEO_MICROPHONE_MINOR_DEVICE_CLASS: 'Microphone', AudioVideoMinorDeviceClass.MICROPHONE: 'Microphone',
AUDIO_VIDEO_LOUDSPEAKER_MINOR_DEVICE_CLASS: 'Loudspeaker', AudioVideoMinorDeviceClass.LOUDSPEAKER: 'Loudspeaker',
AUDIO_VIDEO_HEADPHONES_MINOR_DEVICE_CLASS: 'Headphones', AudioVideoMinorDeviceClass.HEADPHONES: 'Headphones',
AUDIO_VIDEO_PORTABLE_AUDIO_MINOR_DEVICE_CLASS: 'Portable Audio', AudioVideoMinorDeviceClass.PORTABLE_AUDIO: 'Portable Audio',
AUDIO_VIDEO_CAR_AUDIO_MINOR_DEVICE_CLASS: 'Car audio', AudioVideoMinorDeviceClass.CAR_AUDIO: 'Car audio',
AUDIO_VIDEO_SET_TOP_BOX_MINOR_DEVICE_CLASS: 'Set-top box', AudioVideoMinorDeviceClass.SET_TOP_BOX: 'Set-top box',
AUDIO_VIDEO_HIFI_AUDIO_DEVICE_MINOR_DEVICE_CLASS: 'HiFi Audio Device', AudioVideoMinorDeviceClass.HIFI_AUDIO_DEVICE: 'HiFi Audio Device',
AUDIO_VIDEO_VCR_MINOR_DEVICE_CLASS: 'VCR', AudioVideoMinorDeviceClass.VCR: 'VCR',
AUDIO_VIDEO_VIDEO_CAMERA_MINOR_DEVICE_CLASS: 'Video Camera', AudioVideoMinorDeviceClass.VIDEO_CAMERA: 'Video Camera',
AUDIO_VIDEO_CAMCORDER_MINOR_DEVICE_CLASS: 'Camcorder', AudioVideoMinorDeviceClass.CAMCORDER: 'Camcorder',
AUDIO_VIDEO_VIDEO_MONITOR_MINOR_DEVICE_CLASS: 'Video Monitor', AudioVideoMinorDeviceClass.VIDEO_MONITOR: 'Video Monitor',
AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER_MINOR_DEVICE_CLASS: 'Video Display and Loudspeaker', AudioVideoMinorDeviceClass.VIDEO_DISPLAY_AND_LOUDSPEAKER: 'Video Display and Loudspeaker',
AUDIO_VIDEO_VIDEO_CONFERENCING_MINOR_DEVICE_CLASS: 'Video Conferencing', AudioVideoMinorDeviceClass.VIDEO_CONFERENCING: 'Video Conferencing',
AUDIO_VIDEO_GAMING_OR_TOY_MINOR_DEVICE_CLASS: 'Gaming/Toy' AudioVideoMinorDeviceClass.GAMING_OR_TOY: 'Gaming/Toy',
} }
PERIPHERAL_UNCATEGORIZED_MINOR_DEVICE_CLASS = 0x00 class PeripheralMinorDeviceClass(utils.OpenIntEnum):
PERIPHERAL_KEYBOARD_MINOR_DEVICE_CLASS = 0x10 UNCATEGORIZED = 0x00
PERIPHERAL_POINTING_DEVICE_MINOR_DEVICE_CLASS = 0x20 KEYBOARD = 0x10
PERIPHERAL_COMBO_KEYBOARD_POINTING_DEVICE_MINOR_DEVICE_CLASS = 0x30 POINTING_DEVICE = 0x20
PERIPHERAL_JOYSTICK_MINOR_DEVICE_CLASS = 0x01 COMBO_KEYBOARD_POINTING_DEVICE = 0x30
PERIPHERAL_GAMEPAD_MINOR_DEVICE_CLASS = 0x02 JOYSTICK = 0x01
PERIPHERAL_REMOTE_CONTROL_MINOR_DEVICE_CLASS = 0x03 GAMEPAD = 0x02
PERIPHERAL_SENSING_DEVICE_MINOR_DEVICE_CLASS = 0x04 REMOTE_CONTROL = 0x03
PERIPHERAL_DIGITIZER_TABLET_MINOR_DEVICE_CLASS = 0x05 SENSING_DEVICE = 0x04
PERIPHERAL_CARD_READER_MINOR_DEVICE_CLASS = 0x06 DIGITIZER_TABLET = 0x05
PERIPHERAL_DIGITAL_PEN_MINOR_DEVICE_CLASS = 0x07 CARD_READER = 0x06
PERIPHERAL_HANDHELD_SCANNER_MINOR_DEVICE_CLASS = 0x08 DIGITAL_PEN = 0x07
PERIPHERAL_HANDHELD_GESTURAL_INPUT_DEVICE_MINOR_DEVICE_CLASS = 0x09 HANDHELD_SCANNER = 0x08
HANDHELD_GESTURAL_INPUT_DEVICE = 0x09
PERIPHERAL_MINOR_DEVICE_CLASS_NAMES = { PERIPHERAL_MINOR_DEVICE_CLASS_LABELS: ClassVar[dict[PeripheralMinorDeviceClass, str]] = {
PERIPHERAL_UNCATEGORIZED_MINOR_DEVICE_CLASS: 'Uncategorized', PeripheralMinorDeviceClass.UNCATEGORIZED: 'Uncategorized',
PERIPHERAL_KEYBOARD_MINOR_DEVICE_CLASS: 'Keyboard', PeripheralMinorDeviceClass.KEYBOARD: 'Keyboard',
PERIPHERAL_POINTING_DEVICE_MINOR_DEVICE_CLASS: 'Pointing device', PeripheralMinorDeviceClass.POINTING_DEVICE: 'Pointing device',
PERIPHERAL_COMBO_KEYBOARD_POINTING_DEVICE_MINOR_DEVICE_CLASS: 'Combo keyboard/pointing device', PeripheralMinorDeviceClass.COMBO_KEYBOARD_POINTING_DEVICE: 'Combo keyboard/pointing device',
PERIPHERAL_JOYSTICK_MINOR_DEVICE_CLASS: 'Joystick', PeripheralMinorDeviceClass.JOYSTICK: 'Joystick',
PERIPHERAL_GAMEPAD_MINOR_DEVICE_CLASS: 'Gamepad', PeripheralMinorDeviceClass.GAMEPAD: 'Gamepad',
PERIPHERAL_REMOTE_CONTROL_MINOR_DEVICE_CLASS: 'Remote control', PeripheralMinorDeviceClass.REMOTE_CONTROL: 'Remote control',
PERIPHERAL_SENSING_DEVICE_MINOR_DEVICE_CLASS: 'Sensing device', PeripheralMinorDeviceClass.SENSING_DEVICE: 'Sensing device',
PERIPHERAL_DIGITIZER_TABLET_MINOR_DEVICE_CLASS: 'Digitizer tablet', PeripheralMinorDeviceClass.DIGITIZER_TABLET: 'Digitizer tablet',
PERIPHERAL_CARD_READER_MINOR_DEVICE_CLASS: 'Card Reader', PeripheralMinorDeviceClass.CARD_READER: 'Card Reader',
PERIPHERAL_DIGITAL_PEN_MINOR_DEVICE_CLASS: 'Digital Pen', PeripheralMinorDeviceClass.DIGITAL_PEN: 'Digital Pen',
PERIPHERAL_HANDHELD_SCANNER_MINOR_DEVICE_CLASS: 'Handheld scanner', PeripheralMinorDeviceClass.HANDHELD_SCANNER: 'Handheld scanner',
PERIPHERAL_HANDHELD_GESTURAL_INPUT_DEVICE_MINOR_DEVICE_CLASS: 'Handheld gestural input device' PeripheralMinorDeviceClass.HANDHELD_GESTURAL_INPUT_DEVICE: 'Handheld gestural input device',
} }
WEARABLE_UNCATEGORIZED_MINOR_DEVICE_CLASS = 0x00 class WearableMinorDeviceClass(utils.OpenIntEnum):
WEARABLE_WRISTWATCH_MINOR_DEVICE_CLASS = 0x01 UNCATEGORIZED = 0x00
WEARABLE_PAGER_MINOR_DEVICE_CLASS = 0x02 WRISTWATCH = 0x01
WEARABLE_JACKET_MINOR_DEVICE_CLASS = 0x03 PAGER = 0x02
WEARABLE_HELMET_MINOR_DEVICE_CLASS = 0x04 JACKET = 0x03
WEARABLE_GLASSES_MINOR_DEVICE_CLASS = 0x05 HELMET = 0x04
GLASSES = 0x05
WEARABLE_MINOR_DEVICE_CLASS_NAMES = { WEARABLE_MINOR_DEVICE_CLASS_LABELS: ClassVar[dict[WearableMinorDeviceClass, str]] = {
WEARABLE_UNCATEGORIZED_MINOR_DEVICE_CLASS: 'Uncategorized', WearableMinorDeviceClass.UNCATEGORIZED: 'Uncategorized',
WEARABLE_WRISTWATCH_MINOR_DEVICE_CLASS: 'Wristwatch', WearableMinorDeviceClass.WRISTWATCH: 'Wristwatch',
WEARABLE_PAGER_MINOR_DEVICE_CLASS: 'Pager', WearableMinorDeviceClass.PAGER: 'Pager',
WEARABLE_JACKET_MINOR_DEVICE_CLASS: 'Jacket', WearableMinorDeviceClass.JACKET: 'Jacket',
WEARABLE_HELMET_MINOR_DEVICE_CLASS: 'Helmet', WearableMinorDeviceClass.HELMET: 'Helmet',
WEARABLE_GLASSES_MINOR_DEVICE_CLASS: 'Glasses', WearableMinorDeviceClass.GLASSES: 'Glasses',
} }
TOY_UNCATEGORIZED_MINOR_DEVICE_CLASS = 0x00 class ToyMinorDeviceClass(utils.OpenIntEnum):
TOY_ROBOT_MINOR_DEVICE_CLASS = 0x01 UNCATEGORIZED = 0x00
TOY_VEHICLE_MINOR_DEVICE_CLASS = 0x02 ROBOT = 0x01
TOY_DOLL_ACTION_FIGURE_MINOR_DEVICE_CLASS = 0x03 VEHICLE = 0x02
TOY_CONTROLLER_MINOR_DEVICE_CLASS = 0x04 DOLL_ACTION_FIGURE = 0x03
TOY_GAME_MINOR_DEVICE_CLASS = 0x05 CONTROLLER = 0x04
GAME = 0x05
TOY_MINOR_DEVICE_CLASS_NAMES = { TOY_MINOR_DEVICE_CLASS_LABELS: ClassVar[dict[ToyMinorDeviceClass, str]] = {
TOY_UNCATEGORIZED_MINOR_DEVICE_CLASS: 'Uncategorized', ToyMinorDeviceClass.UNCATEGORIZED: 'Uncategorized',
TOY_ROBOT_MINOR_DEVICE_CLASS: 'Robot', ToyMinorDeviceClass.ROBOT: 'Robot',
TOY_VEHICLE_MINOR_DEVICE_CLASS: 'Vehicle', ToyMinorDeviceClass.VEHICLE: 'Vehicle',
TOY_DOLL_ACTION_FIGURE_MINOR_DEVICE_CLASS: 'Doll/Action figure', ToyMinorDeviceClass.DOLL_ACTION_FIGURE: 'Doll/Action figure',
TOY_CONTROLLER_MINOR_DEVICE_CLASS: 'Controller', ToyMinorDeviceClass.CONTROLLER: 'Controller',
TOY_GAME_MINOR_DEVICE_CLASS: 'Game', ToyMinorDeviceClass.GAME: 'Game',
} }
HEALTH_UNDEFINED_MINOR_DEVICE_CLASS = 0x00 class HealthMinorDeviceClass(utils.OpenIntEnum):
HEALTH_BLOOD_PRESSURE_MONITOR_MINOR_DEVICE_CLASS = 0x01 UNDEFINED = 0x00
HEALTH_THERMOMETER_MINOR_DEVICE_CLASS = 0x02 BLOOD_PRESSURE_MONITOR = 0x01
HEALTH_WEIGHING_SCALE_MINOR_DEVICE_CLASS = 0x03 THERMOMETER = 0x02
HEALTH_GLUCOSE_METER_MINOR_DEVICE_CLASS = 0x04 WEIGHING_SCALE = 0x03
HEALTH_PULSE_OXIMETER_MINOR_DEVICE_CLASS = 0x05 GLUCOSE_METER = 0x04
HEALTH_HEART_PULSE_RATE_MONITOR_MINOR_DEVICE_CLASS = 0x06 PULSE_OXIMETER = 0x05
HEALTH_HEALTH_DATA_DISPLAY_MINOR_DEVICE_CLASS = 0x07 HEART_PULSE_RATE_MONITOR = 0x06
HEALTH_STEP_COUNTER_MINOR_DEVICE_CLASS = 0x08 HEALTH_DATA_DISPLAY = 0x07
HEALTH_BODY_COMPOSITION_ANALYZER_MINOR_DEVICE_CLASS = 0x09 STEP_COUNTER = 0x08
HEALTH_PEAK_FLOW_MONITOR_MINOR_DEVICE_CLASS = 0x0A BODY_COMPOSITION_ANALYZER = 0x09
HEALTH_MEDICATION_MONITOR_MINOR_DEVICE_CLASS = 0x0B PEAK_FLOW_MONITOR = 0x0A
HEALTH_KNEE_PROSTHESIS_MINOR_DEVICE_CLASS = 0x0C MEDICATION_MONITOR = 0x0B
HEALTH_ANKLE_PROSTHESIS_MINOR_DEVICE_CLASS = 0x0D KNEE_PROSTHESIS = 0x0C
HEALTH_GENERIC_HEALTH_MANAGER_MINOR_DEVICE_CLASS = 0x0E ANKLE_PROSTHESIS = 0x0D
HEALTH_PERSONAL_MOBILITY_DEVICE_MINOR_DEVICE_CLASS = 0x0F GENERIC_HEALTH_MANAGER = 0x0E
PERSONAL_MOBILITY_DEVICE = 0x0F
HEALTH_MINOR_DEVICE_CLASS_NAMES = { HEALTH_MINOR_DEVICE_CLASS_LABELS: ClassVar[dict[HealthMinorDeviceClass, str]] = {
HEALTH_UNDEFINED_MINOR_DEVICE_CLASS: 'Undefined', HealthMinorDeviceClass.UNDEFINED: 'Undefined',
HEALTH_BLOOD_PRESSURE_MONITOR_MINOR_DEVICE_CLASS: 'Blood Pressure Monitor', HealthMinorDeviceClass.BLOOD_PRESSURE_MONITOR: 'Blood Pressure Monitor',
HEALTH_THERMOMETER_MINOR_DEVICE_CLASS: 'Thermometer', HealthMinorDeviceClass.THERMOMETER: 'Thermometer',
HEALTH_WEIGHING_SCALE_MINOR_DEVICE_CLASS: 'Weighing Scale', HealthMinorDeviceClass.WEIGHING_SCALE: 'Weighing Scale',
HEALTH_GLUCOSE_METER_MINOR_DEVICE_CLASS: 'Glucose Meter', HealthMinorDeviceClass.GLUCOSE_METER: 'Glucose Meter',
HEALTH_PULSE_OXIMETER_MINOR_DEVICE_CLASS: 'Pulse Oximeter', HealthMinorDeviceClass.PULSE_OXIMETER: 'Pulse Oximeter',
HEALTH_HEART_PULSE_RATE_MONITOR_MINOR_DEVICE_CLASS: 'Heart/Pulse Rate Monitor', HealthMinorDeviceClass.HEART_PULSE_RATE_MONITOR: 'Heart/Pulse Rate Monitor',
HEALTH_HEALTH_DATA_DISPLAY_MINOR_DEVICE_CLASS: 'Health Data Display', HealthMinorDeviceClass.HEALTH_DATA_DISPLAY: 'Health Data Display',
HEALTH_STEP_COUNTER_MINOR_DEVICE_CLASS: 'Step Counter', HealthMinorDeviceClass.STEP_COUNTER: 'Step Counter',
HEALTH_BODY_COMPOSITION_ANALYZER_MINOR_DEVICE_CLASS: 'Body Composition Analyzer', HealthMinorDeviceClass.BODY_COMPOSITION_ANALYZER: 'Body Composition Analyzer',
HEALTH_PEAK_FLOW_MONITOR_MINOR_DEVICE_CLASS: 'Peak Flow Monitor', HealthMinorDeviceClass.PEAK_FLOW_MONITOR: 'Peak Flow Monitor',
HEALTH_MEDICATION_MONITOR_MINOR_DEVICE_CLASS: 'Medication Monitor', HealthMinorDeviceClass.MEDICATION_MONITOR: 'Medication Monitor',
HEALTH_KNEE_PROSTHESIS_MINOR_DEVICE_CLASS: 'Knee Prosthesis', HealthMinorDeviceClass.KNEE_PROSTHESIS: 'Knee Prosthesis',
HEALTH_ANKLE_PROSTHESIS_MINOR_DEVICE_CLASS: 'Ankle Prosthesis', HealthMinorDeviceClass.ANKLE_PROSTHESIS: 'Ankle Prosthesis',
HEALTH_GENERIC_HEALTH_MANAGER_MINOR_DEVICE_CLASS: 'Generic Health Manager', HealthMinorDeviceClass.GENERIC_HEALTH_MANAGER: 'Generic Health Manager',
HEALTH_PERSONAL_MOBILITY_DEVICE_MINOR_DEVICE_CLASS: 'Personal Mobility Device', HealthMinorDeviceClass.PERSONAL_MOBILITY_DEVICE: 'Personal Mobility Device',
} }
MINOR_DEVICE_CLASS_NAMES = { MINOR_DEVICE_CLASS_LABELS: ClassVar[dict[MajorDeviceClass, dict[Any, str]]] = {
COMPUTER_MAJOR_DEVICE_CLASS: COMPUTER_MINOR_DEVICE_CLASS_NAMES, MajorDeviceClass.COMPUTER: COMPUTER_MINOR_DEVICE_CLASS_LABELS,
PHONE_MAJOR_DEVICE_CLASS: PHONE_MINOR_DEVICE_CLASS_NAMES, MajorDeviceClass.PHONE: PHONE_MINOR_DEVICE_CLASS_LABELS,
AUDIO_VIDEO_MAJOR_DEVICE_CLASS: AUDIO_VIDEO_MINOR_DEVICE_CLASS_NAMES, MajorDeviceClass.LAN_NETWORK_ACCESS_POINT: LAN_NETWORK_MINOR_DEVICE_CLASS_LABELS,
PERIPHERAL_MAJOR_DEVICE_CLASS: PERIPHERAL_MINOR_DEVICE_CLASS_NAMES, MajorDeviceClass.AUDIO_VIDEO: AUDIO_VIDEO_MINOR_DEVICE_CLASS_LABELS,
WEARABLE_MAJOR_DEVICE_CLASS: WEARABLE_MINOR_DEVICE_CLASS_NAMES, MajorDeviceClass.PERIPHERAL: PERIPHERAL_MINOR_DEVICE_CLASS_LABELS,
TOY_MAJOR_DEVICE_CLASS: TOY_MINOR_DEVICE_CLASS_NAMES, MajorDeviceClass.WEARABLE: WEARABLE_MINOR_DEVICE_CLASS_LABELS,
HEALTH_MAJOR_DEVICE_CLASS: HEALTH_MINOR_DEVICE_CLASS_NAMES, 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.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 # fmt: on
# pylint: enable=line-too-long # pylint: enable=line-too-long
@@ -711,16 +945,16 @@ class DeviceClass:
@staticmethod @staticmethod
def service_class_labels(service_class_flags): def service_class_labels(service_class_flags):
return bit_flags_to_strings( return bit_flags_to_strings(
service_class_flags, DeviceClass.SERVICE_CLASS_LABELS service_class_flags, ClassOfDevice.MAJOR_SERVICE_CLASS_LABELS
) )
@staticmethod @staticmethod
def major_device_class_name(device_class): 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 @staticmethod
def minor_device_class_name(major_device_class, minor_device_class): 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: if class_names is None:
return f'#{minor_device_class:02X}' return f'#{minor_device_class:02X}'
return name_or_number(class_names, minor_device_class) return name_or_number(class_names, minor_device_class)
@@ -1255,6 +1489,10 @@ class Appearance:
category = cls.Category(appearance >> 6) category = cls.Category(appearance >> 6)
return cls(category, appearance & 0x3F) 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: def __init__(self, category: Category, subcategory: int) -> None:
self.category = category self.category = category
if subcategory_class := self.SUBCATEGORY_CLASSES.get(category): if subcategory_class := self.SUBCATEGORY_CLASSES.get(category):
@@ -1265,6 +1503,9 @@ class Appearance:
def __int__(self) -> int: def __int__(self) -> int:
return self.category << 6 | self.subcategory return self.category << 6 | self.subcategory
def __bytes__(self) -> bytes:
return int(self).to_bytes(2, byteorder="little")
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
'Appearance(' 'Appearance('
@@ -1276,6 +1517,61 @@ class Appearance:
def __str__(self) -> str: def __str__(self) -> str:
return f'{self.category.name}/{self.subcategory.name}' 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 # Advertising Data
@@ -1419,15 +1715,25 @@ class AdvertisingData:
BR_EDR_CONTROLLER_FLAG = Flags.SIMULTANEOUS_LE_BR_EDR_CAPABLE BR_EDR_CONTROLLER_FLAG = Flags.SIMULTANEOUS_LE_BR_EDR_CAPABLE
BR_EDR_HOST_FLAG = 0x10 # Deprecated BR_EDR_HOST_FLAG = 0x10 # Deprecated
ad_structures: list[tuple[int, bytes]] ad_structures: list[tuple[AdvertisingData.Type, bytes]]
# fmt: on # fmt: on
# pylint: enable=line-too-long # 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: if ad_structures is None:
ad_structures = [] 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 @classmethod
def from_bytes(cls, data: bytes) -> AdvertisingData: def from_bytes(cls, data: bytes) -> AdvertisingData:
@@ -1444,8 +1750,7 @@ class AdvertisingData:
'LE Limited Discoverable Mode', 'LE Limited Discoverable Mode',
'LE General Discoverable Mode', 'LE General Discoverable Mode',
'BR/EDR Not Supported', 'BR/EDR Not Supported',
'Simultaneous LE and BR/EDR (Controller)', 'Simultaneous LE and BR/EDR',
'Simultaneous LE and BR/EDR (Host)',
] ]
) )
return ', '.join(bit_flags_to_strings(flags, flag_names)) return ', '.join(bit_flags_to_strings(flags, flag_names))
@@ -1604,7 +1909,7 @@ class AdvertisingData:
if length > 0: if length > 0:
ad_type = data[offset] ad_type = data[offset]
ad_data = data[offset + 1 : offset + length] 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 offset += length
@overload @overload
@@ -1623,6 +1928,7 @@ class AdvertisingData:
], ],
raw: Literal[False] = False, raw: Literal[False] = False,
) -> list[list[UUID]]: ... ) -> list[list[UUID]]: ...
@overload @overload
def get_all( def get_all(
self, self,
@@ -1633,6 +1939,7 @@ class AdvertisingData:
], ],
raw: Literal[False] = False, raw: Literal[False] = False,
) -> list[tuple[UUID, bytes]]: ... ) -> list[tuple[UUID, bytes]]: ...
@overload @overload
def get_all( def get_all(
self, self,
@@ -1644,6 +1951,7 @@ class AdvertisingData:
], ],
raw: Literal[False] = False, raw: Literal[False] = False,
) -> list[str]: ... ) -> list[str]: ...
@overload @overload
def get_all( def get_all(
self, self,
@@ -1655,26 +1963,31 @@ class AdvertisingData:
], ],
raw: Literal[False] = False, raw: Literal[False] = False,
) -> list[int]: ... ) -> list[int]: ...
@overload @overload
def get_all( def get_all(
self, self,
type_id: Literal[AdvertisingData.Type.PERIPHERAL_CONNECTION_INTERVAL_RANGE,], type_id: Literal[AdvertisingData.Type.PERIPHERAL_CONNECTION_INTERVAL_RANGE,],
raw: Literal[False] = False, raw: Literal[False] = False,
) -> list[tuple[int, int]]: ... ) -> list[tuple[int, int]]: ...
@overload @overload
def get_all( def get_all(
self, self,
type_id: Literal[AdvertisingData.Type.MANUFACTURER_SPECIFIC_DATA,], type_id: Literal[AdvertisingData.Type.MANUFACTURER_SPECIFIC_DATA,],
raw: Literal[False] = False, raw: Literal[False] = False,
) -> list[tuple[int, bytes]]: ... ) -> list[tuple[int, bytes]]: ...
@overload @overload
def get_all( def get_all(
self, self,
type_id: Literal[AdvertisingData.Type.APPEARANCE,], type_id: Literal[AdvertisingData.Type.APPEARANCE,],
raw: Literal[False] = False, raw: Literal[False] = False,
) -> list[Appearance]: ... ) -> list[Appearance]: ...
@overload @overload
def get_all(self, type_id: int, raw: Literal[True]) -> list[bytes]: ... def get_all(self, type_id: int, raw: Literal[True]) -> list[bytes]: ...
@overload @overload
def get_all( def get_all(
self, type_id: int, raw: bool = False 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] 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. Returns a (possibly empty) list of matches.
''' '''
@@ -1708,6 +2021,7 @@ class AdvertisingData:
], ],
raw: Literal[False] = False, raw: Literal[False] = False,
) -> Optional[list[UUID]]: ... ) -> Optional[list[UUID]]: ...
@overload @overload
def get( def get(
self, self,
@@ -1718,6 +2032,7 @@ class AdvertisingData:
], ],
raw: Literal[False] = False, raw: Literal[False] = False,
) -> Optional[tuple[UUID, bytes]]: ... ) -> Optional[tuple[UUID, bytes]]: ...
@overload @overload
def get( def get(
self, self,
@@ -1729,6 +2044,7 @@ class AdvertisingData:
], ],
raw: Literal[False] = False, raw: Literal[False] = False,
) -> Optional[Optional[str]]: ... ) -> Optional[Optional[str]]: ...
@overload @overload
def get( def get(
self, self,
@@ -1740,26 +2056,31 @@ class AdvertisingData:
], ],
raw: Literal[False] = False, raw: Literal[False] = False,
) -> Optional[int]: ... ) -> Optional[int]: ...
@overload @overload
def get( def get(
self, self,
type_id: Literal[AdvertisingData.Type.PERIPHERAL_CONNECTION_INTERVAL_RANGE,], type_id: Literal[AdvertisingData.Type.PERIPHERAL_CONNECTION_INTERVAL_RANGE,],
raw: Literal[False] = False, raw: Literal[False] = False,
) -> Optional[tuple[int, int]]: ... ) -> Optional[tuple[int, int]]: ...
@overload @overload
def get( def get(
self, self,
type_id: Literal[AdvertisingData.Type.MANUFACTURER_SPECIFIC_DATA,], type_id: Literal[AdvertisingData.Type.MANUFACTURER_SPECIFIC_DATA,],
raw: Literal[False] = False, raw: Literal[False] = False,
) -> Optional[tuple[int, bytes]]: ... ) -> Optional[tuple[int, bytes]]: ...
@overload @overload
def get( def get(
self, self,
type_id: Literal[AdvertisingData.Type.APPEARANCE,], type_id: Literal[AdvertisingData.Type.APPEARANCE,],
raw: Literal[False] = False, raw: Literal[False] = False,
) -> Optional[Appearance]: ... ) -> Optional[Appearance]: ...
@overload @overload
def get(self, type_id: int, raw: Literal[True]) -> Optional[bytes]: ... def get(self, type_id: int, raw: Literal[True]) -> Optional[bytes]: ...
@overload @overload
def get( def get(
self, type_id: int, raw: bool = False self, type_id: int, raw: bool = False
@@ -1767,7 +2088,7 @@ class AdvertisingData:
def get(self, type_id: int, raw: bool = False) -> Optional[AdvertisingDataObject]: 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. Returns the first entry, or None if no structure matches.
''' '''
@@ -1822,7 +2143,22 @@ class ConnectionPHY:
# LE Role # LE Role
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class LeRole(enum.IntEnum): class LeRole(enum.IntEnum):
# fmt: off
PERIPHERAL_ONLY = 0x00 PERIPHERAL_ONLY = 0x00
CENTRAL_ONLY = 0x01 CENTRAL_ONLY = 0x01
BOTH_PERIPHERAL_PREFERRED = 0x02 BOTH_PERIPHERAL_PREFERRED = 0x02
BOTH_CENTRAL_PREFERRED = 0x03 BOTH_CENTRAL_PREFERRED = 0x03
# -----------------------------------------------------------------------------
# Security Manager OOB Flag
# -----------------------------------------------------------------------------
class SecurityManagerOutOfBandFlag(enum.IntFlag):
"""
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

1018
bumble/data_types.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -45,7 +45,18 @@ from typing import (
from typing_extensions import Self from typing_extensions import Self
from bumble import core, gatt_client, gatt_server, hci, l2cap, pairing, sdp, smp, utils from bumble import (
core,
data_types,
gatt_client,
gatt_server,
hci,
l2cap,
pairing,
sdp,
smp,
utils,
)
from bumble.att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU from bumble.att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU
from bumble.colors import color from bumble.colors import color
from bumble.core import ( from bumble.core import (
@@ -2049,9 +2060,7 @@ class DeviceConfiguration:
connectable: bool = True connectable: bool = True
discoverable: bool = True discoverable: bool = True
advertising_data: bytes = bytes( advertising_data: bytes = bytes(
AdvertisingData( AdvertisingData([data_types.CompleteLocalName(DEVICE_DEFAULT_NAME)])
[(AdvertisingData.COMPLETE_LOCAL_NAME, bytes(DEVICE_DEFAULT_NAME, 'utf-8'))]
)
) )
irk: bytes = bytes(16) # This really must be changed for any level of security irk: bytes = bytes(16) # This really must be changed for any level of security
keystore: Optional[str] = None keystore: Optional[str] = None
@@ -2095,9 +2104,7 @@ class DeviceConfiguration:
self.advertising_data = bytes.fromhex(advertising_data) self.advertising_data = bytes.fromhex(advertising_data)
elif name is not None: elif name is not None:
self.advertising_data = bytes( self.advertising_data = bytes(
AdvertisingData( AdvertisingData([data_types.CompleteLocalName(self.name)])
[(AdvertisingData.COMPLETE_LOCAL_NAME, bytes(self.name, 'utf-8'))]
)
) )
# Load scan response data # Load scan response data
@@ -3544,14 +3551,7 @@ class Device(utils.CompositeEventEmitter):
# Synthesize an inquiry response if none is set already # Synthesize an inquiry response if none is set already
if self.inquiry_response is None: if self.inquiry_response is None:
self.inquiry_response = bytes( self.inquiry_response = bytes(
AdvertisingData( AdvertisingData([data_types.CompleteLocalName(self.name)])
[
(
AdvertisingData.COMPLETE_LOCAL_NAME,
bytes(self.name, 'utf-8'),
)
]
)
) )
# Update the controller # Update the controller

View File

@@ -2135,6 +2135,7 @@ class Address:
if len(address) == 12 + 5: if len(address) == 12 + 5:
# Form with ':' separators # Form with ':' separators
address = address.replace(':', '') address = address.replace(':', '')
self.address_bytes = bytes(reversed(bytes.fromhex(address))) self.address_bytes = bytes(reversed(bytes.fromhex(address)))
if len(self.address_bytes) != 6: if len(self.address_bytes) != 6:

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,7 @@ import struct
import sys import sys
import bumble.logging import bumble.logging
from bumble import data_types
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import Device from bumble.device import Device
from bumble.profiles.device_information_service import DeviceInformationService from bumble.profiles.device_information_service import DeviceInformationService
@@ -53,11 +54,11 @@ async def main() -> None:
device.advertising_data = bytes( device.advertising_data = bytes(
AdvertisingData( AdvertisingData(
[ [
( data_types.CompleteLocalName('Bumble Device'),
AdvertisingData.COMPLETE_LOCAL_NAME, data_types.Appearance(
bytes('Bumble Device', 'utf-8'), 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 time
import bumble.logging import bumble.logging
from bumble import data_types
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import Device from bumble.device import Device
from bumble.profiles.device_information_service import DeviceInformationService from bumble.profiles.device_information_service import DeviceInformationService
@@ -88,15 +89,14 @@ async def main() -> None:
device.advertising_data = bytes( device.advertising_data = bytes(
AdvertisingData( AdvertisingData(
[ [
( data_types.CompleteLocalName('Bumble Heart'),
AdvertisingData.COMPLETE_LOCAL_NAME, data_types.IncompleteListOf16BitServiceUUIDs(
bytes('Bumble Heart', 'utf-8'), [heart_rate_service.uuid]
), ),
( data_types.Appearance(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, data_types.Appearance.Category.HEART_RATE_SENSOR,
bytes(heart_rate_service.uuid), 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 websockets
import bumble.logging import bumble.logging
from bumble import data_types
from bumble.colors import color from bumble.colors import color
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import Connection, Device, Peer from bumble.device import Connection, Device, Peer
@@ -341,16 +342,18 @@ async def keyboard_device(device, command):
device.advertising_data = bytes( device.advertising_data = bytes(
AdvertisingData( AdvertisingData(
[ [
( data_types.CompleteLocalName('Bumble Keyboard'),
AdvertisingData.COMPLETE_LOCAL_NAME, data_types.IncompleteListOf16BitServiceUUIDs(
bytes('Bumble Keyboard', 'utf-8'), [GATT_HUMAN_INTERFACE_DEVICE_SERVICE]
), ),
( data_types.Appearance(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, data_types.Appearance.Category.HUMAN_INTERFACE_DEVICE,
bytes(GATT_HUMAN_INTERFACE_DEVICE_SERVICE), 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 sys
import bumble.logging import bumble.logging
from bumble import data_types
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import AdvertisingType, Device from bumble.device import AdvertisingType, Device
from bumble.hci import Address from bumble.hci import Address
@@ -60,7 +61,10 @@ async def main() -> None:
device.scan_response_data = bytes( device.scan_response_data = bytes(
AdvertisingData( 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 websockets
import bumble.logging import bumble.logging
from bumble import decoder, gatt from bumble import data_types, decoder, gatt
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import AdvertisingParameters, Device from bumble.device import AdvertisingParameters, Device
from bumble.profiles import asha from bumble.profiles import asha
@@ -78,14 +78,10 @@ async def main() -> None:
bytes( bytes(
AdvertisingData( AdvertisingData(
[ [
( data_types.CompleteLocalName(device.name),
AdvertisingData.COMPLETE_LOCAL_NAME, data_types.Flags(AdvertisingData.Flags(0x06)),
bytes(device.name, 'utf-8'), data_types.IncompleteListOf16BitServiceUUIDs(
), [gatt.GATT_ASHA_SERVICE]
(AdvertisingData.FLAGS, bytes([0x06])),
(
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
bytes(gatt.GATT_ASHA_SERVICE),
), ),
] ]
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,13 @@
# Imports # 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 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__': if __name__ == '__main__':
test_ad_data() test_ad_data()

View File

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