From 116dc9b319ca91245ccc9749a196bebe73dbeb29 Mon Sep 17 00:00:00 2001 From: Gilles Boccon-Gibod Date: Fri, 29 Aug 2025 13:16:07 -0700 Subject: [PATCH] add support for data type classes --- .vscode/settings.json | 5 +- apps/auracast.py | 29 +- apps/lea_unicast/app.py | 23 +- apps/pair.py | 41 +- apps/scan.py | 12 +- bumble/core.py | 808 ++++++++++---- bumble/data_types.py | 1018 ++++++++++++++++++ bumble/device.py | 30 +- bumble/hci.py | 1 + bumble/profiles/asha.py | 11 +- bumble/profiles/bap.py | 25 +- examples/battery_server.py | 14 +- examples/device_information_server.py | 9 +- examples/heart_rate_server.py | 14 +- examples/keyboard.py | 19 +- examples/run_advertiser.py | 6 +- examples/run_asha_sink.py | 14 +- examples/run_csis_servers.py | 24 +- examples/run_hap_server.py | 24 +- examples/run_mcp_client.py | 16 +- examples/run_unicast_server.py | 24 +- examples/run_vcp_renderer.py | 24 +- tests/core_test.py | 26 +- web/heart_rate_monitor/heart_rate_monitor.py | 25 +- 24 files changed, 1775 insertions(+), 467 deletions(-) create mode 100644 bumble/data_types.py diff --git a/.vscode/settings.json b/.vscode/settings.json index c6696ab..a68eba8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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" + ] } diff --git a/apps/auracast.py b/apps/auracast.py index 19bff3d..1e7413c 100644 --- a/apps/auracast.py +++ b/apps/auracast.py @@ -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,21 +859,13 @@ 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('>> {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): diff --git a/bumble/core.py b/bumble/core.py index d7c6cc9..0cb9b0b 100644 --- a/bumble/core.py +++ b/bumble/core.py @@ -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(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) - 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,219 +491,439 @@ class DeviceClass: 'Object Transfer', 'Audio', 'Telephony', - 'Information' + 'Information', ] + 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_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', + } + + 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_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', + } + + 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_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', + } + + 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 + 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 + GAMING_OR_TOY = 0x12 + + 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', + } + + 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_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', + } + + class WearableMinorDeviceClass(utils.OpenIntEnum): + UNCATEGORIZED = 0x00 + WRISTWATCH = 0x01 + PAGER = 0x02 + JACKET = 0x03 + HELMET = 0x04 + GLASSES = 0x05 + + 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', + } + + class ToyMinorDeviceClass(utils.OpenIntEnum): + UNCATEGORIZED = 0x00 + ROBOT = 0x01 + VEHICLE = 0x02 + DOLL_ACTION_FIGURE = 0x03 + CONTROLLER = 0x04 + GAME = 0x05 + + 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', + } + + 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_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_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.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 = 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 + 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 - 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' - } + 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 - 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 + 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 - 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' - } + 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 - 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 + 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 - 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' - } + 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 - 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 - # (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 - # (RESERVED) = 0x11 - AUDIO_VIDEO_GAMING_OR_TOY_MINOR_DEVICE_CLASS = 0x12 + 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 - 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' - } - - 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 - - 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' - } - - 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 - - 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', - } - - 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 - - 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', - } - - 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 - - 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', - } - - 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, - } + 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 @@ -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): - PERIPHERAL_ONLY = 0x00 - CENTRAL_ONLY = 0x01 + # fmt: off + PERIPHERAL_ONLY = 0x00 + CENTRAL_ONLY = 0x01 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 diff --git a/bumble/data_types.py b/bumble/data_types.py new file mode 100644 index 0000000..cc896e0 --- /dev/null +++ b/bumble/data_types.py @@ -0,0 +1,1018 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Classes representing "Data Types" defined in +"Supplement to the Bluetooth Core Specification", Part A and +"Assigned Numbers", 2.3 Common Data Types. +""" + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +from __future__ import annotations + +import dataclasses +import math +import struct +from typing import Any, ClassVar, Literal, Optional, TypeVar, Union, overload + +from typing_extensions import Self + +from bumble import company_ids, core, hci + + +# ----------------------------------------------------------------------------- +class GenericAdvertisingData(core.DataType): + """Data Type for which there is no specific subclass""" + + label = "Generic Advertising Data" + ad_data: bytes + + def __init__(self, ad_data: bytes, ad_type: core.AdvertisingData.Type) -> None: + self.ad_data = ad_data + self.ad_type = ad_type + + def value_string(self) -> str: + return f"type={self.ad_type.name}, data={self.ad_data.hex().upper()}" + + @classmethod + def from_bytes( + cls, + ad_data: bytes, + ad_type: core.AdvertisingData.Type = core.AdvertisingData.Type(0), + ) -> GenericAdvertisingData: + return cls(ad_data, ad_type) + + def __bytes__(self) -> bytes: + return self.ad_data + + def __eq__(self, other: Any) -> bool: + return ( + isinstance(other, GenericAdvertisingData) + and self.ad_type == other.ad_type + and self.ad_data == other.ad_data + ) + + +@dataclasses.dataclass +class ListOfServiceUUIDs(core.DataType): + """Base class for complete or incomplete lists of UUIDs.""" + + _uuid_size: ClassVar[int] = 0 + uuids: list[core.UUID] + + @classmethod + def from_bytes(cls, data: bytes) -> ListOfServiceUUIDs: + return cls( + [ + core.UUID.from_bytes(data[x : x + cls._uuid_size]) + for x in range(0, len(data), cls._uuid_size) + ] + ) + + def __post_init__(self) -> None: + for uuid in self.uuids: + if len(uuid.uuid_bytes) != self._uuid_size: + raise TypeError("incompatible UUID type") + + def __bytes__(self) -> bytes: + return b"".join(bytes(uuid) for uuid in self.uuids) + + def value_string(self) -> str: + return ", ".join(list(map(str, self.uuids))) + + +class IncompleteListOf16BitServiceUUIDs(ListOfServiceUUIDs): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.1 SERVICE OR SERVICE CLASS UUID + """ + + _uuid_size = 2 + label = "Incomplete List Of 16-bit Service or Service Class UUIDs" + ad_type = core.AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS + + +class CompleteListOf16BitServiceUUIDs(ListOfServiceUUIDs): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.1 SERVICE OR SERVICE CLASS UUID + """ + + _uuid_size = 2 + label = "Complete List Of 16-bit Service or Service Class UUIDs" + ad_type = core.AdvertisingData.COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS + + +class IncompleteListOf32BitServiceUUIDs(ListOfServiceUUIDs): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.1 SERVICE OR SERVICE CLASS UUID + """ + + _uuid_size = 4 + label = "Incomplete List Of 32-bit Service or Service Class UUIDs" + ad_type = core.AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS + + +class CompleteListOf32BitServiceUUIDs(ListOfServiceUUIDs): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.1 SERVICE OR SERVICE CLASS UUID + """ + + _uuid_size = 4 + label = "Complete List Of 32-bit Service or Service Class UUIDs" + ad_type = core.AdvertisingData.COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS + + +class IncompleteListOf128BitServiceUUIDs(ListOfServiceUUIDs): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.1 SERVICE OR SERVICE CLASS UUID + """ + + _uuid_size = 16 + label = "Incomplete List Of 128-bit Service or Service Class UUIDs" + ad_type = core.AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS + + +class CompleteListOf128BitServiceUUIDs(ListOfServiceUUIDs): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.1 SERVICE OR SERVICE CLASS UUID + """ + + _uuid_size = 16 + label = "Complete List Of 128-bit Service or Service Class UUIDs" + ad_type = core.AdvertisingData.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS + + +class StringDataType(str, core.DataType): + @classmethod + def from_bytes(cls, data: bytes) -> Self: + return cls(data.decode("utf-8")) + + def __bytes__(self) -> bytes: + return self.encode("utf-8") + + def __str__(self) -> str: + return core.DataType.__str__(self) + + def value_string(self) -> str: + return repr(self) + + +class CompleteLocalName(StringDataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.2 LOCAL NAME + """ + + label = "Complete Local Name" + ad_type = core.AdvertisingData.COMPLETE_LOCAL_NAME + + +class ShortenedLocalName(StringDataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.2 LOCAL NAME + """ + + label = "Shortened Local Name" + ad_type = core.AdvertisingData.SHORTENED_LOCAL_NAME + + +class Flags(int, core.DataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.3 FLAGS + """ + + label = "Flags" + ad_type = core.AdvertisingData.FLAGS + + def __init__(self, flags: core.AdvertisingData.Flags) -> None: + pass + + @classmethod + def from_bytes(cls, data: bytes) -> Flags: # type: ignore[override] + return cls(core.AdvertisingData.Flags(int.from_bytes(data, byteorder="little"))) + + def __bytes__(self) -> bytes: + bytes_length = 1 if self == 0 else math.ceil(self.bit_length() / 8) + return self.to_bytes(length=bytes_length, byteorder="little") + + def value_string(self) -> str: + return core.AdvertisingData.Flags(self).name or "" + + +@dataclasses.dataclass +class ManufacturerSpecificData(core.DataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.4 MANUFACTURER SPECIFIC DATA + """ + + label = "Manufacturer Specific Data" + ad_type = core.AdvertisingData.Type.MANUFACTURER_SPECIFIC_DATA + + company_identifier: int + data: bytes + + @classmethod + def from_bytes(cls, data: bytes) -> ManufacturerSpecificData: + company_identifier = int.from_bytes(data[:2], "little") + return cls(company_identifier, data[2:]) + + def __bytes__(self) -> bytes: + return self.company_identifier.to_bytes(2, "little") + self.data + + def value_string(self) -> str: + if company := company_ids.COMPANY_IDENTIFIERS.get(self.company_identifier): + company_str = repr(company) + else: + company_str = f'0x{self.company_identifier:04X}' + return f"company={company_str}, data={self.data.hex().upper()}" + + +class FixedSizeIntDataType(int, core.DataType): + _fixed_size: int = 0 + _signed: bool = False + + @classmethod + def from_bytes(cls, data: bytes) -> Self: # type: ignore[override] + if len(data) != cls._fixed_size: + raise ValueError(f"data must be {cls._fixed_size} byte") + return cls(int.from_bytes(data, byteorder="little", signed=cls._signed)) + + def __bytes__(self) -> bytes: + return self.to_bytes( + length=self._fixed_size, byteorder="little", signed=self._signed + ) + + def value_string(self) -> str: + return str(int(self)) + + +class TxPowerLevel(FixedSizeIntDataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.5 TX POWER LEVEL + """ + + _fixed_size = 1 + _signed = True + label = "TX Power Level" + ad_type = core.AdvertisingData.Type.TX_POWER_LEVEL + + +class FixedSizeBytesDataType(bytes, core.DataType): + _fixed_size: int = 0 + + @classmethod + def from_bytes(cls, data: bytes) -> Self: + if len(data) != cls._fixed_size: + raise ValueError(f"data must be {cls._fixed_size} bytes") + return cls(data) + + def value_string(self) -> str: + return self.hex().upper() + + def __str__(self) -> str: + return core.DataType.__str__(self) + + +class ClassOfDevice(core.ClassOfDevice, core.DataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.6 SECURE SIMPLE PAIRING OUT OF BAND (OOB) + """ + + label = "Class of Device" + ad_type = core.AdvertisingData.Type.CLASS_OF_DEVICE + + @classmethod + def from_bytes(cls, data: bytes) -> ClassOfDevice: + return cls.from_int(int.from_bytes(data, byteorder="little")) + + def __bytes__(self) -> bytes: + return int(self).to_bytes(3, byteorder="little") + + def __eq__(self, value: Any) -> bool: + return core.ClassOfDevice.__eq__(self, value) + + def value_string(self) -> str: + return ( + f"{self.major_service_classes_labels()}," + f"{self.major_device_class_label()}/" + f"{self.minor_device_class_label()}" + ) + + def __str__(self) -> str: + return core.DataType.__str__(self) + + +class SecureSimplePairingHashC192(FixedSizeBytesDataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.6 SECURE SIMPLE PAIRING OUT OF BAND (OOB) + """ + + _fixed_size = 16 + label = "Secure Simple Pairing Hash C-192" + ad_type = core.AdvertisingData.Type.SIMPLE_PAIRING_HASH_C_192 + + +class SecureSimplePairingRandomizerR192(FixedSizeBytesDataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.6 SECURE SIMPLE PAIRING OUT OF BAND (OOB) + """ + + _fixed_size = 16 + label = "Secure Simple Pairing Randomizer R-192" + ad_type = core.AdvertisingData.Type.SIMPLE_PAIRING_RANDOMIZER_R_192 + + +class SecureSimplePairingHashC256(FixedSizeBytesDataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.6 SECURE SIMPLE PAIRING OUT OF BAND (OOB) + """ + + _fixed_size = 16 + label = "Secure Simple Pairing Hash C-256" + ad_type = core.AdvertisingData.Type.SIMPLE_PAIRING_HASH_C_256 + + +class SecureSimplePairingRandomizerR256(FixedSizeBytesDataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.6 SECURE SIMPLE PAIRING OUT OF BAND (OOB) + """ + + _fixed_size = 16 + label = "Secure Simple Pairing Randomizer R-256" + ad_type = core.AdvertisingData.Type.SIMPLE_PAIRING_RANDOMIZER_R_256 + + +class LeSecureConnectionsConfirmationValue(FixedSizeBytesDataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.6 SECURE SIMPLE PAIRING OUT OF BAND (OOB) + """ + + _fixed_size = 16 + label = "LE Secure Connections Confirmation Value" + ad_type = core.AdvertisingData.Type.LE_SECURE_CONNECTIONS_CONFIRMATION_VALUE + + +class LeSecureConnectionsRandomValue(FixedSizeBytesDataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.6 SECURE SIMPLE PAIRING OUT OF BAND (OOB) + """ + + _fixed_size = 16 + label = "LE Secure Connections Random Value" + ad_type = core.AdvertisingData.Type.LE_SECURE_CONNECTIONS_RANDOM_VALUE + + +class SecurityManagerOutOfBandFlag(int, core.DataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.7 SECURITY MANAGER OUT OF BAND (OOB) + """ + + label = "Security Manager Out of Band Flag" + ad_type = core.AdvertisingData.Type.SECURITY_MANAGER_OUT_OF_BAND_FLAGS + + def __init__(self, flag: core.SecurityManagerOutOfBandFlag) -> None: + pass + + @classmethod + # type: ignore[override] + def from_bytes(cls, data: bytes) -> SecurityManagerOutOfBandFlag: + if len(data) != 1: + raise ValueError("data must be 1 byte") + return SecurityManagerOutOfBandFlag(core.SecurityManagerOutOfBandFlag(data[0])) + + def __bytes__(self) -> bytes: + return bytes([self]) + + def __str__(self) -> str: + return core.DataType.__str__(self) + + def value_string(self) -> str: + return core.SecurityManagerOutOfBandFlag(self).name or "" + + +class SecurityManagerTKValue(FixedSizeBytesDataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.8 SECURITY MANAGER TK VALUE + """ + + _fixed_size = 16 + label = "Security Manager TK Value" + ad_type = core.AdvertisingData.Type.SECURITY_MANAGER_TK_VALUE + + +@dataclasses.dataclass +class PeripheralConnectionIntervalRange(core.DataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.9 PERIPHERAL CONNECTION INTERVAL RANGE + """ + + label = "Peripheral Connection Interval Range" + ad_type = core.AdvertisingData.Type.PERIPHERAL_CONNECTION_INTERVAL_RANGE + + connection_interval_min: int + connection_interval_max: int + + @classmethod + def from_bytes(cls, data: bytes) -> PeripheralConnectionIntervalRange: + return cls(*struct.unpack(" bytes: + return struct.pack( + " str: + return ( + f"connection_interval_min={self.connection_interval_min}, " + f"connection_interval_max={self.connection_interval_max}" + ) + + +class ListOf16BitServiceSolicitationUUIDs(ListOfServiceUUIDs): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.10 SERVICE SOLICITATION + """ + + _uuid_size = 2 + label = "List of 16 bit Service Solicitation UUIDs" + ad_type = core.AdvertisingData.Type.LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS + + +class ListOf32BitServiceSolicitationUUIDs(ListOfServiceUUIDs): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.10 SERVICE SOLICITATION + """ + + _uuid_size = 4 + label = "List of 32 bit Service Solicitation UUIDs" + ad_type = core.AdvertisingData.Type.LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS + + +class ListOf128BitServiceSolicitationUUIDs(ListOfServiceUUIDs): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.10 SERVICE SOLICITATION + """ + + _uuid_size = 16 + label = "List of 128 bit Service Solicitation UUIDs" + ad_type = core.AdvertisingData.Type.LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS + + +@dataclasses.dataclass +class ServiceData(core.DataType): + """Base class for service data lists of UUIDs.""" + + _uuid_size: ClassVar[int] = 0 + + service_uuid: core.UUID + data: bytes + + @classmethod + def from_bytes(cls, data: bytes) -> Self: + service_uuid = core.UUID.from_bytes(data[: cls._uuid_size]) + return cls(service_uuid, data[cls._uuid_size :]) + + def __bytes__(self) -> bytes: + return self.service_uuid.to_bytes() + self.data + + def value_string(self) -> str: + return f"service={self.service_uuid}, data={self.data.hex().upper()}" + + +class ServiceData16BitUUID(ServiceData): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.11 SERVICE DATA + """ + + _uuid_size = 2 + label = "Service Data - 16 bit UUID" + ad_type = core.AdvertisingData.Type.SERVICE_DATA_16_BIT_UUID + + +class ServiceData32BitUUID(ServiceData): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.11 SERVICE DATA + """ + + _uuid_size = 4 + label = "Service Data - 32 bit UUID" + ad_type = core.AdvertisingData.Type.SERVICE_DATA_32_BIT_UUID + + +class ServiceData128BitUUID(ServiceData): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.11 SERVICE DATA + """ + + _uuid_size = 16 + label = "Service Data - 128 bit UUID" + ad_type = core.AdvertisingData.Type.SERVICE_DATA_128_BIT_UUID + + +class Appearance(core.Appearance, core.DataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.12 APPEARANCE + """ + + label = "Appearance" + ad_type = core.AdvertisingData.Type.APPEARANCE + + @classmethod + def from_bytes(cls, data: bytes): + return cls.from_int(int.from_bytes(data, byteorder="little")) + + def __bytes__(self) -> bytes: + return int(self).to_bytes(2, byteorder="little") + + def __str__(self) -> str: + return core.DataType.__str__(self) + + def value_string(self) -> str: + return core.Appearance.__str__(self) + + +class PublicTargetAddress(hci.Address, core.DataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.13 PUBLIC TARGET ADDRESS + """ + + label = "Public Target Address" + ad_type = core.AdvertisingData.Type.PUBLIC_TARGET_ADDRESS + + def __init__(self, address: hci.Address) -> None: + self.address_bytes = address.address_bytes + self.address_type = hci.Address.PUBLIC_DEVICE_ADDRESS + + @classmethod + def from_bytes(cls, data: bytes) -> PublicTargetAddress: + return cls(hci.Address(data)) + + def __str__(self) -> str: + return core.DataType.__str__(self) + + def to_string(self, use_label: bool = False) -> str: + return core.DataType.to_string(self, use_label) + + def value_string(self) -> str: + return hci.Address.to_string(self, with_type_qualifier=False) + + +class RandomTargetAddress(hci.Address, core.DataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.14 RANDOM TARGET ADDRESS + """ + + label = "Random Target Address" + ad_type = core.AdvertisingData.Type.RANDOM_TARGET_ADDRESS + + def __init__(self, address: hci.Address) -> None: + self.address_bytes = address.address_bytes + self.address_type = hci.Address.RANDOM_DEVICE_ADDRESS + + @classmethod + def from_bytes(cls, data: bytes) -> RandomTargetAddress: + return cls(hci.Address(data)) + + def __str__(self) -> str: + return core.DataType.__str__(self) + + def to_string(self, use_label: bool = False) -> str: + return core.DataType.to_string(self, use_label) + + def value_string(self) -> str: + return hci.Address.to_string(self, with_type_qualifier=False) + + +class AdvertisingInterval(FixedSizeIntDataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.15 ADVERTISING INTERVAL + """ + + _fixed_size = 2 + label = "Advertising Interval" + ad_type = core.AdvertisingData.Type.ADVERTISING_INTERVAL + + +class AdvertisingIntervalLong(int, core.DataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.15 ADVERTISING INTERVAL + """ + + label = "Advertising Interval - long" + ad_type = core.AdvertisingData.Type.ADVERTISING_INTERVAL_LONG + + @classmethod + def from_bytes(cls, data: bytes) -> Self: # type: ignore[override] + return cls(int.from_bytes(data, byteorder="little")) + + def __bytes__(self) -> bytes: + return self.to_bytes(length=4 if self >= 0x1000000 else 3, byteorder="little") + + def value_string(self) -> str: + return str(int(self)) + + +class LeBluetoothDeviceAddress(hci.Address, core.DataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.16 LE BLUETOOTH DEVICE ADDRESS + """ + + label = "LE Bluetooth Device Address" + ad_type = core.AdvertisingData.Type.LE_BLUETOOTH_DEVICE_ADDRESS + + def __init__(self, address: hci.Address) -> None: + self.address_bytes = address.address_bytes + self.address_type = address.address_type + + @classmethod + def from_bytes(cls, data: bytes) -> LeBluetoothDeviceAddress: + return cls(hci.Address(data[1:], hci.AddressType(data[0]))) + + def __bytes__(self) -> bytes: + return bytes([self.address_type]) + self.address_bytes + + def __str__(self) -> str: + return core.DataType.__str__(self) + + def to_string(self, use_label: bool = False) -> str: + return core.DataType.to_string(self, use_label) + + def value_string(self) -> str: + return ( + f"{hci.Address.to_string(self, with_type_qualifier=False)}" + f"/{'PUBLIC' if self.is_public else 'RANDOM'}" + ) + + +class LeRole(int, core.DataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.17 LE ROLE + """ + + label = "LE Role" + ad_type = core.AdvertisingData.Type.LE_ROLE + + def __init__(self, role: core.LeRole) -> None: + pass + + @classmethod + def from_bytes(cls, data: bytes) -> Self: # type: ignore[override] + return cls(core.LeRole(data[0])) + + def __bytes__(self) -> bytes: + return bytes([self]) + + def value_string(self) -> str: + return core.LeRole(self).name + + +class Uri(str, core.DataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.18 UNIFORM RESOURCE IDENTIFIER (URI) + """ + + label = "URI" + ad_type = core.AdvertisingData.Type.URI + + @classmethod + def from_bytes(cls, data: bytes) -> Self: + return cls(data.decode("utf-8")) + + def __bytes__(self): + return self.encode("utf-8") + + def __str__(self) -> str: + return core.DataType.__str__(self) + + def value_string(self) -> str: + return repr(self) + + +class LeSupportedFeatures(int, core.DataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.19 LE SUPPORTED FEATURES + """ + + label = "LE Supported Features" + ad_type = core.AdvertisingData.Type.LE_SUPPORTED_FEATURES + + @classmethod + def from_bytes(cls, data: bytes) -> LeSupportedFeatures: # type: ignore[override] + return cls(int.from_bytes(data, byteorder="little")) + + def __bytes__(self) -> bytes: + bytes_length = 1 if self == 0 else math.ceil(self.bit_length() / 8) + return self.to_bytes(length=bytes_length, byteorder="little") + + def value_string(self) -> str: + return hci.LeFeatureMask(self).name or "" + + +@dataclasses.dataclass +class ChannelMapUpdateIndication(core.DataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.20 CHANNEL MAP UPDATE INDICATION + """ + + label = "Channel Map Update Indication" + ad_type = core.AdvertisingData.Type.CHANNEL_MAP_UPDATE_INDICATION + + chm: int + instant: int + + @classmethod + def from_bytes(cls, data: bytes) -> ChannelMapUpdateIndication: + return cls( + int.from_bytes(data[:5], byteorder="little"), + int.from_bytes(data[5:7], byteorder="little"), + ) + + def __bytes__(self) -> bytes: + return self.chm.to_bytes(5, byteorder="little") + self.instant.to_bytes( + 2, byteorder="little" + ) + + def value_string(self) -> str: + return f"chm={self.chm:010X}, instant={self.instant}" + + +class BigInfo(core.DataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.21 BIGINFO + """ + + # TODO + + +class BroadcastCode(str, core.DataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.22 BROADCAST_CODE + """ + + label = "Broadcast Code" + ad_type = core.AdvertisingData.Type.BROADCAST_CODE + + def __init__(self, value: str) -> None: + encoded = value.encode("utf-8") + if len(encoded) > 16: + raise ValueError("broadcast code must be <= 16 bytes in utf-8 encoding") + + @classmethod + def from_bytes(cls, data: bytes) -> Self: + return cls(data.strip(bytes([0])).decode("utf-8")) + + def __bytes__(self) -> bytes: + return self.encode("utf-8") + + def __str__(self) -> str: + return core.DataType.__str__(self) + + def value_string(self) -> str: + return repr(self) + + +@dataclasses.dataclass +class EncryptedData(core.DataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.23 ENCRYPTED DATA + """ + + label = "Encrypted Data" + ad_type = core.AdvertisingData.Type.ENCRYPTED_ADVERTISING_DATA + + randomizer: int + payload: bytes + mic: bytes + + @classmethod + def from_bytes(cls, data: bytes) -> EncryptedData: + randomizer = int.from_bytes(data[:5], byteorder="little") + payload = data[5 : len(data) - 4] + mic = data[-4:] + return cls(randomizer, payload, mic) + + def __bytes__(self) -> bytes: + return self.randomizer.to_bytes(5, byteorder="little") + self.payload + self.mic + + def value_string(self) -> str: + return ( + f"randomizer=0x{self.randomizer:010X}, " + f"payload={self.payload.hex().upper()}, " + f"mic={self.mic.hex().upper()}" + ) + + +@dataclasses.dataclass +class PeriodicAdvertisingResponseTimingInformation(core.DataType): + """ + See Supplement to the Bluetooth Core Specification, Part A + 1.24 PERIODIC ADVERTISING RESPONSE TIMING INFORMATION + """ + + label = "Periodic Advertising Response Timing Information" + ad_type = core.AdvertisingData.Type.PERIODIC_ADVERTISING_RESPONSE_TIMING_INFORMATION + + rspaa: int + num_subevents: int + subevent_interval: int + response_slot_delay: int + response_slot_spacing: int + + @classmethod + def from_bytes(cls, data: bytes) -> PeriodicAdvertisingResponseTimingInformation: + return cls( + int.from_bytes(data[:4], byteorder="little"), + data[4], + data[5], + data[6], + data[7], + ) + + def __bytes__(self) -> bytes: + return self.rspaa.to_bytes(4, byteorder="little") + bytes( + [ + self.num_subevents, + self.subevent_interval, + self.response_slot_delay, + self.response_slot_spacing, + ] + ) + + def value_string(self) -> str: + return ( + f"rspaa=0x{self.rspaa:08X}, " + f"num_subevents={self.num_subevents}, " + f"subevent_interval={self.subevent_interval}, " + f"response_slot_delay={self.response_slot_delay}, " + f"response_slot_spacing={self.response_slot_spacing}" + ) + + +class BroadcastName(StringDataType): + """ + See Assigned Numbers, 6.12.6.13 Broadcast_Name + """ + + label = "Broadcast Name" + ad_type = core.AdvertisingData.Type.BROADCAST_NAME + + +class ResolvableSetIdentifier(FixedSizeBytesDataType): + """ + See Coordinated Set Identification Service, 3.1 RSI AD Type + """ + + label = "Resolvable Set Identifier" + ad_type = core.AdvertisingData.Type.RESOLVABLE_SET_IDENTIFIER + _fixed_size = 6 + + +# ----------------------------------------------------------------------------- +_AD_TO_DATA_TYPE_CLASS_MAP: dict[core.AdvertisingData.Type, type[core.DataType]] = { + core.AdvertisingData.Type.FLAGS: Flags, + core.AdvertisingData.Type.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS: IncompleteListOf16BitServiceUUIDs, + core.AdvertisingData.Type.COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS: CompleteListOf16BitServiceUUIDs, + core.AdvertisingData.Type.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS: IncompleteListOf32BitServiceUUIDs, + core.AdvertisingData.Type.COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS: CompleteListOf32BitServiceUUIDs, + core.AdvertisingData.Type.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS: IncompleteListOf128BitServiceUUIDs, + core.AdvertisingData.Type.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS: CompleteListOf128BitServiceUUIDs, + core.AdvertisingData.Type.SHORTENED_LOCAL_NAME: ShortenedLocalName, + core.AdvertisingData.Type.COMPLETE_LOCAL_NAME: CompleteLocalName, + core.AdvertisingData.Type.TX_POWER_LEVEL: TxPowerLevel, + core.AdvertisingData.Type.CLASS_OF_DEVICE: ClassOfDevice, + core.AdvertisingData.Type.SIMPLE_PAIRING_HASH_C_192: SecureSimplePairingHashC192, + core.AdvertisingData.Type.SIMPLE_PAIRING_RANDOMIZER_R_192: SecureSimplePairingRandomizerR192, + # core.AdvertisingData.Type.DEVICE_ID: TBD, + core.AdvertisingData.Type.SECURITY_MANAGER_TK_VALUE: SecurityManagerTKValue, + core.AdvertisingData.Type.SECURITY_MANAGER_OUT_OF_BAND_FLAGS: SecurityManagerOutOfBandFlag, + core.AdvertisingData.Type.PERIPHERAL_CONNECTION_INTERVAL_RANGE: PeripheralConnectionIntervalRange, + core.AdvertisingData.Type.LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS: ListOf16BitServiceSolicitationUUIDs, + core.AdvertisingData.Type.LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS: ListOf128BitServiceSolicitationUUIDs, + core.AdvertisingData.Type.SERVICE_DATA_16_BIT_UUID: ServiceData16BitUUID, + core.AdvertisingData.Type.PUBLIC_TARGET_ADDRESS: PublicTargetAddress, + core.AdvertisingData.Type.RANDOM_TARGET_ADDRESS: RandomTargetAddress, + core.AdvertisingData.Type.APPEARANCE: Appearance, + core.AdvertisingData.Type.ADVERTISING_INTERVAL: AdvertisingInterval, + core.AdvertisingData.Type.LE_BLUETOOTH_DEVICE_ADDRESS: LeBluetoothDeviceAddress, + core.AdvertisingData.Type.LE_ROLE: LeRole, + core.AdvertisingData.Type.SIMPLE_PAIRING_HASH_C_256: SecureSimplePairingHashC256, + core.AdvertisingData.Type.SIMPLE_PAIRING_RANDOMIZER_R_256: SecureSimplePairingRandomizerR256, + core.AdvertisingData.Type.LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS: ListOf32BitServiceSolicitationUUIDs, + core.AdvertisingData.Type.SERVICE_DATA_32_BIT_UUID: ServiceData32BitUUID, + core.AdvertisingData.Type.SERVICE_DATA_128_BIT_UUID: ServiceData128BitUUID, + core.AdvertisingData.Type.LE_SECURE_CONNECTIONS_CONFIRMATION_VALUE: LeSecureConnectionsConfirmationValue, + core.AdvertisingData.Type.LE_SECURE_CONNECTIONS_RANDOM_VALUE: LeSecureConnectionsRandomValue, + core.AdvertisingData.Type.URI: Uri, + # core.AdvertisingData.Type.INDOOR_POSITIONING: TBD, + # core.AdvertisingData.Type.TRANSPORT_DISCOVERY_DATA: TBD, + core.AdvertisingData.Type.LE_SUPPORTED_FEATURES: LeSupportedFeatures, + core.AdvertisingData.Type.CHANNEL_MAP_UPDATE_INDICATION: ChannelMapUpdateIndication, + # core.AdvertisingData.Type.PB_ADV: TBD, + # core.AdvertisingData.Type.MESH_MESSAGE: TBD, + # core.AdvertisingData.Type.MESH_BEACON: TBD, + # core.AdvertisingData.Type.BIGINFO: BigInfo, + core.AdvertisingData.Type.BROADCAST_CODE: BroadcastCode, + core.AdvertisingData.Type.RESOLVABLE_SET_IDENTIFIER: ResolvableSetIdentifier, + core.AdvertisingData.Type.ADVERTISING_INTERVAL_LONG: AdvertisingIntervalLong, + core.AdvertisingData.Type.BROADCAST_NAME: BroadcastName, + core.AdvertisingData.Type.ENCRYPTED_ADVERTISING_DATA: EncryptedData, + core.AdvertisingData.Type.PERIODIC_ADVERTISING_RESPONSE_TIMING_INFORMATION: PeriodicAdvertisingResponseTimingInformation, + # core.AdvertisingData.Type.ELECTRONIC_SHELF_LABEL: TBD, + # core.AdvertisingData.Type.THREE_D_INFORMATION_DATA: TBD, + core.AdvertisingData.Type.MANUFACTURER_SPECIFIC_DATA: ManufacturerSpecificData, +} + + +def data_type_from_advertising_data( + advertising_data_type: core.AdvertisingData.Type, + advertising_data: bytes, +) -> core.DataType: + """ + Creates a DataType object given a type ID and serialized data. + + NOTE: in general, if you know the type ID, it is preferrable to simply call the + `from_bytes` factory class method of the associated DataType class directly. + For example, use BroadcastName.from_bytes(bn_data) rather than + data_type_from_advertising_data(AdvertisingData.Type.BROADCAST_NAME, bn_data) + + Args: + advertising_data_type: type ID of the data. + advertising_data: serialized data. + + Returns: + a DataType subclass instance. + + """ + if data_type_class := _AD_TO_DATA_TYPE_CLASS_MAP.get(advertising_data_type): + return data_type_class.from_bytes(advertising_data) + + return GenericAdvertisingData(advertising_data, advertising_data_type) + + +def data_types_from_advertising_data( + advertising_data: core.AdvertisingData, +) -> list[core.DataType]: + """ + Create DataType objects representing all the advertising data structs contained + in an AdvertisingData object. + + Args: + advertising_data: the AdvertisingData in which to look for the data type. + + Returns: + a list of DataType subclass instances. + """ + return [ + data_type_from_advertising_data(ad_type, ad_data) + for (ad_type, ad_data) in advertising_data.ad_structures + ] diff --git a/bumble/device.py b/bumble/device.py index 3be2189..e67f78d 100644 --- a/bumble/device.py +++ b/bumble/device.py @@ -45,7 +45,18 @@ from typing import ( 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.colors import color from bumble.core import ( @@ -2049,9 +2060,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 @@ -2095,9 +2104,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 @@ -3544,14 +3551,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 diff --git a/bumble/hci.py b/bumble/hci.py index 06d6cd3..cd6f913 100644 --- a/bumble/hci.py +++ b/bumble/hci.py @@ -2135,6 +2135,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: diff --git a/bumble/profiles/asha.py b/bumble/profiles/asha.py index 7ed2624..4330446 100644 --- a/bumble/profiles/asha.py +++ b/bumble/profiles/asha.py @@ -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], - ), + ) ] ) ) diff --git a/bumble/profiles/bap.py b/bumble/profiles/bap.py index 99bbc5e..49c2e3d 100644 --- a/bumble/profiles/bap.py +++ b/bumble/profiles/bap.py @@ -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), + ' 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(' 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(' 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(' None: device.scan_response_data = bytes( AdvertisingData( [ - (AdvertisingData.APPEARANCE, struct.pack(' 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] ), ] ) diff --git a/examples/run_csis_servers.py b/examples/run_csis_servers.py index 78fac89..ea609fb 100644 --- a/examples/run_csis_servers.py +++ b/examples/run_csis_servers.py @@ -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'), + 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.FLAGS, - bytes( - [ - 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] ), ] ) diff --git a/examples/run_hap_server.py b/examples/run_hap_server.py index c30ea14..5094766 100644 --- a/examples/run_hap_server.py +++ b/examples/run_hap_server.py @@ -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'), + 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.FLAGS, - bytes( - [ - 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] ), ] ) diff --git a/examples/run_mcp_client.py b/examples/run_mcp_client.py index db72ca6..fb5c34a 100644 --- a/examples/run_mcp_client.py +++ b/examples/run_mcp_client.py @@ -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] ), ] ) diff --git a/examples/run_unicast_server.py b/examples/run_unicast_server.py index fb2c6a0..6d4eae0 100644 --- a/examples/run_unicast_server.py +++ b/examples/run_unicast_server.py @@ -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'), + 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.FLAGS, - bytes( - [ - 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] ), ] ) diff --git a/examples/run_vcp_renderer.py b/examples/run_vcp_renderer.py index c2b52ac..4217ac8 100644 --- a/examples/run_vcp_renderer.py +++ b/examples/run_vcp_renderer.py @@ -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'), + 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.FLAGS, - bytes( - [ - 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] ), ] ) diff --git a/tests/core_test.py b/tests/core_test.py index a77be12..02ebbaa 100644 --- a/tests/core_test.py +++ b/tests/core_test.py @@ -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() diff --git a/web/heart_rate_monitor/heart_rate_monitor.py b/web/heart_rate_monitor/heart_rate_monitor.py index 4a843b4..4ba8ef5 100644 --- a/web/heart_rate_monitor/heart_rate_monitor.py +++ b/web/heart_rate_monitor/heart_rate_monitor.py @@ -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 ), - ( - AdvertisingData.COMPLETE_LOCAL_NAME, - bytes('Bumble Heart', 'utf-8'), + data_types.CompleteLocalName('Bumble Heart'), + data_types.IncompleteListOf16BitServiceUUIDs( + [self.heart_rate_service.uuid] ), - ( - AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, - bytes(self.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('