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

1"""BLE Advertising data types and parsing utilities. 

2 

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""" 

10 

11from __future__ import annotations 

12 

13from enum import IntEnum, IntFlag 

14from typing import Any 

15 

16import msgspec 

17 

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 

22 

23 

24class PDUType(IntEnum): 

25 """BLE Advertising PDU Types (Core Spec Vol 6, Part B, Section 2.3).""" 

26 

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 

36 

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) 

41 

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 ) 

54 

55 

56class PDUHeaderFlags(IntFlag): 

57 """BLE PDU header bit masks for parsing operations. 

58 

59 These masks are pre-positioned to their correct bit locations, 

60 eliminating the need for shifts during extraction. 

61 """ 

62 

63 TYPE_MASK = 0x0F 

64 RFU_BIT_4 = 0x10 

65 RFU_BIT_5 = 0x20 

66 TX_ADD_MASK = 0x40 

67 RX_ADD_MASK = 0x80 

68 

69 @classmethod 

70 def extract_bits(cls, header: int, mask: int) -> int | bool: 

71 """Extract bits from header using the specified mask. 

72 

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) 

81 

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 

91 

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)) 

96 

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)) 

101 

102 

103class PDULayout: 

104 """BLE PDU structure size and offset constants. 

105 

106 Defines the sizes and offsets of fields within BLE PDU structures 

107 following Bluetooth Core Spec Vol 6, Part B. 

108 """ 

109 

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 

120 

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 

132 

133 

134class ExtendedHeaderFlags(IntEnum): 

135 """Extended advertising header field presence flags (BLE 5.0+). 

136 

137 Each flag indicates whether a corresponding field is present 

138 in the extended advertising header. 

139 """ 

140 

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 

149 

150 

151class BLEAdvertisingFlags(IntFlag): 

152 """BLE Advertising Flags (Core Spec Supplement, Part A, Section 1.3). 

153 

154 These flags indicate the discoverable mode and capabilities of the advertising device. 

155 """ 

156 

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 

165 

166 

167class BLEExtendedHeader(msgspec.Struct, kw_only=True): 

168 """Extended Advertising Header fields (BLE 5.0+).""" 

169 

170 extended_header_length: int = 0 

171 adv_mode: int = 0 

172 

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"" 

181 

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) 

186 

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) 

191 

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) 

196 

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) 

201 

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) 

206 

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) 

211 

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) 

216 

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) 

221 

222 

223class BLEAdvertisingPDU(msgspec.Struct, kw_only=True): 

224 """BLE Advertising PDU structure.""" 

225 

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 

234 

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 

239 

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 

244 

245 @property 

246 def pdu_name(self) -> str: 

247 """Get human-readable PDU type name.""" 

248 return self.pdu_type.name 

249 

250 

251class CoreAdvertisingData(msgspec.Struct, kw_only=True): 

252 """Core advertising data - device identification and services. 

253 

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 """ 

263 

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 

271 

272 

273class DeviceProperties(msgspec.Struct, kw_only=True): 

274 """Device capability and appearance properties. 

275 

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 """ 

285 

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 

292 

293 

294class DirectedAdvertisingData(msgspec.Struct, kw_only=True): 

295 """Directed advertising and timing parameters. 

296 

297 These AD types specify target devices and advertising timing. 

298 

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 """ 

307 

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"" 

314 

315 

316class OOBSecurityData(msgspec.Struct, kw_only=True): 

317 """Out-of-Band (OOB) security data advertised for pairing. 

318 

319 These AD types provide security material for OOB pairing mechanisms. 

320 

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 """ 

329 

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"" 

336 

337 

338class LocationAndSensingData(msgspec.Struct, kw_only=True): 

339 """Location, positioning, and sensing related data. 

340 

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 """ 

347 

348 indoor_positioning: bytes = b"" 

349 three_d_information: bytes = b"" 

350 transport_discovery_data: bytes = b"" 

351 channel_map_update_indication: bytes = b"" 

352 

353 

354class MeshAndBroadcastData(msgspec.Struct, kw_only=True): 

355 """Bluetooth Mesh and audio broadcast related data. 

356 

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 """ 

367 

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"" 

376 

377 

378class SecurityData(msgspec.Struct, kw_only=True): 

379 """Security and encryption related advertising data. 

380 

381 Attributes: 

382 encrypted_advertising_data: Encrypted Advertising Data 

383 resolvable_set_identifier: Resolvable Set Identifier 

384 """ 

385 

386 encrypted_advertising_data: bytes = b"" 

387 resolvable_set_identifier: bytes = b"" 

388 

389 

390class ExtendedAdvertisingData(msgspec.Struct, kw_only=True): 

391 """Extended advertising PDU-level metadata (BLE 5.0+). 

392 

393 This contains PDU-level information specific to extended advertising, 

394 NOT AD types (which go in AdvertisingDataStructures). 

395 

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 """ 

401 

402 extended_payload: bytes = b"" 

403 auxiliary_packets: list[BLEAdvertisingPDU] = msgspec.field(default_factory=list) 

404 periodic_advertising_data: bytes = b"" 

405 

406 

407class AdvertisingDataStructures(msgspec.Struct, kw_only=True): 

408 """Complete parsed advertising data structures organized by category. 

409 

410 Contains all AD Types parsed from advertising PDUs (both legacy and extended). 

411 These are payload content, not PDU-level metadata. 

412 

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 """ 

422 

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) 

430 

431 

432class AdvertisingData(msgspec.Struct, kw_only=True): 

433 """Complete BLE advertising data with device information and metadata. 

434 

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 """ 

441 

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 

446 

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) 

451 

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 

461 

462 

463class AdvertisementData(msgspec.Struct, kw_only=True): 

464 """Complete parsed advertisement with PDU structures and interpreted data. 

465 

466 This is the unified result from Device.update_advertisement(), containing 

467 both low-level AD structures and high-level vendor-specific interpretation. 

468 

469 The interpreted_data field is typed as Any to maintain msgspec.Struct compatibility 

470 while supporting generic vendor-specific result types at runtime. 

471 

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 

477 

478 Example: 

479 # Using connection manager (recommended) 

480 ad_data = BleakConnectionManager.convert_advertisement(bleak_advertisement) 

481 result = device.update_advertisement(ad_data) 

482 

483 # Access low-level AD structures 

484 print(result.ad_structures.core.manufacturer_data) # {0x0499: b'...'} 

485 print(result.ad_structures.properties.flags) 

486 

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}") 

491 

492 """ 

493 

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 

498 

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 

503 

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 

508 

509 @property 

510 def local_name(self) -> str: 

511 """Convenience accessor for device local name.""" 

512 return self.ad_structures.core.local_name 

513 

514 @property 

515 def has_interpretation(self) -> bool: 

516 """Check if vendor-specific interpretation was applied.""" 

517 return self.interpreted_data is not None