# Copyright 2021-2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ----------------------------------------------------------------------------- # Imports # ----------------------------------------------------------------------------- from __future__ import annotations import dataclasses import enum import struct from typing import List, Optional, Tuple, Union, cast, Dict from typing_extensions import Self from bumble.company_ids import COMPANY_IDENTIFIERS from bumble.utils import OpenIntEnum # ----------------------------------------------------------------------------- # Constants # ----------------------------------------------------------------------------- # fmt: off BT_CENTRAL_ROLE = 0 BT_PERIPHERAL_ROLE = 1 BT_BR_EDR_TRANSPORT = 0 BT_LE_TRANSPORT = 1 # fmt: on # ----------------------------------------------------------------------------- # Utils # ----------------------------------------------------------------------------- def bit_flags_to_strings(bits, bit_flag_names): names = [] index = 0 while bits != 0: if bits & 1: name = bit_flag_names[index] if index < len(bit_flag_names) else f'#{index}' names.append(name) bits >>= 1 index += 1 return names def name_or_number(dictionary: Dict[int, str], number: int, width: int = 2) -> str: name = dictionary.get(number) if name is not None: return name return f'[0x{number:0{width}X}]' def padded_bytes(buffer, size): padding_size = max(size - len(buffer), 0) return buffer + bytes(padding_size) def get_dict_key_by_value(dictionary, value): for key, val in dictionary.items(): if val == value: return key return None # ----------------------------------------------------------------------------- # Exceptions # ----------------------------------------------------------------------------- class BaseBumbleError(Exception): """Base Error raised by Bumble.""" class BaseError(BaseBumbleError): """Base class for errors with an error code, error name and namespace""" def __init__( self, error_code: Optional[int], error_namespace: str = '', error_name: str = '', details: str = '', ): super().__init__() self.error_code = error_code self.error_namespace = error_namespace self.error_name = error_name self.details = details def __str__(self): if self.error_namespace: namespace = f'{self.error_namespace}/' else: namespace = '' have_name = self.error_name != '' have_code = self.error_code is not None if have_name and have_code: error_text = f'{self.error_name} [0x{self.error_code:X}]' elif have_name and not have_code: error_text = self.error_name elif not have_name and have_code: error_text = f'0x{self.error_code:X}' else: error_text = '' return f'{type(self).__name__}({namespace}{error_text})' class ProtocolError(BaseError): """Protocol Error""" class TimeoutError(BaseBumbleError): # pylint: disable=redefined-builtin """Timeout Error""" class CommandTimeoutError(BaseBumbleError): """Command Timeout Error""" class InvalidStateError(BaseBumbleError): """Invalid State Error""" class InvalidArgumentError(BaseBumbleError, ValueError): """Invalid Argument Error""" class InvalidPacketError(BaseBumbleError, ValueError): """Invalid Packet Error""" class InvalidOperationError(BaseBumbleError, RuntimeError): """Invalid Operation Error""" class NotSupportedError(BaseBumbleError, RuntimeError): """Not Supported""" class OutOfResourcesError(BaseBumbleError, RuntimeError): """Out of Resources Error""" class UnreachableError(BaseBumbleError): """The code path raising this error should be unreachable.""" class ConnectionError(BaseError): # pylint: disable=redefined-builtin """Connection Error""" FAILURE = 0x01 CONNECTION_REFUSED = 0x02 def __init__( self, error_code, transport, peer_address, error_namespace='', error_name='', details='', ): super().__init__(error_code, error_namespace, error_name, details) self.transport = transport self.peer_address = peer_address class ConnectionParameterUpdateError(BaseError): """Connection Parameter Update Error""" # ----------------------------------------------------------------------------- # UUID # # NOTE: the internal byte representation is in little-endian byte order # # Base UUID: 00000000-0000-1000-8000- 00805F9B34FB # ----------------------------------------------------------------------------- class UUID: ''' See Bluetooth spec Vol 3, Part B - 2.5.1 UUID Note that this class expects and works in little-endian byte-order throughout. The exception is when interacting with strings, which are in big-endian byte-order. ''' BASE_UUID = bytes.fromhex('00001000800000805F9B34FB')[::-1] # little-endian UUIDS: List[UUID] = [] # Registry of all instances created uuid_bytes: bytes name: Optional[str] def __init__( self, uuid_str_or_int: Union[str, int], name: Optional[str] = None ) -> None: if isinstance(uuid_str_or_int, int): self.uuid_bytes = struct.pack(' UUID: # Register this object in the class registry, and update the entry's name if # it wasn't set already for uuid in self.UUIDS: if self == uuid: if uuid.name is None: uuid.name = self.name return uuid self.UUIDS.append(self) return self @classmethod def from_bytes(cls, uuid_bytes: bytes, name: Optional[str] = None) -> UUID: if len(uuid_bytes) in (2, 4, 16): self = cls.__new__(cls) self.uuid_bytes = uuid_bytes self.name = name return self.register() raise InvalidArgumentError('only 2, 4 and 16 bytes are allowed') @classmethod def from_16_bits(cls, uuid_16: int, name: Optional[str] = None) -> UUID: return cls.from_bytes(struct.pack(' UUID: return cls.from_bytes(struct.pack(' Tuple[int, UUID]: return len(uuid_as_bytes), cls.from_bytes(uuid_as_bytes[offset:]) @classmethod def parse_uuid_2(cls, uuid_as_bytes: bytes, offset: int) -> Tuple[int, UUID]: return offset + 2, cls.from_bytes(uuid_as_bytes[offset : offset + 2]) def to_bytes(self, force_128: bool = False) -> bytes: ''' Serialize UUID in little-endian byte-order ''' if not force_128: return self.uuid_bytes if len(self.uuid_bytes) == 2: return self.BASE_UUID + self.uuid_bytes + bytes([0, 0]) elif len(self.uuid_bytes) == 4: return self.BASE_UUID + self.uuid_bytes elif len(self.uuid_bytes) == 16: return self.uuid_bytes else: assert False, "unreachable" def to_pdu_bytes(self) -> bytes: ''' Convert to bytes for use in an ATT PDU. According to Vol 3, Part F - 3.2.1 Attribute Type: "All 32-bit Attribute UUIDs shall be converted to 128-bit UUIDs when the Attribute UUID is contained in an ATT PDU." ''' return self.to_bytes(force_128=(len(self.uuid_bytes) == 4)) def to_hex_str(self, separator: str = '') -> str: if len(self.uuid_bytes) == 2 or len(self.uuid_bytes) == 4: return bytes(reversed(self.uuid_bytes)).hex().upper() return separator.join( [ bytes(reversed(self.uuid_bytes[12:16])).hex(), bytes(reversed(self.uuid_bytes[10:12])).hex(), bytes(reversed(self.uuid_bytes[8:10])).hex(), bytes(reversed(self.uuid_bytes[6:8])).hex(), bytes(reversed(self.uuid_bytes[0:6])).hex(), ] ).upper() def __bytes__(self) -> bytes: return self.to_bytes() def __eq__(self, other: object) -> bool: if isinstance(other, UUID): return self.to_bytes(force_128=True) == other.to_bytes(force_128=True) if isinstance(other, str): return UUID(other) == self return False def __hash__(self) -> int: return hash(self.uuid_bytes) def __str__(self) -> str: result = self.to_hex_str(separator='-') if len(self.uuid_bytes) == 2: result = 'UUID-16:' + result elif len(self.uuid_bytes) == 4: result = 'UUID-32:' + result if self.name is not None: result += f' ({self.name})' return result # ----------------------------------------------------------------------------- # Common UUID constants # ----------------------------------------------------------------------------- # fmt: off # pylint: disable=line-too-long # Protocol Identifiers BT_SDP_PROTOCOL_ID = UUID.from_16_bits(0x0001, 'SDP') BT_UDP_PROTOCOL_ID = UUID.from_16_bits(0x0002, 'UDP') BT_RFCOMM_PROTOCOL_ID = UUID.from_16_bits(0x0003, 'RFCOMM') BT_TCP_PROTOCOL_ID = UUID.from_16_bits(0x0004, 'TCP') BT_TCS_BIN_PROTOCOL_ID = UUID.from_16_bits(0x0005, 'TCP-BIN') BT_TCS_AT_PROTOCOL_ID = UUID.from_16_bits(0x0006, 'TCS-AT') BT_ATT_PROTOCOL_ID = UUID.from_16_bits(0x0007, 'ATT') BT_OBEX_PROTOCOL_ID = UUID.from_16_bits(0x0008, 'OBEX') BT_IP_PROTOCOL_ID = UUID.from_16_bits(0x0009, 'IP') BT_FTP_PROTOCOL_ID = UUID.from_16_bits(0x000A, 'FTP') BT_HTTP_PROTOCOL_ID = UUID.from_16_bits(0x000C, 'HTTP') BT_WSP_PROTOCOL_ID = UUID.from_16_bits(0x000E, 'WSP') BT_BNEP_PROTOCOL_ID = UUID.from_16_bits(0x000F, 'BNEP') BT_UPNP_PROTOCOL_ID = UUID.from_16_bits(0x0010, 'UPNP') BT_HIDP_PROTOCOL_ID = UUID.from_16_bits(0x0011, 'HIDP') BT_HARDCOPY_CONTROL_CHANNEL_PROTOCOL_ID = UUID.from_16_bits(0x0012, 'HardcopyControlChannel') BT_HARDCOPY_DATA_CHANNEL_PROTOCOL_ID = UUID.from_16_bits(0x0014, 'HardcopyDataChannel') BT_HARDCOPY_NOTIFICATION_PROTOCOL_ID = UUID.from_16_bits(0x0016, 'HardcopyNotification') BT_AVCTP_PROTOCOL_ID = UUID.from_16_bits(0x0017, 'AVCTP') BT_AVDTP_PROTOCOL_ID = UUID.from_16_bits(0x0019, 'AVDTP') BT_CMTP_PROTOCOL_ID = UUID.from_16_bits(0x001B, 'CMTP') BT_MCAP_CONTROL_CHANNEL_PROTOCOL_ID = UUID.from_16_bits(0x001E, 'MCAPControlChannel') BT_MCAP_DATA_CHANNEL_PROTOCOL_ID = UUID.from_16_bits(0x001F, 'MCAPDataChannel') BT_L2CAP_PROTOCOL_ID = UUID.from_16_bits(0x0100, 'L2CAP') # Service Classes and Profiles BT_SERVICE_DISCOVERY_SERVER_SERVICE_CLASS_ID_SERVICE = UUID.from_16_bits(0x1000, 'ServiceDiscoveryServerServiceClassID') BT_BROWSE_GROUP_DESCRIPTOR_SERVICE_CLASS_ID_SERVICE = UUID.from_16_bits(0x1001, 'BrowseGroupDescriptorServiceClassID') BT_SERIAL_PORT_SERVICE = UUID.from_16_bits(0x1101, 'SerialPort') BT_LAN_ACCESS_USING_PPP_SERVICE = UUID.from_16_bits(0x1102, 'LANAccessUsingPPP') BT_DIALUP_NETWORKING_SERVICE = UUID.from_16_bits(0x1103, 'DialupNetworking') BT_IR_MCSYNC_SERVICE = UUID.from_16_bits(0x1104, 'IrMCSync') BT_OBEX_OBJECT_PUSH_SERVICE = UUID.from_16_bits(0x1105, 'OBEXObjectPush') BT_OBEX_FILE_TRANSFER_SERVICE = UUID.from_16_bits(0x1106, 'OBEXFileTransfer') BT_IR_MCSYNC_COMMAND_SERVICE = UUID.from_16_bits(0x1107, 'IrMCSyncCommand') BT_HEADSET_SERVICE = UUID.from_16_bits(0x1108, 'Headset') BT_CORDLESS_TELEPHONY_SERVICE = UUID.from_16_bits(0x1109, 'CordlessTelephony') BT_AUDIO_SOURCE_SERVICE = UUID.from_16_bits(0x110A, 'AudioSource') BT_AUDIO_SINK_SERVICE = UUID.from_16_bits(0x110B, 'AudioSink') BT_AV_REMOTE_CONTROL_TARGET_SERVICE = UUID.from_16_bits(0x110C, 'A/V_RemoteControlTarget') BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE = UUID.from_16_bits(0x110D, 'AdvancedAudioDistribution') BT_AV_REMOTE_CONTROL_SERVICE = UUID.from_16_bits(0x110E, 'A/V_RemoteControl') BT_AV_REMOTE_CONTROL_CONTROLLER_SERVICE = UUID.from_16_bits(0x110F, 'A/V_RemoteControlController') BT_INTERCOM_SERVICE = UUID.from_16_bits(0x1110, 'Intercom') BT_FAX_SERVICE = UUID.from_16_bits(0x1111, 'Fax') BT_HEADSET_AUDIO_GATEWAY_SERVICE = UUID.from_16_bits(0x1112, 'Headset - Audio Gateway') BT_WAP_SERVICE = UUID.from_16_bits(0x1113, 'WAP') BT_WAP_CLIENT_SERVICE = UUID.from_16_bits(0x1114, 'WAP_CLIENT') BT_PANU_SERVICE = UUID.from_16_bits(0x1115, 'PANU') BT_NAP_SERVICE = UUID.from_16_bits(0x1116, 'NAP') BT_GN_SERVICE = UUID.from_16_bits(0x1117, 'GN') BT_DIRECT_PRINTING_SERVICE = UUID.from_16_bits(0x1118, 'DirectPrinting') BT_REFERENCE_PRINTING_SERVICE = UUID.from_16_bits(0x1119, 'ReferencePrinting') BT_BASIC_IMAGING_PROFILE_SERVICE = UUID.from_16_bits(0x111A, 'Basic Imaging Profile') BT_IMAGING_RESPONDER_SERVICE = UUID.from_16_bits(0x111B, 'ImagingResponder') BT_IMAGING_AUTOMATIC_ARCHIVE_SERVICE = UUID.from_16_bits(0x111C, 'ImagingAutomaticArchive') BT_IMAGING_REFERENCED_OBJECTS_SERVICE = UUID.from_16_bits(0x111D, 'ImagingReferencedObjects') BT_HANDSFREE_SERVICE = UUID.from_16_bits(0x111E, 'Handsfree') BT_HANDSFREE_AUDIO_GATEWAY_SERVICE = UUID.from_16_bits(0x111F, 'HandsfreeAudioGateway') BT_DIRECT_PRINTING_REFERENCE_OBJECTS_SERVICE = UUID.from_16_bits(0x1120, 'DirectPrintingReferenceObjectsService') BT_REFLECTED_UI_SERVICE = UUID.from_16_bits(0x1121, 'ReflectedUI') BT_BASIC_PRINTING_SERVICE = UUID.from_16_bits(0x1122, 'BasicPrinting') BT_PRINTING_STATUS_SERVICE = UUID.from_16_bits(0x1123, 'PrintingStatus') BT_HUMAN_INTERFACE_DEVICE_SERVICE = UUID.from_16_bits(0x1124, 'HumanInterfaceDeviceService') BT_HARDCOPY_CABLE_REPLACEMENT_SERVICE = UUID.from_16_bits(0x1125, 'HardcopyCableReplacement') BT_HCR_PRINT_SERVICE = UUID.from_16_bits(0x1126, 'HCR_Print') BT_HCR_SCAN_SERVICE = UUID.from_16_bits(0x1127, 'HCR_Scan') BT_COMMON_ISDN_ACCESS_SERVICE = UUID.from_16_bits(0x1128, 'Common_ISDN_Access') BT_SIM_ACCESS_SERVICE = UUID.from_16_bits(0x112D, 'SIM_Access') BT_PHONEBOOK_ACCESS_PCE_SERVICE = UUID.from_16_bits(0x112E, 'Phonebook Access - PCE') BT_PHONEBOOK_ACCESS_PSE_SERVICE = UUID.from_16_bits(0x112F, 'Phonebook Access - PSE') BT_PHONEBOOK_ACCESS_SERVICE = UUID.from_16_bits(0x1130, 'Phonebook Access') BT_HEADSET_HS_SERVICE = UUID.from_16_bits(0x1131, 'Headset - HS') BT_MESSAGE_ACCESS_SERVER_SERVICE = UUID.from_16_bits(0x1132, 'Message Access Server') BT_MESSAGE_NOTIFICATION_SERVER_SERVICE = UUID.from_16_bits(0x1133, 'Message Notification Server') BT_MESSAGE_ACCESS_PROFILE_SERVICE = UUID.from_16_bits(0x1134, 'Message Access Profile') BT_GNSS_SERVICE = UUID.from_16_bits(0x1135, 'GNSS') BT_GNSS_SERVER_SERVICE = UUID.from_16_bits(0x1136, 'GNSS_Server') BT_3D_DISPLAY_SERVICE = UUID.from_16_bits(0x1137, '3D Display') BT_3D_GLASSES_SERVICE = UUID.from_16_bits(0x1138, '3D Glasses') BT_3D_SYNCHRONIZATION_SERVICE = UUID.from_16_bits(0x1139, '3D Synchronization') BT_MPS_PROFILE_SERVICE = UUID.from_16_bits(0x113A, 'MPS Profile') BT_MPS_SC_SERVICE = UUID.from_16_bits(0x113B, 'MPS SC') BT_ACCESS_SERVICE_SERVICE = UUID.from_16_bits(0x113C, 'CTN Access Service') BT_CTN_NOTIFICATION_SERVICE_SERVICE = UUID.from_16_bits(0x113D, 'CTN Notification Service') BT_CTN_PROFILE_SERVICE = UUID.from_16_bits(0x113E, 'CTN Profile') BT_PNP_INFORMATION_SERVICE = UUID.from_16_bits(0x1200, 'PnPInformation') BT_GENERIC_NETWORKING_SERVICE = UUID.from_16_bits(0x1201, 'GenericNetworking') BT_GENERIC_FILE_TRANSFER_SERVICE = UUID.from_16_bits(0x1202, 'GenericFileTransfer') BT_GENERIC_AUDIO_SERVICE = UUID.from_16_bits(0x1203, 'GenericAudio') BT_GENERIC_TELEPHONY_SERVICE = UUID.from_16_bits(0x1204, 'GenericTelephony') BT_UPNP_SERVICE = UUID.from_16_bits(0x1205, 'UPNP_Service') BT_UPNP_IP_SERVICE = UUID.from_16_bits(0x1206, 'UPNP_IP_Service') BT_ESDP_UPNP_IP_PAN_SERVICE = UUID.from_16_bits(0x1300, 'ESDP_UPNP_IP_PAN') BT_ESDP_UPNP_IP_LAP_SERVICE = UUID.from_16_bits(0x1301, 'ESDP_UPNP_IP_LAP') BT_ESDP_UPNP_L2CAP_SERVICE = UUID.from_16_bits(0x1302, 'ESDP_UPNP_L2CAP') BT_VIDEO_SOURCE_SERVICE = UUID.from_16_bits(0x1303, 'VideoSource') BT_VIDEO_SINK_SERVICE = UUID.from_16_bits(0x1304, 'VideoSink') BT_VIDEO_DISTRIBUTION_SERVICE = UUID.from_16_bits(0x1305, 'VideoDistribution') BT_HDP_SERVICE = UUID.from_16_bits(0x1400, 'HDP') BT_HDP_SOURCE_SERVICE = UUID.from_16_bits(0x1401, 'HDP Source') BT_HDP_SINK_SERVICE = UUID.from_16_bits(0x1402, 'HDP Sink') # fmt: on # pylint: enable=line-too-long # ----------------------------------------------------------------------------- # DeviceClass # ----------------------------------------------------------------------------- class DeviceClass: # fmt: off # pylint: disable=line-too-long # 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 = [ 'Limited Discoverable Mode', 'LE audio', '(reserved)', 'Positioning', 'Networking', 'Rendering', 'Capturing', 'Object Transfer', 'Audio', 'Telephony', '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 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 = 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 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' } 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 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' } 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 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, } # fmt: on # pylint: enable=line-too-long @staticmethod def split_class_of_device(class_of_device): # Split the bit fields of the composite class of device value into: # (service_classes, major_device_class, minor_device_class) return ( (class_of_device >> 13 & 0x7FF), (class_of_device >> 8 & 0x1F), (class_of_device >> 2 & 0x3F), ) @staticmethod def pack_class_of_device(service_classes, major_device_class, minor_device_class): return service_classes << 13 | major_device_class << 8 | minor_device_class << 2 @staticmethod def service_class_labels(service_class_flags): return bit_flags_to_strings( service_class_flags, DeviceClass.SERVICE_CLASS_LABELS ) @staticmethod def major_device_class_name(device_class): return name_or_number(DeviceClass.MAJOR_DEVICE_CLASS_NAMES, 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) if class_names is None: return f'#{minor_device_class:02X}' return name_or_number(class_names, minor_device_class) # ----------------------------------------------------------------------------- # Appearance # ----------------------------------------------------------------------------- class Appearance: class Category(OpenIntEnum): UNKNOWN = 0x0000 PHONE = 0x0001 COMPUTER = 0x0002 WATCH = 0x0003 CLOCK = 0x0004 DISPLAY = 0x0005 REMOTE_CONTROL = 0x0006 EYE_GLASSES = 0x0007 TAG = 0x0008 KEYRING = 0x0009 MEDIA_PLAYER = 0x000A BARCODE_SCANNER = 0x000B THERMOMETER = 0x000C HEART_RATE_SENSOR = 0x000D BLOOD_PRESSURE = 0x000E HUMAN_INTERFACE_DEVICE = 0x000F GLUCOSE_METER = 0x0010 RUNNING_WALKING_SENSOR = 0x0011 CYCLING = 0x0012 CONTROL_DEVICE = 0x0013 NETWORK_DEVICE = 0x0014 SENSOR = 0x0015 LIGHT_FIXTURES = 0x0016 FAN = 0x0017 HVAC = 0x0018 AIR_CONDITIONING = 0x0019 HUMIDIFIER = 0x001A HEATING = 0x001B ACCESS_CONTROL = 0x001C MOTORIZED_DEVICE = 0x001D POWER_DEVICE = 0x001E LIGHT_SOURCE = 0x001F WINDOW_COVERING = 0x0020 AUDIO_SINK = 0x0021 AUDIO_SOURCE = 0x0022 MOTORIZED_VEHICLE = 0x0023 DOMESTIC_APPLIANCE = 0x0024 WEARABLE_AUDIO_DEVICE = 0x0025 AIRCRAFT = 0x0026 AV_EQUIPMENT = 0x0027 DISPLAY_EQUIPMENT = 0x0028 HEARING_AID = 0x0029 GAMING = 0x002A SIGNAGE = 0x002B PULSE_OXIMETER = 0x0031 WEIGHT_SCALE = 0x0032 PERSONAL_MOBILITY_DEVICE = 0x0033 CONTINUOUS_GLUCOSE_MONITOR = 0x0034 INSULIN_PUMP = 0x0035 MEDICATION_DELIVERY = 0x0036 SPIROMETER = 0x0037 OUTDOOR_SPORTS_ACTIVITY = 0x0051 class UnknownSubcategory(OpenIntEnum): GENERIC_UNKNOWN = 0x00 class PhoneSubcategory(OpenIntEnum): GENERIC_PHONE = 0x00 class ComputerSubcategory(OpenIntEnum): GENERIC_COMPUTER = 0x00 DESKTOP_WORKSTATION = 0x01 SERVER_CLASS_COMPUTER = 0x02 LAPTOP = 0x03 HANDHELD_PC_PDA = 0x04 PALM_SIZE_PC_PDA = 0x05 WEARABLE_COMPUTER = 0x06 TABLET = 0x07 DOCKING_STATION = 0x08 ALL_IN_ONE = 0x09 BLADE_SERVER = 0x0A CONVERTIBLE = 0x0B DETACHABLE = 0x0C IOT_GATEWAY = 0x0D MINI_PC = 0x0E STICK_PC = 0x0F class WatchSubcategory(OpenIntEnum): GENENERIC_WATCH = 0x00 SPORTS_WATCH = 0x01 SMARTWATCH = 0x02 class ClockSubcategory(OpenIntEnum): GENERIC_CLOCK = 0x00 class DisplaySubcategory(OpenIntEnum): GENERIC_DISPLAY = 0x00 class RemoteControlSubcategory(OpenIntEnum): GENERIC_REMOTE_CONTROL = 0x00 class EyeglassesSubcategory(OpenIntEnum): GENERIC_EYEGLASSES = 0x00 class TagSubcategory(OpenIntEnum): GENERIC_TAG = 0x00 class KeyringSubcategory(OpenIntEnum): GENERIC_KEYRING = 0x00 class MediaPlayerSubcategory(OpenIntEnum): GENERIC_MEDIA_PLAYER = 0x00 class BarcodeScannerSubcategory(OpenIntEnum): GENERIC_BARCODE_SCANNER = 0x00 class ThermometerSubcategory(OpenIntEnum): GENERIC_THERMOMETER = 0x00 EAR_THERMOMETER = 0x01 class HeartRateSensorSubcategory(OpenIntEnum): GENERIC_HEART_RATE_SENSOR = 0x00 HEART_RATE_BELT = 0x01 class BloodPressureSubcategory(OpenIntEnum): GENERIC_BLOOD_PRESSURE = 0x00 ARM_BLOOD_PRESSURE = 0x01 WRIST_BLOOD_PRESSURE = 0x02 class HumanInterfaceDeviceSubcategory(OpenIntEnum): GENERIC_HUMAN_INTERFACE_DEVICE = 0x00 KEYBOARD = 0x01 MOUSE = 0x02 JOYSTICK = 0x03 GAMEPAD = 0x04 DIGITIZER_TABLET = 0x05 CARD_READER = 0x06 DIGITAL_PEN = 0x07 BARCODE_SCANNER = 0x08 TOUCHPAD = 0x09 PRESENTATION_REMOTE = 0x0A class GlucoseMeterSubcategory(OpenIntEnum): GENERIC_GLUCOSE_METER = 0x00 class RunningWalkingSensorSubcategory(OpenIntEnum): GENERIC_RUNNING_WALKING_SENSOR = 0x00 IN_SHOE_RUNNING_WALKING_SENSOR = 0x01 ON_SHOW_RUNNING_WALKING_SENSOR = 0x02 ON_HIP_RUNNING_WALKING_SENSOR = 0x03 class CyclingSubcategory(OpenIntEnum): GENERIC_CYCLING = 0x00 CYCLING_COMPUTER = 0x01 SPEED_SENSOR = 0x02 CADENCE_SENSOR = 0x03 POWER_SENSOR = 0x04 SPEED_AND_CADENCE_SENSOR = 0x05 class ControlDeviceSubcategory(OpenIntEnum): GENERIC_CONTROL_DEVICE = 0x00 SWITCH = 0x01 MULTI_SWITCH = 0x02 BUTTON = 0x03 SLIDER = 0x04 ROTARY_SWITCH = 0x05 TOUCH_PANEL = 0x06 SINGLE_SWITCH = 0x07 DOUBLE_SWITCH = 0x08 TRIPLE_SWITCH = 0x09 BATTERY_SWITCH = 0x0A ENERGY_HARVESTING_SWITCH = 0x0B PUSH_BUTTON = 0x0C class NetworkDeviceSubcategory(OpenIntEnum): GENERIC_NETWORK_DEVICE = 0x00 ACCESS_POINT = 0x01 MESH_DEVICE = 0x02 MESH_NETWORK_PROXY = 0x03 class SensorSubcategory(OpenIntEnum): GENERIC_SENSOR = 0x00 MOTION_SENSOR = 0x01 AIR_QUALITY_SENSOR = 0x02 TEMPERATURE_SENSOR = 0x03 HUMIDITY_SENSOR = 0x04 LEAK_SENSOR = 0x05 SMOKE_SENSOR = 0x06 OCCUPANCY_SENSOR = 0x07 CONTACT_SENSOR = 0x08 CARBON_MONOXIDE_SENSOR = 0x09 CARBON_DIOXIDE_SENSOR = 0x0A AMBIENT_LIGHT_SENSOR = 0x0B ENERGY_SENSOR = 0x0C COLOR_LIGHT_SENSOR = 0x0D RAIN_SENSOR = 0x0E FIRE_SENSOR = 0x0F WIND_SENSOR = 0x10 PROXIMITY_SENSOR = 0x11 MULTI_SENSOR = 0x12 FLUSH_MOUNTED_SENSOR = 0x13 CEILING_MOUNTED_SENSOR = 0x14 WALL_MOUNTED_SENSOR = 0x15 MULTISENSOR = 0x16 ENERGY_METER = 0x17 FLAME_DETECTOR = 0x18 VEHICLE_TIRE_PRESSURE_SENSOR = 0x19 class LightFixturesSubcategory(OpenIntEnum): GENERIC_LIGHT_FIXTURES = 0x00 WALL_LIGHT = 0x01 CEILING_LIGHT = 0x02 FLOOR_LIGHT = 0x03 CABINET_LIGHT = 0x04 DESK_LIGHT = 0x05 TROFFER_LIGHT = 0x06 PENDANT_LIGHT = 0x07 IN_GROUND_LIGHT = 0x08 FLOOD_LIGHT = 0x09 UNDERWATER_LIGHT = 0x0A BOLLARD_WITH_LIGHT = 0x0B PATHWAY_LIGHT = 0x0C GARDEN_LIGHT = 0x0D POLE_TOP_LIGHT = 0x0E SPOTLIGHT = 0x0F LINEAR_LIGHT = 0x10 STREET_LIGHT = 0x11 SHELVES_LIGHT = 0x12 BAY_LIGHT = 0x013 EMERGENCY_EXIT_LIGHT = 0x14 LIGHT_CONTROLLER = 0x15 LIGHT_DRIVER = 0x16 BULB = 0x17 LOW_BAY_LIGHT = 0x18 HIGH_BAY_LIGHT = 0x19 class FanSubcategory(OpenIntEnum): GENERIC_FAN = 0x00 CEILING_FAN = 0x01 AXIAL_FAN = 0x02 EXHAUST_FAN = 0x03 PEDESTAL_FAN = 0x04 DESK_FAN = 0x05 WALL_FAN = 0x06 class HvacSubcategory(OpenIntEnum): GENERIC_HVAC = 0x00 THERMOSTAT = 0x01 HUMIDIFIER = 0x02 DEHUMIDIFIER = 0x03 HEATER = 0x04 RADIATOR = 0x05 BOILER = 0x06 HEAT_PUMP = 0x07 INFRARED_HEATER = 0x08 RADIANT_PANEL_HEATER = 0x09 FAN_HEATER = 0x0A AIR_CURTAIN = 0x0B class AirConditioningSubcategory(OpenIntEnum): GENERIC_AIR_CONDITIONING = 0x00 class HumidifierSubcategory(OpenIntEnum): GENERIC_HUMIDIFIER = 0x00 class HeatingSubcategory(OpenIntEnum): GENERIC_HEATING = 0x00 RADIATOR = 0x01 BOILER = 0x02 HEAT_PUMP = 0x03 INFRARED_HEATER = 0x04 RADIANT_PANEL_HEATER = 0x05 FAN_HEATER = 0x06 AIR_CURTAIN = 0x07 class AccessControlSubcategory(OpenIntEnum): GENERIC_ACCESS_CONTROL = 0x00 ACCESS_DOOR = 0x01 GARAGE_DOOR = 0x02 EMERGENCY_EXIT_DOOR = 0x03 ACCESS_LOCK = 0x04 ELEVATOR = 0x05 WINDOW = 0x06 ENTRANCE_GATE = 0x07 DOOR_LOCK = 0x08 LOCKER = 0x09 class MotorizedDeviceSubcategory(OpenIntEnum): GENERIC_MOTORIZED_DEVICE = 0x00 MOTORIZED_GATE = 0x01 AWNING = 0x02 BLINDS_OR_SHADES = 0x03 CURTAINS = 0x04 SCREEN = 0x05 class PowerDeviceSubcategory(OpenIntEnum): GENERIC_POWER_DEVICE = 0x00 POWER_OUTLET = 0x01 POWER_STRIP = 0x02 PLUG = 0x03 POWER_SUPPLY = 0x04 LED_DRIVER = 0x05 FLUORESCENT_LAMP_GEAR = 0x06 HID_LAMP_GEAR = 0x07 CHARGE_CASE = 0x08 POWER_BANK = 0x09 class LightSourceSubcategory(OpenIntEnum): GENERIC_LIGHT_SOURCE = 0x00 INCANDESCENT_LIGHT_BULB = 0x01 LED_LAMP = 0x02 HID_LAMP = 0x03 FLUORESCENT_LAMP = 0x04 LED_ARRAY = 0x05 MULTI_COLOR_LED_ARRAY = 0x06 LOW_VOLTAGE_HALOGEN = 0x07 ORGANIC_LIGHT_EMITTING_DIODE = 0x08 class WindowCoveringSubcategory(OpenIntEnum): GENERIC_WINDOW_COVERING = 0x00 WINDOW_SHADES = 0x01 WINDOW_BLINDS = 0x02 WINDOW_AWNING = 0x03 WINDOW_CURTAIN = 0x04 EXTERIOR_SHUTTER = 0x05 EXTERIOR_SCREEN = 0x06 class AudioSinkSubcategory(OpenIntEnum): GENERIC_AUDIO_SINK = 0x00 STANDALONE_SPEAKER = 0x01 SOUNDBAR = 0x02 BOOKSHELF_SPEAKER = 0x03 STANDMOUNTED_SPEAKER = 0x04 SPEAKERPHONE = 0x05 class AudioSourceSubcategory(OpenIntEnum): GENERIC_AUDIO_SOURCE = 0x00 MICROPHONE = 0x01 ALARM = 0x02 BELL = 0x03 HORN = 0x04 BROADCASTING_DEVICE = 0x05 SERVICE_DESK = 0x06 KIOSK = 0x07 BROADCASTING_ROOM = 0x08 AUDITORIUM = 0x09 class MotorizedVehicleSubcategory(OpenIntEnum): GENERIC_MOTORIZED_VEHICLE = 0x00 CAR = 0x01 LARGE_GOODS_VEHICLE = 0x02 TWO_WHEELED_VEHICLE = 0x03 MOTORBIKE = 0x04 SCOOTER = 0x05 MOPED = 0x06 THREE_WHEELED_VEHICLE = 0x07 LIGHT_VEHICLE = 0x08 QUAD_BIKE = 0x09 MINIBUS = 0x0A BUS = 0x0B TROLLEY = 0x0C AGRICULTURAL_VEHICLE = 0x0D CAMPER_CARAVAN = 0x0E RECREATIONAL_VEHICLE_MOTOR_HOME = 0x0F class DomesticApplianceSubcategory(OpenIntEnum): GENERIC_DOMESTIC_APPLIANCE = 0x00 REFRIGERATOR = 0x01 FREEZER = 0x02 OVEN = 0x03 MICROWAVE = 0x04 TOASTER = 0x05 WASHING_MACHINE = 0x06 DRYER = 0x07 COFFEE_MAKER = 0x08 CLOTHES_IRON = 0x09 CURLING_IRON = 0x0A HAIR_DRYER = 0x0B VACUUM_CLEANER = 0x0C ROBOTIC_VACUUM_CLEANER = 0x0D RICE_COOKER = 0x0E CLOTHES_STEAMER = 0x0F class WearableAudioDeviceSubcategory(OpenIntEnum): GENERIC_WEARABLE_AUDIO_DEVICE = 0x00 EARBUD = 0x01 HEADSET = 0x02 HEADPHONES = 0x03 NECK_BAND = 0x04 class AircraftSubcategory(OpenIntEnum): GENERIC_AIRCRAFT = 0x00 LIGHT_AIRCRAFT = 0x01 MICROLIGHT = 0x02 PARAGLIDER = 0x03 LARGE_PASSENGER_AIRCRAFT = 0x04 class AvEquipmentSubcategory(OpenIntEnum): GENERIC_AV_EQUIPMENT = 0x00 AMPLIFIER = 0x01 RECEIVER = 0x02 RADIO = 0x03 TUNER = 0x04 TURNTABLE = 0x05 CD_PLAYER = 0x06 DVD_PLAYER = 0x07 BLUERAY_PLAYER = 0x08 OPTICAL_DISC_PLAYER = 0x09 SET_TOP_BOX = 0x0A class DisplayEquipmentSubcategory(OpenIntEnum): GENERIC_DISPLAY_EQUIPMENT = 0x00 TELEVISION = 0x01 MONITOR = 0x02 PROJECTOR = 0x03 class HearingAidSubcategory(OpenIntEnum): GENERIC_HEARING_AID = 0x00 IN_EAR_HEARING_AID = 0x01 BEHIND_EAR_HEARING_AID = 0x02 COCHLEAR_IMPLANT = 0x03 class GamingSubcategory(OpenIntEnum): GENERIC_GAMING = 0x00 HOME_VIDEO_GAME_CONSOLE = 0x01 PORTABLE_HANDHELD_CONSOLE = 0x02 class SignageSubcategory(OpenIntEnum): GENERIC_SIGNAGE = 0x00 DIGITAL_SIGNAGE = 0x01 ELECTRONIC_LABEL = 0x02 class PulseOximeterSubcategory(OpenIntEnum): GENERIC_PULSE_OXIMETER = 0x00 FINGERTIP_PULSE_OXIMETER = 0x01 WRIST_WORN_PULSE_OXIMETER = 0x02 class WeightScaleSubcategory(OpenIntEnum): GENERIC_WEIGHT_SCALE = 0x00 class PersonalMobilityDeviceSubcategory(OpenIntEnum): GENERIC_PERSONAL_MOBILITY_DEVICE = 0x00 POWERED_WHEELCHAIR = 0x01 MOBILITY_SCOOTER = 0x02 class ContinuousGlucoseMonitorSubcategory(OpenIntEnum): GENERIC_CONTINUOUS_GLUCOSE_MONITOR = 0x00 class InsulinPumpSubcategory(OpenIntEnum): GENERIC_INSULIN_PUMP = 0x00 INSULIN_PUMP_DURABLE_PUMP = 0x01 INSULIN_PUMP_PATCH_PUMP = 0x02 INSULIN_PEN = 0x03 class MedicationDeliverySubcategory(OpenIntEnum): GENERIC_MEDICATION_DELIVERY = 0x00 class SpirometerSubcategory(OpenIntEnum): GENERIC_SPIROMETER = 0x00 HANDHELD_SPIROMETER = 0x01 class OutdoorSportsActivitySubcategory(OpenIntEnum): GENERIC_OUTDOOR_SPORTS_ACTIVITY = 0x00 LOCATION_DISPLAY = 0x01 LOCATION_AND_NAVIGATION_DISPLAY = 0x02 LOCATION_POD = 0x03 LOCATION_AND_NAVIGATION_POD = 0x04 class _OpenSubcategory(OpenIntEnum): GENERIC = 0x00 SUBCATEGORY_CLASSES = { Category.UNKNOWN: UnknownSubcategory, Category.PHONE: PhoneSubcategory, Category.COMPUTER: ComputerSubcategory, Category.WATCH: WatchSubcategory, Category.CLOCK: ClockSubcategory, Category.DISPLAY: DisplaySubcategory, Category.REMOTE_CONTROL: RemoteControlSubcategory, Category.EYE_GLASSES: EyeglassesSubcategory, Category.TAG: TagSubcategory, Category.KEYRING: KeyringSubcategory, Category.MEDIA_PLAYER: MediaPlayerSubcategory, Category.BARCODE_SCANNER: BarcodeScannerSubcategory, Category.THERMOMETER: ThermometerSubcategory, Category.HEART_RATE_SENSOR: HeartRateSensorSubcategory, Category.BLOOD_PRESSURE: BloodPressureSubcategory, Category.HUMAN_INTERFACE_DEVICE: HumanInterfaceDeviceSubcategory, Category.GLUCOSE_METER: GlucoseMeterSubcategory, Category.RUNNING_WALKING_SENSOR: RunningWalkingSensorSubcategory, Category.CYCLING: CyclingSubcategory, Category.CONTROL_DEVICE: ControlDeviceSubcategory, Category.NETWORK_DEVICE: NetworkDeviceSubcategory, Category.SENSOR: SensorSubcategory, Category.LIGHT_FIXTURES: LightFixturesSubcategory, Category.FAN: FanSubcategory, Category.HVAC: HvacSubcategory, Category.AIR_CONDITIONING: AirConditioningSubcategory, Category.HUMIDIFIER: HumidifierSubcategory, Category.HEATING: HeatingSubcategory, Category.ACCESS_CONTROL: AccessControlSubcategory, Category.MOTORIZED_DEVICE: MotorizedDeviceSubcategory, Category.POWER_DEVICE: PowerDeviceSubcategory, Category.LIGHT_SOURCE: LightSourceSubcategory, Category.WINDOW_COVERING: WindowCoveringSubcategory, Category.AUDIO_SINK: AudioSinkSubcategory, Category.AUDIO_SOURCE: AudioSourceSubcategory, Category.MOTORIZED_VEHICLE: MotorizedVehicleSubcategory, Category.DOMESTIC_APPLIANCE: DomesticApplianceSubcategory, Category.WEARABLE_AUDIO_DEVICE: WearableAudioDeviceSubcategory, Category.AIRCRAFT: AircraftSubcategory, Category.AV_EQUIPMENT: AvEquipmentSubcategory, Category.DISPLAY_EQUIPMENT: DisplayEquipmentSubcategory, Category.HEARING_AID: HearingAidSubcategory, Category.GAMING: GamingSubcategory, Category.SIGNAGE: SignageSubcategory, Category.PULSE_OXIMETER: PulseOximeterSubcategory, Category.WEIGHT_SCALE: WeightScaleSubcategory, Category.PERSONAL_MOBILITY_DEVICE: PersonalMobilityDeviceSubcategory, Category.CONTINUOUS_GLUCOSE_MONITOR: ContinuousGlucoseMonitorSubcategory, Category.INSULIN_PUMP: InsulinPumpSubcategory, Category.MEDICATION_DELIVERY: MedicationDeliverySubcategory, Category.SPIROMETER: SpirometerSubcategory, Category.OUTDOOR_SPORTS_ACTIVITY: OutdoorSportsActivitySubcategory, } category: Category subcategory: enum.IntEnum @classmethod def from_int(cls, appearance: int) -> Self: category = cls.Category(appearance >> 6) return cls(category, appearance & 0x3F) def __init__(self, category: Category, subcategory: int) -> None: self.category = category if subcategory_class := self.SUBCATEGORY_CLASSES.get(category): self.subcategory = subcategory_class(subcategory) else: self.subcategory = self._OpenSubcategory(subcategory) def __int__(self) -> int: return self.category << 6 | self.subcategory def __repr__(self) -> str: return ( 'Appearance(' f'category={self.category.name}, ' f'subcategory={self.subcategory.name}' ')' ) def __str__(self) -> str: return f'{self.category.name}/{self.subcategory.name}' # ----------------------------------------------------------------------------- # Advertising Data # ----------------------------------------------------------------------------- AdvertisingDataObject = Union[ List[UUID], Tuple[UUID, bytes], bytes, str, int, Tuple[int, int], Tuple[int, bytes], Appearance, ] class AdvertisingData: # fmt: off # pylint: disable=line-too-long FLAGS = 0x01 INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS = 0x02 COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS = 0x03 INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS = 0x04 COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS = 0x05 INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS = 0x06 COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS = 0x07 SHORTENED_LOCAL_NAME = 0x08 COMPLETE_LOCAL_NAME = 0x09 TX_POWER_LEVEL = 0x0A CLASS_OF_DEVICE = 0x0D SIMPLE_PAIRING_HASH_C = 0x0E SIMPLE_PAIRING_HASH_C_192 = 0x0E SIMPLE_PAIRING_RANDOMIZER_R = 0x0F SIMPLE_PAIRING_RANDOMIZER_R_192 = 0x0F DEVICE_ID = 0x10 SECURITY_MANAGER_TK_VALUE = 0x10 SECURITY_MANAGER_OUT_OF_BAND_FLAGS = 0x11 PERIPHERAL_CONNECTION_INTERVAL_RANGE = 0x12 LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS = 0x14 LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS = 0x15 SERVICE_DATA = 0x16 SERVICE_DATA_16_BIT_UUID = 0x16 PUBLIC_TARGET_ADDRESS = 0x17 RANDOM_TARGET_ADDRESS = 0x18 APPEARANCE = 0x19 ADVERTISING_INTERVAL = 0x1A LE_BLUETOOTH_DEVICE_ADDRESS = 0x1B LE_ROLE = 0x1C SIMPLE_PAIRING_HASH_C_256 = 0x1D SIMPLE_PAIRING_RANDOMIZER_R_256 = 0x1E LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS = 0x1F SERVICE_DATA_32_BIT_UUID = 0x20 SERVICE_DATA_128_BIT_UUID = 0x21 LE_SECURE_CONNECTIONS_CONFIRMATION_VALUE = 0x22 LE_SECURE_CONNECTIONS_RANDOM_VALUE = 0x23 URI = 0x24 INDOOR_POSITIONING = 0x25 TRANSPORT_DISCOVERY_DATA = 0x26 LE_SUPPORTED_FEATURES = 0x27 CHANNEL_MAP_UPDATE_INDICATION = 0x28 PB_ADV = 0x29 MESH_MESSAGE = 0x2A MESH_BEACON = 0x2B BIGINFO = 0x2C BROADCAST_CODE = 0x2D RESOLVABLE_SET_IDENTIFIER = 0x2E ADVERTISING_INTERVAL_LONG = 0x2F BROADCAST_NAME = 0x30 ENCRYPTED_ADVERTISING_DATA = 0X31 PERIODIC_ADVERTISING_RESPONSE_TIMING_INFORMATION = 0X32 ELECTRONIC_SHELF_LABEL = 0X34 THREE_D_INFORMATION_DATA = 0x3D MANUFACTURER_SPECIFIC_DATA = 0xFF AD_TYPE_NAMES = { FLAGS: 'FLAGS', INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS: 'INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS', COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS: 'COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS', INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS: 'INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS', COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS: 'COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS', INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS: 'INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS', COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS: 'COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS', SHORTENED_LOCAL_NAME: 'SHORTENED_LOCAL_NAME', COMPLETE_LOCAL_NAME: 'COMPLETE_LOCAL_NAME', TX_POWER_LEVEL: 'TX_POWER_LEVEL', CLASS_OF_DEVICE: 'CLASS_OF_DEVICE', SIMPLE_PAIRING_HASH_C: 'SIMPLE_PAIRING_HASH_C', SIMPLE_PAIRING_HASH_C_192: 'SIMPLE_PAIRING_HASH_C_192', SIMPLE_PAIRING_RANDOMIZER_R: 'SIMPLE_PAIRING_RANDOMIZER_R', SIMPLE_PAIRING_RANDOMIZER_R_192: 'SIMPLE_PAIRING_RANDOMIZER_R_192', DEVICE_ID: 'DEVICE_ID', SECURITY_MANAGER_TK_VALUE: 'SECURITY_MANAGER_TK_VALUE', SECURITY_MANAGER_OUT_OF_BAND_FLAGS: 'SECURITY_MANAGER_OUT_OF_BAND_FLAGS', PERIPHERAL_CONNECTION_INTERVAL_RANGE: 'PERIPHERAL_CONNECTION_INTERVAL_RANGE', LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS: 'LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS', LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS: 'LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS', SERVICE_DATA_16_BIT_UUID: 'SERVICE_DATA_16_BIT_UUID', PUBLIC_TARGET_ADDRESS: 'PUBLIC_TARGET_ADDRESS', RANDOM_TARGET_ADDRESS: 'RANDOM_TARGET_ADDRESS', APPEARANCE: 'APPEARANCE', ADVERTISING_INTERVAL: 'ADVERTISING_INTERVAL', LE_BLUETOOTH_DEVICE_ADDRESS: 'LE_BLUETOOTH_DEVICE_ADDRESS', LE_ROLE: 'LE_ROLE', SIMPLE_PAIRING_HASH_C_256: 'SIMPLE_PAIRING_HASH_C_256', SIMPLE_PAIRING_RANDOMIZER_R_256: 'SIMPLE_PAIRING_RANDOMIZER_R_256', LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS: 'LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS', SERVICE_DATA_32_BIT_UUID: 'SERVICE_DATA_32_BIT_UUID', SERVICE_DATA_128_BIT_UUID: 'SERVICE_DATA_128_BIT_UUID', LE_SECURE_CONNECTIONS_CONFIRMATION_VALUE: 'LE_SECURE_CONNECTIONS_CONFIRMATION_VALUE', LE_SECURE_CONNECTIONS_RANDOM_VALUE: 'LE_SECURE_CONNECTIONS_RANDOM_VALUE', URI: 'URI', INDOOR_POSITIONING: 'INDOOR_POSITIONING', TRANSPORT_DISCOVERY_DATA: 'TRANSPORT_DISCOVERY_DATA', LE_SUPPORTED_FEATURES: 'LE_SUPPORTED_FEATURES', CHANNEL_MAP_UPDATE_INDICATION: 'CHANNEL_MAP_UPDATE_INDICATION', PB_ADV: 'PB_ADV', MESH_MESSAGE: 'MESH_MESSAGE', MESH_BEACON: 'MESH_BEACON', BIGINFO: 'BIGINFO', BROADCAST_CODE: 'BROADCAST_CODE', RESOLVABLE_SET_IDENTIFIER: 'RESOLVABLE_SET_IDENTIFIER', ADVERTISING_INTERVAL_LONG: 'ADVERTISING_INTERVAL_LONG', BROADCAST_NAME: 'BROADCAST_NAME', ENCRYPTED_ADVERTISING_DATA: 'ENCRYPTED_ADVERTISING_DATA', PERIODIC_ADVERTISING_RESPONSE_TIMING_INFORMATION: 'PERIODIC_ADVERTISING_RESPONSE_TIMING_INFORMATION', ELECTRONIC_SHELF_LABEL: 'ELECTRONIC_SHELF_LABEL', THREE_D_INFORMATION_DATA: 'THREE_D_INFORMATION_DATA', MANUFACTURER_SPECIFIC_DATA: 'MANUFACTURER_SPECIFIC_DATA' } LE_LIMITED_DISCOVERABLE_MODE_FLAG = 0x01 LE_GENERAL_DISCOVERABLE_MODE_FLAG = 0x02 BR_EDR_NOT_SUPPORTED_FLAG = 0x04 BR_EDR_CONTROLLER_FLAG = 0x08 BR_EDR_HOST_FLAG = 0x10 ad_structures: List[Tuple[int, bytes]] # fmt: on # pylint: enable=line-too-long def __init__(self, ad_structures: Optional[List[Tuple[int, bytes]]] = None) -> None: if ad_structures is None: ad_structures = [] self.ad_structures = ad_structures[:] @classmethod def from_bytes(cls, data: bytes) -> AdvertisingData: instance = AdvertisingData() instance.append(data) return instance @staticmethod def flags_to_string(flags, short=False): flag_names = ( ['LE Limited', 'LE General', 'No BR/EDR', 'BR/EDR C', 'BR/EDR H'] if short else [ 'LE Limited Discoverable Mode', 'LE General Discoverable Mode', 'BR/EDR Not Supported', 'Simultaneous LE and BR/EDR (Controller)', 'Simultaneous LE and BR/EDR (Host)', ] ) return ','.join(bit_flags_to_strings(flags, flag_names)) @staticmethod def uuid_list_to_objects(ad_data: bytes, uuid_size: int) -> List[UUID]: uuids = [] offset = 0 while (offset + uuid_size) <= len(ad_data): uuids.append(UUID.from_bytes(ad_data[offset : offset + uuid_size])) offset += uuid_size return uuids @staticmethod def uuid_list_to_string(ad_data, uuid_size): return ', '.join( [ str(uuid) for uuid in AdvertisingData.uuid_list_to_objects(ad_data, uuid_size) ] ) @staticmethod def ad_data_to_string(ad_type, ad_data): if ad_type == AdvertisingData.FLAGS: ad_type_str = 'Flags' ad_data_str = AdvertisingData.flags_to_string(ad_data[0], short=True) elif ad_type == AdvertisingData.COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS: ad_type_str = 'Complete List of 16-bit Service Class UUIDs' ad_data_str = AdvertisingData.uuid_list_to_string(ad_data, 2) elif ad_type == AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS: ad_type_str = 'Incomplete List of 16-bit Service Class UUIDs' ad_data_str = AdvertisingData.uuid_list_to_string(ad_data, 2) elif ad_type == AdvertisingData.COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS: ad_type_str = 'Complete List of 32-bit Service Class UUIDs' ad_data_str = AdvertisingData.uuid_list_to_string(ad_data, 4) elif ad_type == AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS: ad_type_str = 'Incomplete List of 32-bit Service Class UUIDs' ad_data_str = AdvertisingData.uuid_list_to_string(ad_data, 4) elif ad_type == AdvertisingData.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS: ad_type_str = 'Complete List of 128-bit Service Class UUIDs' ad_data_str = AdvertisingData.uuid_list_to_string(ad_data, 16) elif ad_type == AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS: ad_type_str = 'Incomplete List of 128-bit Service Class UUIDs' ad_data_str = AdvertisingData.uuid_list_to_string(ad_data, 16) elif ad_type == AdvertisingData.SERVICE_DATA_16_BIT_UUID: ad_type_str = 'Service Data' uuid = UUID.from_bytes(ad_data[:2]) ad_data_str = f'service={uuid}, data={ad_data[2:].hex()}' elif ad_type == AdvertisingData.SERVICE_DATA_32_BIT_UUID: ad_type_str = 'Service Data' uuid = UUID.from_bytes(ad_data[:4]) ad_data_str = f'service={uuid}, data={ad_data[4:].hex()}' elif ad_type == AdvertisingData.SERVICE_DATA_128_BIT_UUID: ad_type_str = 'Service Data' uuid = UUID.from_bytes(ad_data[:16]) ad_data_str = f'service={uuid}, data={ad_data[16:].hex()}' elif ad_type == AdvertisingData.SHORTENED_LOCAL_NAME: ad_type_str = 'Shortened Local Name' ad_data_str = f'"{ad_data.decode("utf-8")}"' elif ad_type == AdvertisingData.COMPLETE_LOCAL_NAME: ad_type_str = 'Complete Local Name' ad_data_str = f'"{ad_data.decode("utf-8")}"' elif ad_type == AdvertisingData.TX_POWER_LEVEL: ad_type_str = 'TX Power Level' ad_data_str = str(ad_data[0]) elif ad_type == AdvertisingData.MANUFACTURER_SPECIFIC_DATA: ad_type_str = 'Manufacturer Specific Data' company_id = struct.unpack_from(' AdvertisingDataObject: if ad_type in ( AdvertisingData.COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, AdvertisingData.LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS, ): return AdvertisingData.uuid_list_to_objects(ad_data, 2) if ad_type in ( AdvertisingData.COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS, AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS, AdvertisingData.LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS, ): return AdvertisingData.uuid_list_to_objects(ad_data, 4) if ad_type in ( AdvertisingData.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, AdvertisingData.LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS, ): return AdvertisingData.uuid_list_to_objects(ad_data, 16) if ad_type == AdvertisingData.SERVICE_DATA_16_BIT_UUID: return (UUID.from_bytes(ad_data[:2]), ad_data[2:]) if ad_type == AdvertisingData.SERVICE_DATA_32_BIT_UUID: return (UUID.from_bytes(ad_data[:4]), ad_data[4:]) if ad_type == AdvertisingData.SERVICE_DATA_128_BIT_UUID: return (UUID.from_bytes(ad_data[:16]), ad_data[16:]) if ad_type in ( AdvertisingData.SHORTENED_LOCAL_NAME, AdvertisingData.COMPLETE_LOCAL_NAME, AdvertisingData.URI, AdvertisingData.BROADCAST_NAME, ): return ad_data.decode("utf-8") if ad_type in (AdvertisingData.TX_POWER_LEVEL, AdvertisingData.FLAGS): return cast(int, struct.unpack('B', ad_data)[0]) if ad_type in (AdvertisingData.ADVERTISING_INTERVAL,): return cast(int, struct.unpack(' None: offset = 0 while offset + 1 < len(data): length = data[offset] offset += 1 if length > 0: ad_type = data[offset] ad_data = data[offset + 1 : offset + length] self.ad_structures.append((ad_type, ad_data)) offset += length def get_all(self, type_id: int, raw: bool = False) -> List[AdvertisingDataObject]: ''' Get Advertising Data Structure(s) with a given type Returns a (possibly empty) list of matches. ''' def process_ad_data(ad_data: bytes) -> AdvertisingDataObject: return ad_data if raw else self.ad_data_to_object(type_id, ad_data) return [process_ad_data(ad[1]) for ad in self.ad_structures if ad[0] == type_id] def get(self, type_id: int, raw: bool = False) -> Optional[AdvertisingDataObject]: ''' Get Advertising Data Structure(s) with a given type Returns the first entry, or None if no structure matches. ''' all_objects = self.get_all(type_id, raw=raw) return all_objects[0] if all_objects else None def __bytes__(self): return b''.join( [bytes([len(x[1]) + 1, x[0]]) + x[1] for x in self.ad_structures] ) def to_string(self, separator=', '): return separator.join( [AdvertisingData.ad_data_to_string(x[0], x[1]) for x in self.ad_structures] ) def __str__(self): return self.to_string() # ----------------------------------------------------------------------------- # Connection Parameters # ----------------------------------------------------------------------------- class ConnectionParameters: def __init__(self, connection_interval, peripheral_latency, supervision_timeout): self.connection_interval = connection_interval self.peripheral_latency = peripheral_latency self.supervision_timeout = supervision_timeout def __str__(self): return ( f'ConnectionParameters(connection_interval={self.connection_interval}, ' f'peripheral_latency={self.peripheral_latency}, ' f'supervision_timeout={self.supervision_timeout}' ) # ----------------------------------------------------------------------------- # Connection PHY # ----------------------------------------------------------------------------- class ConnectionPHY: def __init__(self, tx_phy, rx_phy): self.tx_phy = tx_phy self.rx_phy = rx_phy def __str__(self): return f'ConnectionPHY(tx_phy={self.tx_phy}, rx_phy={self.rx_phy})' # ----------------------------------------------------------------------------- # LE Role # ----------------------------------------------------------------------------- class LeRole(enum.IntEnum): PERIPHERAL_ONLY = 0x00 CENTRAL_ONLY = 0x01 BOTH_PERIPHERAL_PREFERRED = 0x02 BOTH_CENTRAL_PREFERRED = 0x03