Coverage for src/bluetooth_sig/types/advertising.py: 95%
279 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-30 00:10 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-30 00:10 +0000
1"""BLE Advertising data types and parsing utilities."""
3from __future__ import annotations
5from enum import IntEnum, IntFlag
7import msgspec
10class PDUFlags(IntFlag):
11 """BLE PDU parsing bit masks for header operations.
13 These masks are pre-positioned to their correct bit locations,
14 eliminating the need for shifts during extraction.
15 """
17 TYPE_MASK = 0x0F
18 RFU_BIT_4 = 0x10
19 RFU_BIT_5 = 0x20
20 TX_ADD_MASK = 0x40
21 RX_ADD_MASK = 0x80
23 @classmethod
24 def extract_bits(cls, header: int, mask: int) -> int | bool:
25 """Extract bits from header using the specified mask.
27 Returns int for multi-bit masks, bool for single-bit masks.
28 """
29 value = header & mask
30 # If mask has multiple bits set, return the raw value
31 # If mask has only one bit set, return boolean
32 if mask & (mask - 1): # Check if mask has multiple bits set
33 return value
34 return bool(value)
36 @classmethod
37 def extract_pdu_type(cls, header: int) -> PDUType:
38 """Extract PDU type from header byte and return as PDUType enum."""
39 raw_type = int(cls.extract_bits(header, cls.TYPE_MASK))
40 try:
41 return PDUType(raw_type)
42 except ValueError as exc:
43 # For unknown PDU types, we could either raise or return a special value
44 raise ValueError(f"Unknown PDU type: 0x{raw_type:02X}") from exc
46 @classmethod
47 def extract_tx_add(cls, header: int) -> bool:
48 """Extract TX address type from header."""
49 return bool(cls.extract_bits(header, cls.TX_ADD_MASK))
51 @classmethod
52 def extract_rx_add(cls, header: int) -> bool:
53 """Extract RX address type from header."""
54 return bool(cls.extract_bits(header, cls.RX_ADD_MASK))
57class PDUConstants:
58 """BLE PDU parsing constants for sizes and offsets.
60 Following best practices, this uses a class for related
61 constants rather than mixing them with enums/flags.
62 """
64 # PDU Size constants
65 BLE_ADDR: int = 6
66 AUX_PTR: int = 3
67 ADV_DATA_INFO: int = 2
68 CTE_INFO: int = 1
69 SYNC_INFO: int = 18
70 TX_POWER: int = 1
71 PDU_HEADER: int = 2
72 MIN_EXTENDED_PDU: int = 3
73 EXT_HEADER_LENGTH: int = 1
75 # PDU Offsets
76 EXTENDED_HEADER_START: int = 3
77 ADV_MODE: int = 1
78 ADV_ADDR_OFFSET: int = 2
79 TARGET_ADDR_OFFSET: int = 2
80 CTE_INFO_OFFSET: int = 1
81 ADV_DATA_INFO_OFFSET: int = 2
82 AUX_PTR_OFFSET: int = 3
83 SYNC_INFO_OFFSET: int = 18
84 TX_POWER_OFFSET: int = 1
85 PDU_LENGTH_OFFSET: int = 2
88class ExtendedHeaderMode(IntEnum):
89 """Extended Header Mode bit masks (BLE 5.0+)."""
91 ADV_ADDR = 0x01
92 TARGET_ADDR = 0x02
93 CTE_INFO = 0x04
94 ADV_DATA_INFO = 0x08
95 AUX_PTR = 0x10
96 SYNC_INFO = 0x20
97 TX_POWER = 0x40
98 ACAD = 0x80
101class PDUType(IntEnum):
102 """BLE Advertising PDU Types."""
104 ADV_IND = 0x00
105 ADV_DIRECT_IND = 0x01
106 ADV_NONCONN_IND = 0x02
107 SCAN_REQ = 0x03
108 SCAN_RSP = 0x04
109 CONNECT_IND = 0x05
110 ADV_SCAN_IND = 0x06
111 ADV_EXT_IND = 0x07
112 ADV_AUX_IND = 0x08
114 @property
115 def is_extended_advertising(self) -> bool:
116 """Check if this is an extended advertising PDU."""
117 return self in (PDUType.ADV_EXT_IND, PDUType.ADV_AUX_IND)
119 @property
120 def is_legacy_advertising(self) -> bool:
121 """Check if this is a legacy advertising PDU."""
122 return self in (
123 PDUType.ADV_IND,
124 PDUType.ADV_DIRECT_IND,
125 PDUType.ADV_NONCONN_IND,
126 PDUType.SCAN_REQ,
127 PDUType.SCAN_RSP,
128 PDUType.CONNECT_IND,
129 PDUType.ADV_SCAN_IND,
130 )
133class BLEAdvertisementTypes(IntEnum):
134 """BLE Advertisement Data Types (AD Types) as defined in Bluetooth Core Specification."""
136 # Legacy Advertising AD Types
137 FLAGS = 0x01
138 INCOMPLETE_16BIT_SERVICE_UUIDS = 0x02
139 COMPLETE_16BIT_SERVICE_UUIDS = 0x03
140 INCOMPLETE_32BIT_SERVICE_UUIDS = 0x04
141 COMPLETE_32BIT_SERVICE_UUIDS = 0x05
142 INCOMPLETE_128BIT_SERVICE_UUIDS = 0x06
143 COMPLETE_128BIT_SERVICE_UUIDS = 0x07
144 SHORTENED_LOCAL_NAME = 0x08
145 COMPLETE_LOCAL_NAME = 0x09
146 TX_POWER_LEVEL = 0x0A
147 CLASS_OF_DEVICE = 0x0D
148 SIMPLE_PAIRING_HASH_C = 0x0E
149 SIMPLE_PAIRING_RANDOMIZER_R = 0x0F
150 SECURITY_MANAGER_TK_VALUE = 0x10
151 SECURITY_MANAGER_OUT_OF_BAND_FLAGS = 0x11
152 SLAVE_CONNECTION_INTERVAL_RANGE = 0x12
153 SOLICITED_SERVICE_UUIDS_16BIT = 0x14
154 SOLICITED_SERVICE_UUIDS_128BIT = 0x15
155 SERVICE_DATA_16BIT = 0x16
156 PUBLIC_TARGET_ADDRESS = 0x17
157 RANDOM_TARGET_ADDRESS = 0x18
158 APPEARANCE = 0x19
159 ADVERTISING_INTERVAL = 0x1A
160 LE_BLUETOOTH_DEVICE_ADDRESS = 0x1B
161 LE_ROLE = 0x1C
162 SIMPLE_PAIRING_HASH_C256 = 0x1D
163 SIMPLE_PAIRING_RANDOMIZER_R256 = 0x1E
164 SERVICE_DATA_32BIT = 0x20
165 SERVICE_DATA_128BIT = 0x21
166 SECURE_CONNECTIONS_CONFIRMATION_VALUE = 0x22
167 SECURE_CONNECTIONS_RANDOM_VALUE = 0x23
168 URI = 0x24
169 INDOOR_POSITIONING = 0x25
170 TRANSPORT_DISCOVERY_DATA = 0x26
171 LE_SUPPORTED_FEATURES = 0x27
172 CHANNEL_MAP_UPDATE_INDICATION = 0x28
173 PB_ADV = 0x29
174 MESH_MESSAGE = 0x2A
175 MESH_BEACON = 0x2B
176 BIGINFO = 0x2C
177 BROADCAST_CODE = 0x2D
178 RESOLVABLE_SET_IDENTIFIER = 0x2E
179 ADVERTISING_INTERVAL_LONG = 0x2F
180 BROADCAST_NAME = 0x30
181 ENCRYPTED_ADVERTISING_DATA = 0x31
182 PERIODIC_ADVERTISING_RESPONSE_TIMING_INFORMATION = 0x32
183 ELECTRONIC_SHELF_LABEL = 0x34
184 THREE_D_INFORMATION_DATA = 0x3D
185 MANUFACTURER_SPECIFIC_DATA = 0xFF
188class BLEAdvertisingFlags(IntFlag):
189 """BLE Advertising Flags as defined in Bluetooth Core Specification Supplement.
191 These flags indicate the discoverable mode and capabilities of the advertising device.
192 """
194 LE_LIMITED_DISCOVERABLE_MODE = 0x01
195 LE_GENERAL_DISCOVERABLE_MODE = 0x02
196 BR_EDR_NOT_SUPPORTED = 0x04
197 SIMULTANEOUS_LE_BR_EDR_CONTROLLER = 0x08
198 SIMULTANEOUS_LE_BR_EDR_HOST = 0x10
199 RESERVED_BIT_5 = 0x20
200 RESERVED_BIT_6 = 0x40
201 RESERVED_BIT_7 = 0x80
204class BLEExtendedHeader(msgspec.Struct, kw_only=True):
205 """Extended Advertising Header fields (BLE 5.0+)."""
207 extended_header_length: int = 0
208 adv_mode: int = 0
210 extended_advertiser_address: bytes = b""
211 extended_target_address: bytes = b""
212 cte_info: bytes = b""
213 advertising_data_info: bytes = b""
214 auxiliary_pointer: bytes = b""
215 sync_info: bytes = b""
216 tx_power: int | None = None
217 additional_controller_advertising_data: bytes = b""
219 @property
220 def has_extended_advertiser_address(self) -> bool:
221 """Check if extended advertiser address is present."""
222 return bool(self.adv_mode & ExtendedHeaderMode.ADV_ADDR)
224 @property
225 def has_extended_target_address(self) -> bool:
226 """Check if extended target address is present."""
227 return bool(self.adv_mode & ExtendedHeaderMode.TARGET_ADDR)
229 @property
230 def has_cte_info(self) -> bool:
231 """Check if CTE info is present."""
232 return bool(self.adv_mode & ExtendedHeaderMode.CTE_INFO)
234 @property
235 def has_advertising_data_info(self) -> bool:
236 """Check if advertising data info is present."""
237 return bool(self.adv_mode & ExtendedHeaderMode.ADV_DATA_INFO)
239 @property
240 def has_auxiliary_pointer(self) -> bool:
241 """Check if auxiliary pointer is present."""
242 return bool(self.adv_mode & ExtendedHeaderMode.AUX_PTR)
244 @property
245 def has_sync_info(self) -> bool:
246 """Check if sync info is present."""
247 return bool(self.adv_mode & ExtendedHeaderMode.SYNC_INFO)
249 @property
250 def has_tx_power(self) -> bool:
251 """Check if TX power is present."""
252 return bool(self.adv_mode & ExtendedHeaderMode.TX_POWER)
254 @property
255 def has_additional_controller_data(self) -> bool:
256 """Check if additional controller advertising data is present."""
257 return bool(self.adv_mode & ExtendedHeaderMode.ACAD)
260class BLEAdvertisingPDU(msgspec.Struct, kw_only=True):
261 """BLE Advertising PDU structure."""
263 pdu_type: PDUType
264 tx_add: bool
265 rx_add: bool
266 length: int
267 advertiser_address: bytes = b""
268 target_address: bytes = b""
269 payload: bytes = b""
270 extended_header: BLEExtendedHeader | None = None
272 @property
273 def is_extended_advertising(self) -> bool:
274 """Check if this is an extended advertising PDU."""
275 return self.pdu_type.is_extended_advertising
277 @property
278 def is_legacy_advertising(self) -> bool:
279 """Check if this is a legacy advertising PDU."""
280 return self.pdu_type.is_legacy_advertising
282 @property
283 def pdu_name(self) -> str:
284 """Get human-readable PDU type name."""
285 return self.pdu_type.name
288class ParsedADStructures(msgspec.Struct, kw_only=True):
289 """Parsed Advertising Data structures from advertisement payload."""
291 manufacturer_data: dict[int, bytes] = msgspec.field(default_factory=dict)
292 service_uuids: list[str] = msgspec.field(default_factory=list)
293 local_name: str = ""
294 tx_power: int = 0
295 flags: BLEAdvertisingFlags = BLEAdvertisingFlags(0)
296 appearance: int | None = None
297 service_data: dict[str, bytes] = msgspec.field(default_factory=dict)
298 solicited_service_uuids: list[str] = msgspec.field(default_factory=list)
299 uri: str = ""
300 indoor_positioning: bytes = b""
301 transport_discovery_data: bytes = b""
302 le_supported_features: bytes = b""
303 encrypted_advertising_data: bytes = b""
304 periodic_advertising_response_timing: bytes = b""
305 electronic_shelf_label: bytes = b""
306 three_d_information: bytes = b""
307 broadcast_name: str = ""
308 broadcast_code: bytes = b""
309 biginfo: bytes = b""
310 mesh_message: bytes = b""
311 mesh_beacon: bytes = b""
312 public_target_address: list[str] = msgspec.field(default_factory=list)
313 random_target_address: list[str] = msgspec.field(default_factory=list)
314 advertising_interval: int | None = None
315 advertising_interval_long: int | None = None
316 le_bluetooth_device_address: str = ""
317 le_role: int | None = None
318 class_of_device: int | None = None
319 simple_pairing_hash_c: bytes = b""
320 simple_pairing_randomizer_r: bytes = b""
321 security_manager_tk_value: bytes = b""
322 security_manager_out_of_band_flags: bytes = b""
323 slave_connection_interval_range: bytes = b""
324 secure_connections_confirmation: bytes = b""
325 secure_connections_random: bytes = b""
326 channel_map_update_indication: bytes = b""
327 pb_adv: bytes = b""
328 resolvable_set_identifier: bytes = b""
331class DeviceAdvertiserData(msgspec.Struct, kw_only=True):
332 """Parsed advertiser data from device discovery."""
334 raw_data: bytes
335 local_name: str = ""
336 manufacturer_data: dict[int, bytes] = msgspec.field(default_factory=dict)
337 service_uuids: list[str] = msgspec.field(default_factory=list)
338 tx_power: int | None = None
339 rssi: int | None = None
340 flags: BLEAdvertisingFlags | None = None
342 # Additional parsed fields
343 appearance: int | None = None
344 service_data: dict[str, bytes] = msgspec.field(default_factory=dict)
345 solicited_service_uuids: list[str] = msgspec.field(default_factory=list)
346 uri: str = ""
347 indoor_positioning: bytes = b""
348 transport_discovery_data: bytes = b""
349 le_supported_features: bytes = b""
350 encrypted_advertising_data: bytes = b""
351 periodic_advertising_response_timing: bytes = b""
352 electronic_shelf_label: bytes = b""
353 three_d_information: bytes = b""
354 broadcast_name: str = ""
355 biginfo: bytes = b""
356 mesh_message: bytes = b""
357 mesh_beacon: bytes = b""
358 public_target_address: list[str] = msgspec.field(default_factory=list)
359 random_target_address: list[str] = msgspec.field(default_factory=list)
360 advertising_interval: int | None = None
361 advertising_interval_long: int | None = None
362 le_bluetooth_device_address: str = ""
363 le_role: int | None = None
364 class_of_device: int | None = None
365 simple_pairing_hash_c: bytes = b""
366 simple_pairing_randomizer_r: bytes = b""
367 security_manager_tk_value: bytes = b""
368 security_manager_out_of_band_flags: bytes = b""
369 slave_connection_interval_range: bytes = b""
370 secure_connections_confirmation: bytes = b""
371 secure_connections_random: bytes = b""
372 channel_map_update_indication: bytes = b""
373 pb_adv: bytes = b""
374 resolvable_set_identifier: bytes = b""
376 extended_payload: bytes = b""
377 auxiliary_packets: list[BLEAdvertisingPDU] = msgspec.field(default_factory=list)
378 periodic_advertising_data: bytes = b""
379 broadcast_code: bytes = b""
381 @property
382 def is_extended_advertising(self) -> bool:
383 """Check if this advertisement uses extended advertising."""
384 return bool(self.extended_payload) or bool(self.auxiliary_packets)
386 @property
387 def total_payload_size(self) -> int:
388 """Get total payload size including extended data."""
389 base_size = len(self.raw_data)
390 if self.extended_payload:
391 base_size += len(self.extended_payload)
392 for aux_packet in self.auxiliary_packets:
393 base_size += len(aux_packet.payload)
394 return base_size