Coverage for src / bluetooth_sig / types / advertising.py: 93%
231 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 20:14 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 20:14 +0000
1"""BLE Advertising data types and parsing utilities.
3Organization:
4 1. Core PDU Types and Enums - Low-level PDU structure definitions
5 2. Advertising Data Type Registry - AD Type metadata
6 3. Advertising Flags - Device discovery and capabilities flags
7 4. PDU and Header Structures - Structured PDU representations
8 5. Parsed Advertising Data - High-level parsed advertisement content
9"""
11from __future__ import annotations
13from enum import IntEnum, IntFlag
14from typing import Any
16import msgspec
18from bluetooth_sig.types.appearance import AppearanceData
19from bluetooth_sig.types.registry.class_of_device import ClassOfDeviceInfo
20from bluetooth_sig.types.uri import URIData
21from bluetooth_sig.types.uuid import BluetoothUUID
24class PDUType(IntEnum):
25 """BLE Advertising PDU Types (Core Spec Vol 6, Part B, Section 2.3)."""
27 ADV_IND = 0x00
28 ADV_DIRECT_IND = 0x01
29 ADV_NONCONN_IND = 0x02
30 SCAN_REQ = 0x03
31 SCAN_RSP = 0x04
32 CONNECT_IND = 0x05
33 ADV_SCAN_IND = 0x06
34 ADV_EXT_IND = 0x07
35 ADV_AUX_IND = 0x08
37 @property
38 def is_extended_advertising(self) -> bool:
39 """Check if this is an extended advertising PDU."""
40 return self in (PDUType.ADV_EXT_IND, PDUType.ADV_AUX_IND)
42 @property
43 def is_legacy_advertising(self) -> bool:
44 """Check if this is a legacy advertising PDU."""
45 return self in (
46 PDUType.ADV_IND,
47 PDUType.ADV_DIRECT_IND,
48 PDUType.ADV_NONCONN_IND,
49 PDUType.SCAN_REQ,
50 PDUType.SCAN_RSP,
51 PDUType.CONNECT_IND,
52 PDUType.ADV_SCAN_IND,
53 )
56class PDUHeaderFlags(IntFlag):
57 """BLE PDU header bit masks for parsing operations.
59 These masks are pre-positioned to their correct bit locations,
60 eliminating the need for shifts during extraction.
61 """
63 TYPE_MASK = 0x0F
64 RFU_BIT_4 = 0x10
65 RFU_BIT_5 = 0x20
66 TX_ADD_MASK = 0x40
67 RX_ADD_MASK = 0x80
69 @classmethod
70 def extract_bits(cls, header: int, mask: int) -> int | bool:
71 """Extract bits from header using the specified mask.
73 Returns int for multi-bit masks, bool for single-bit masks.
74 """
75 value = header & mask
76 # If mask has multiple bits set, return the raw value
77 # If mask has only one bit set, return boolean
78 if mask & (mask - 1): # Check if mask has multiple bits set
79 return value
80 return bool(value)
82 @classmethod
83 def extract_pdu_type(cls, header: int) -> PDUType:
84 """Extract PDU type from header byte and return as PDUType enum."""
85 raw_type = int(cls.extract_bits(header, cls.TYPE_MASK))
86 try:
87 return PDUType(raw_type)
88 except ValueError as exc:
89 # For unknown PDU types, we could either raise or return a special value
90 raise ValueError(f"Unknown PDU type: 0x{raw_type:02X}") from exc
92 @classmethod
93 def extract_tx_add(cls, header: int) -> bool:
94 """Extract TX address type from header."""
95 return bool(cls.extract_bits(header, cls.TX_ADD_MASK))
97 @classmethod
98 def extract_rx_add(cls, header: int) -> bool:
99 """Extract RX address type from header."""
100 return bool(cls.extract_bits(header, cls.RX_ADD_MASK))
103class PDULayout:
104 """BLE PDU structure size and offset constants.
106 Defines the sizes and offsets of fields within BLE PDU structures
107 following Bluetooth Core Spec Vol 6, Part B.
108 """
110 # PDU Size constants
111 BLE_ADDR: int = 6
112 AUX_PTR: int = 3
113 ADV_DATA_INFO: int = 2
114 CTE_INFO: int = 1
115 SYNC_INFO: int = 18
116 TX_POWER: int = 1
117 PDU_HEADER: int = 2
118 MIN_EXTENDED_PDU: int = 3
119 EXT_HEADER_LENGTH: int = 1
121 # PDU Offsets
122 EXTENDED_HEADER_START: int = 3
123 ADV_MODE: int = 1
124 ADV_ADDR_OFFSET: int = 2
125 TARGET_ADDR_OFFSET: int = 2
126 CTE_INFO_OFFSET: int = 1
127 ADV_DATA_INFO_OFFSET: int = 2
128 AUX_PTR_OFFSET: int = 3
129 SYNC_INFO_OFFSET: int = 18
130 TX_POWER_OFFSET: int = 1
131 PDU_LENGTH_OFFSET: int = 2
134class ExtendedHeaderFlags(IntEnum):
135 """Extended advertising header field presence flags (BLE 5.0+).
137 Each flag indicates whether a corresponding field is present
138 in the extended advertising header.
139 """
141 ADV_ADDR = 0x01
142 TARGET_ADDR = 0x02
143 CTE_INFO = 0x04
144 ADV_DATA_INFO = 0x08
145 AUX_PTR = 0x10
146 SYNC_INFO = 0x20
147 TX_POWER = 0x40
148 ACAD = 0x80
151class BLEAdvertisingFlags(IntFlag):
152 """BLE Advertising Flags (Core Spec Supplement, Part A, Section 1.3).
154 These flags indicate the discoverable mode and capabilities of the advertising device.
155 """
157 LE_LIMITED_DISCOVERABLE_MODE = 0x01
158 LE_GENERAL_DISCOVERABLE_MODE = 0x02
159 BR_EDR_NOT_SUPPORTED = 0x04
160 SIMULTANEOUS_LE_BR_EDR_CONTROLLER = 0x08
161 SIMULTANEOUS_LE_BR_EDR_HOST = 0x10
162 RESERVED_BIT_5 = 0x20
163 RESERVED_BIT_6 = 0x40
164 RESERVED_BIT_7 = 0x80
167class BLEExtendedHeader(msgspec.Struct, kw_only=True):
168 """Extended Advertising Header fields (BLE 5.0+)."""
170 extended_header_length: int = 0
171 adv_mode: int = 0
173 extended_advertiser_address: bytes = b""
174 extended_target_address: bytes = b""
175 cte_info: bytes = b""
176 advertising_data_info: bytes = b""
177 auxiliary_pointer: bytes = b""
178 sync_info: bytes = b""
179 tx_power: int | None = None
180 additional_controller_advertising_data: bytes = b""
182 @property
183 def has_extended_advertiser_address(self) -> bool:
184 """Check if extended advertiser address is present."""
185 return bool(self.adv_mode & ExtendedHeaderFlags.ADV_ADDR)
187 @property
188 def has_extended_target_address(self) -> bool:
189 """Check if extended target address is present."""
190 return bool(self.adv_mode & ExtendedHeaderFlags.TARGET_ADDR)
192 @property
193 def has_cte_info(self) -> bool:
194 """Check if CTE info is present."""
195 return bool(self.adv_mode & ExtendedHeaderFlags.CTE_INFO)
197 @property
198 def has_advertising_data_info(self) -> bool:
199 """Check if advertising data info is present."""
200 return bool(self.adv_mode & ExtendedHeaderFlags.ADV_DATA_INFO)
202 @property
203 def has_auxiliary_pointer(self) -> bool:
204 """Check if auxiliary pointer is present."""
205 return bool(self.adv_mode & ExtendedHeaderFlags.AUX_PTR)
207 @property
208 def has_sync_info(self) -> bool:
209 """Check if sync info is present."""
210 return bool(self.adv_mode & ExtendedHeaderFlags.SYNC_INFO)
212 @property
213 def has_tx_power(self) -> bool:
214 """Check if TX power is present."""
215 return bool(self.adv_mode & ExtendedHeaderFlags.TX_POWER)
217 @property
218 def has_additional_controller_data(self) -> bool:
219 """Check if additional controller advertising data is present."""
220 return bool(self.adv_mode & ExtendedHeaderFlags.ACAD)
223class BLEAdvertisingPDU(msgspec.Struct, kw_only=True):
224 """BLE Advertising PDU structure."""
226 pdu_type: PDUType
227 tx_add: bool
228 rx_add: bool
229 length: int
230 advertiser_address: bytes = b""
231 target_address: bytes = b""
232 payload: bytes = b""
233 extended_header: BLEExtendedHeader | None = None
235 @property
236 def is_extended_advertising(self) -> bool:
237 """Check if this is an extended advertising PDU."""
238 return self.pdu_type.is_extended_advertising
240 @property
241 def is_legacy_advertising(self) -> bool:
242 """Check if this is a legacy advertising PDU."""
243 return self.pdu_type.is_legacy_advertising
245 @property
246 def pdu_name(self) -> str:
247 """Get human-readable PDU type name."""
248 return self.pdu_type.name
251class CoreAdvertisingData(msgspec.Struct, kw_only=True):
252 """Core advertising data - device identification and services.
254 Attributes:
255 manufacturer_data: Manufacturer-specific data keyed by company ID
256 manufacturer_names: Resolved company names keyed by company ID
257 service_uuids: List of advertised service UUIDs
258 service_data: Service-specific data keyed by service UUID
259 solicited_service_uuids: List of service UUIDs the device is seeking
260 local_name: Device's local name (complete or shortened)
261 uri_data: Parsed URI with scheme info from UriSchemesRegistry
262 """
264 manufacturer_data: dict[int, bytes] = msgspec.field(default_factory=dict)
265 manufacturer_names: dict[int, str] = msgspec.field(default_factory=dict)
266 service_uuids: list[BluetoothUUID] = msgspec.field(default_factory=list)
267 service_data: dict[BluetoothUUID, bytes] = msgspec.field(default_factory=dict)
268 solicited_service_uuids: list[BluetoothUUID] = msgspec.field(default_factory=list)
269 local_name: str = ""
270 uri_data: URIData | None = None
273class DeviceProperties(msgspec.Struct, kw_only=True):
274 """Device capability and appearance properties.
276 Attributes:
277 flags: BLE advertising flags (discoverable mode, capabilities)
278 appearance: Device appearance category and subcategory
279 tx_power: Transmission power level in dBm
280 le_role: LE role (peripheral, central, etc.)
281 le_supported_features: LE supported features bit field
282 class_of_device: Classic Bluetooth Class of Device value
283 class_of_device_info: Parsed Class of Device information
284 """
286 flags: BLEAdvertisingFlags = BLEAdvertisingFlags(0)
287 appearance: AppearanceData | None = None
288 tx_power: int = 0
289 le_role: int | None = None
290 le_supported_features: bytes = b""
291 class_of_device: ClassOfDeviceInfo | None = None
294class DirectedAdvertisingData(msgspec.Struct, kw_only=True):
295 """Directed advertising and timing parameters.
297 These AD types specify target devices and advertising timing.
299 Attributes:
300 public_target_address: List of public target addresses (AD 0x17)
301 random_target_address: List of random target addresses (AD 0x18)
302 le_bluetooth_device_address: LE Bluetooth device address (AD 0x1B)
303 advertising_interval: Advertising interval in 0.625ms units (AD 0x1A)
304 advertising_interval_long: Long advertising interval (AD 0x2F)
305 peripheral_connection_interval_range: Preferred connection interval (AD 0x12)
306 """
308 public_target_address: list[str] = msgspec.field(default_factory=list)
309 random_target_address: list[str] = msgspec.field(default_factory=list)
310 le_bluetooth_device_address: str = ""
311 advertising_interval: int | None = None
312 advertising_interval_long: int | None = None
313 peripheral_connection_interval_range: bytes = b""
316class OOBSecurityData(msgspec.Struct, kw_only=True):
317 """Out-of-Band (OOB) security data advertised for pairing.
319 These AD types provide security material for OOB pairing mechanisms.
321 Attributes:
322 simple_pairing_hash_c: Simple Pairing Hash C-192/C-256 (AD 0x0E, 0x1D)
323 simple_pairing_randomizer_r: Simple Pairing Randomizer R-192/R-256 (AD 0x0F, 0x1E)
324 secure_connections_confirmation: LE SC Confirmation Value (AD 0x22)
325 secure_connections_random: LE SC Random Value (AD 0x23)
326 security_manager_tk_value: Security Manager TK Value (AD 0x10)
327 security_manager_oob_flags: SM Out of Band Flags (AD 0x11)
328 """
330 simple_pairing_hash_c: bytes = b""
331 simple_pairing_randomizer_r: bytes = b""
332 secure_connections_confirmation: bytes = b""
333 secure_connections_random: bytes = b""
334 security_manager_tk_value: bytes = b""
335 security_manager_oob_flags: bytes = b""
338class LocationAndSensingData(msgspec.Struct, kw_only=True):
339 """Location, positioning, and sensing related data.
341 Attributes:
342 indoor_positioning: Indoor positioning data
343 three_d_information: 3D information data
344 transport_discovery_data: Transport Discovery Data
345 channel_map_update_indication: Channel Map Update Indication
346 """
348 indoor_positioning: bytes = b""
349 three_d_information: bytes = b""
350 transport_discovery_data: bytes = b""
351 channel_map_update_indication: bytes = b""
354class MeshAndBroadcastData(msgspec.Struct, kw_only=True):
355 """Bluetooth Mesh and audio broadcast related data.
357 Attributes:
358 mesh_message: Mesh Message
359 mesh_beacon: Mesh Beacon
360 pb_adv: Provisioning Bearer over advertising
361 broadcast_name: Broadcast name
362 broadcast_code: Broadcast Code for encrypted audio
363 biginfo: BIG Info for Broadcast Isochronous Groups
364 periodic_advertising_response_timing: Periodic Advertising Response Timing Info
365 electronic_shelf_label: Electronic Shelf Label data
366 """
368 mesh_message: bytes = b""
369 mesh_beacon: bytes = b""
370 pb_adv: bytes = b""
371 broadcast_name: str = ""
372 broadcast_code: bytes = b""
373 biginfo: bytes = b""
374 periodic_advertising_response_timing: bytes = b""
375 electronic_shelf_label: bytes = b""
378class SecurityData(msgspec.Struct, kw_only=True):
379 """Security and encryption related advertising data.
381 Attributes:
382 encrypted_advertising_data: Encrypted Advertising Data
383 resolvable_set_identifier: Resolvable Set Identifier
384 """
386 encrypted_advertising_data: bytes = b""
387 resolvable_set_identifier: bytes = b""
390class ExtendedAdvertisingData(msgspec.Struct, kw_only=True):
391 """Extended advertising PDU-level metadata (BLE 5.0+).
393 This contains PDU-level information specific to extended advertising,
394 NOT AD types (which go in AdvertisingDataStructures).
396 Attributes:
397 extended_payload: Raw extended advertising payload bytes
398 auxiliary_packets: Chained AUX_ADV_IND packets via AuxPtr
399 periodic_advertising_data: Data from periodic advertising train
400 """
402 extended_payload: bytes = b""
403 auxiliary_packets: list[BLEAdvertisingPDU] = msgspec.field(default_factory=list)
404 periodic_advertising_data: bytes = b""
407class AdvertisingDataStructures(msgspec.Struct, kw_only=True):
408 """Complete parsed advertising data structures organized by category.
410 Contains all AD Types parsed from advertising PDUs (both legacy and extended).
411 These are payload content, not PDU-level metadata.
413 Attributes:
414 core: Device identification and services (manufacturer data, UUIDs, name)
415 properties: Device capabilities (flags, appearance, tx_power, features)
416 directed: Directed advertising parameters (target addresses, intervals)
417 oob_security: Out-of-Band security data for pairing
418 location: Location and sensing data
419 mesh: Mesh network and broadcast audio data
420 security: Encrypted advertising and privacy data
421 """
423 core: CoreAdvertisingData = msgspec.field(default_factory=CoreAdvertisingData)
424 properties: DeviceProperties = msgspec.field(default_factory=DeviceProperties)
425 directed: DirectedAdvertisingData = msgspec.field(default_factory=DirectedAdvertisingData)
426 oob_security: OOBSecurityData = msgspec.field(default_factory=OOBSecurityData)
427 location: LocationAndSensingData = msgspec.field(default_factory=LocationAndSensingData)
428 mesh: MeshAndBroadcastData = msgspec.field(default_factory=MeshAndBroadcastData)
429 security: SecurityData = msgspec.field(default_factory=SecurityData)
432class AdvertisingData(msgspec.Struct, kw_only=True):
433 """Complete BLE advertising data with device information and metadata.
435 Attributes:
436 raw_data: Raw bytes from the advertising packet
437 ad_structures: Parsed AD structures organized by category
438 extended: Extended advertising data (BLE 5.0+)
439 rssi: Received signal strength indicator in dBm
440 """
442 raw_data: bytes
443 ad_structures: AdvertisingDataStructures = msgspec.field(default_factory=AdvertisingDataStructures)
444 extended: ExtendedAdvertisingData = msgspec.field(default_factory=ExtendedAdvertisingData)
445 rssi: int | None = None
447 @property
448 def is_extended_advertising(self) -> bool:
449 """Check if this advertisement uses extended advertising."""
450 return bool(self.extended.extended_payload) or bool(self.extended.auxiliary_packets)
452 @property
453 def total_payload_size(self) -> int:
454 """Get total payload size including extended data."""
455 base_size = len(self.raw_data)
456 if self.extended.extended_payload:
457 base_size += len(self.extended.extended_payload)
458 for aux_packet in self.extended.auxiliary_packets:
459 base_size += len(aux_packet.payload)
460 return base_size
463class AdvertisementData(msgspec.Struct, kw_only=True):
464 """Complete parsed advertisement with PDU structures and interpreted data.
466 This is the unified result from Device.update_advertisement(), containing
467 both low-level AD structures and high-level vendor-specific interpretation.
469 The interpreted_data field is typed as Any to maintain msgspec.Struct compatibility
470 while supporting generic vendor-specific result types at runtime.
472 Attributes:
473 ad_structures: Parsed AD structures (manufacturer_data, service_data, etc.)
474 interpreted_data: Vendor-specific typed result (e.g., sensor readings), or None
475 interpreter_name: Name of the interpreter used (e.g., "BTHome", "Xiaomi"), or None
476 rssi: Received signal strength indicator in dBm
478 Example:
479 # Using connection manager (recommended)
480 ad_data = BleakConnectionManager.convert_advertisement(bleak_advertisement)
481 result = device.update_advertisement(ad_data)
483 # Access low-level AD structures
484 print(result.ad_structures.core.manufacturer_data) # {0x0499: b'...'}
485 print(result.ad_structures.properties.flags)
487 # Access vendor-specific interpreted data
488 if result.interpreted_data:
489 print(f"Interpreter: {result.interpreter_name}")
490 print(f"Temperature: {result.interpreted_data.temperature}")
492 """
494 ad_structures: AdvertisingDataStructures = msgspec.field(default_factory=AdvertisingDataStructures)
495 interpreted_data: Any = None
496 interpreter_name: str | None = None
497 rssi: int | None = None
499 @property
500 def manufacturer_data(self) -> dict[int, bytes]:
501 """Convenience accessor for manufacturer data (company_id → payload)."""
502 return self.ad_structures.core.manufacturer_data
504 @property
505 def service_data(self) -> dict[BluetoothUUID, bytes]:
506 """Convenience accessor for service data (UUID → payload)."""
507 return self.ad_structures.core.service_data
509 @property
510 def local_name(self) -> str:
511 """Convenience accessor for device local name."""
512 return self.ad_structures.core.local_name
514 @property
515 def has_interpretation(self) -> bool:
516 """Check if vendor-specific interpretation was applied."""
517 return self.interpreted_data is not None