Coverage for src / bluetooth_sig / types / advertising / pdu.py: 91%
127 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""BLE Advertising PDU types and header structures.
3Core PDU-level definitions following Bluetooth Core Spec Vol 6, Part B.
4"""
6from __future__ import annotations
8from enum import IntEnum, IntFlag
10import msgspec
12from bluetooth_sig.types.advertising.extended import (
13 AdvertisingDataInfo,
14 AuxiliaryPointer,
15 CTEInfo,
16 SyncInfo,
17)
20class PDUType(IntEnum):
21 """BLE Advertising PDU Types (Core Spec Vol 6, Part B, Section 2.3)."""
23 ADV_IND = 0x00
24 ADV_DIRECT_IND = 0x01
25 ADV_NONCONN_IND = 0x02
26 SCAN_REQ = 0x03
27 SCAN_RSP = 0x04
28 CONNECT_IND = 0x05
29 ADV_SCAN_IND = 0x06
30 ADV_EXT_IND = 0x07
31 ADV_AUX_IND = 0x08
33 @property
34 def is_extended_advertising(self) -> bool:
35 """Check if this is an extended advertising PDU."""
36 return self in (PDUType.ADV_EXT_IND, PDUType.ADV_AUX_IND)
38 @property
39 def is_legacy_advertising(self) -> bool:
40 """Check if this is a legacy advertising PDU."""
41 return self in (
42 PDUType.ADV_IND,
43 PDUType.ADV_DIRECT_IND,
44 PDUType.ADV_NONCONN_IND,
45 PDUType.SCAN_REQ,
46 PDUType.SCAN_RSP,
47 PDUType.CONNECT_IND,
48 PDUType.ADV_SCAN_IND,
49 )
52class PDUHeaderFlags(IntFlag):
53 """BLE PDU header bit masks for parsing operations.
55 These masks are pre-positioned to their correct bit locations,
56 eliminating the need for shifts during extraction.
57 """
59 TYPE_MASK = 0x0F
60 RFU_BIT_4 = 0x10
61 RFU_BIT_5 = 0x20
62 TX_ADD_MASK = 0x40
63 RX_ADD_MASK = 0x80
65 @classmethod
66 def extract_bits(cls, header: int, mask: int) -> int | bool:
67 """Extract bits from header using the specified mask.
69 Returns int for multi-bit masks, bool for single-bit masks.
70 """
71 value = header & mask
72 # If mask has multiple bits set, return the raw value
73 # If mask has only one bit set, return boolean
74 if mask & (mask - 1): # Check if mask has multiple bits set
75 return value
76 return bool(value)
78 @classmethod
79 def extract_pdu_type(cls, header: int) -> PDUType:
80 """Extract PDU type from header byte and return as PDUType enum."""
81 raw_type = int(cls.extract_bits(header, cls.TYPE_MASK))
82 try:
83 return PDUType(raw_type)
84 except ValueError as exc:
85 # For unknown PDU types, we could either raise or return a special value
86 raise ValueError(f"Unknown PDU type: 0x{raw_type:02X}") from exc
88 @classmethod
89 def extract_tx_add(cls, header: int) -> bool:
90 """Extract TX address type from header."""
91 return bool(cls.extract_bits(header, cls.TX_ADD_MASK))
93 @classmethod
94 def extract_rx_add(cls, header: int) -> bool:
95 """Extract RX address type from header."""
96 return bool(cls.extract_bits(header, cls.RX_ADD_MASK))
99class PDULayout:
100 """BLE PDU structure size and offset constants.
102 Defines the sizes and offsets of fields within BLE PDU structures
103 following Bluetooth Core Spec Vol 6, Part B.
104 """
106 # PDU Size constants
107 BLE_ADDR: int = 6
108 AUX_PTR: int = 3
109 ADV_DATA_INFO: int = 2
110 CTE_INFO: int = 1
111 SYNC_INFO: int = 18
112 TX_POWER: int = 1
113 PDU_HEADER: int = 2
114 MIN_EXTENDED_PDU: int = 3
115 EXT_HEADER_LENGTH: int = 1
117 # PDU Offsets
118 EXTENDED_HEADER_START: int = 3
119 ADV_MODE: int = 1
120 ADV_ADDR_OFFSET: int = 2
121 TARGET_ADDR_OFFSET: int = 2
122 CTE_INFO_OFFSET: int = 1
123 ADV_DATA_INFO_OFFSET: int = 2
124 AUX_PTR_OFFSET: int = 3
125 SYNC_INFO_OFFSET: int = 18
126 TX_POWER_OFFSET: int = 1
127 PDU_LENGTH_OFFSET: int = 2
130class ExtendedHeaderFlags(IntEnum):
131 """Extended advertising header field presence flags (BLE 5.0+).
133 Each flag indicates whether a corresponding field is present
134 in the extended advertising header.
135 """
137 ADV_ADDR = 0x01
138 TARGET_ADDR = 0x02
139 CTE_INFO = 0x04
140 ADV_DATA_INFO = 0x08
141 AUX_PTR = 0x10
142 SYNC_INFO = 0x20
143 TX_POWER = 0x40
144 ACAD = 0x80
147class BLEExtendedHeader(msgspec.Struct, kw_only=True):
148 """Extended Advertising Header fields (BLE 5.0+)."""
150 extended_header_length: int = 0
151 adv_mode: int = 0
153 extended_advertiser_address: str = "" # MAC address XX:XX:XX:XX:XX:XX
154 extended_target_address: str = "" # MAC address XX:XX:XX:XX:XX:XX
155 cte_info: CTEInfo | None = None
156 advertising_data_info: AdvertisingDataInfo | None = None
157 auxiliary_pointer: AuxiliaryPointer | None = None
158 sync_info: SyncInfo | None = None
159 tx_power: int | None = None
160 additional_controller_advertising_data: bytes = b""
162 @property
163 def has_extended_advertiser_address(self) -> bool:
164 """Check if extended advertiser address is present."""
165 return bool(self.adv_mode & ExtendedHeaderFlags.ADV_ADDR)
167 @property
168 def has_extended_target_address(self) -> bool:
169 """Check if extended target address is present."""
170 return bool(self.adv_mode & ExtendedHeaderFlags.TARGET_ADDR)
172 @property
173 def has_cte_info(self) -> bool:
174 """Check if CTE info is present."""
175 return bool(self.adv_mode & ExtendedHeaderFlags.CTE_INFO)
177 @property
178 def has_advertising_data_info(self) -> bool:
179 """Check if advertising data info is present."""
180 return bool(self.adv_mode & ExtendedHeaderFlags.ADV_DATA_INFO)
182 @property
183 def has_auxiliary_pointer(self) -> bool:
184 """Check if auxiliary pointer is present."""
185 return bool(self.adv_mode & ExtendedHeaderFlags.AUX_PTR)
187 @property
188 def has_sync_info(self) -> bool:
189 """Check if sync info is present."""
190 return bool(self.adv_mode & ExtendedHeaderFlags.SYNC_INFO)
192 @property
193 def has_tx_power(self) -> bool:
194 """Check if TX power is present."""
195 return bool(self.adv_mode & ExtendedHeaderFlags.TX_POWER)
197 @property
198 def has_additional_controller_data(self) -> bool:
199 """Check if additional controller advertising data is present."""
200 return bool(self.adv_mode & ExtendedHeaderFlags.ACAD)
203class BLEAdvertisingPDU(msgspec.Struct, kw_only=True):
204 """BLE Advertising PDU structure."""
206 pdu_type: PDUType
207 tx_add: bool
208 rx_add: bool
209 length: int
210 advertiser_address: str = "" # MAC address XX:XX:XX:XX:XX:XX
211 target_address: str = "" # MAC address XX:XX:XX:XX:XX:XX
212 payload: bytes = b""
213 extended_header: BLEExtendedHeader | None = None
215 @property
216 def is_extended_advertising(self) -> bool:
217 """Check if this is an extended advertising PDU."""
218 return self.pdu_type.is_extended_advertising
220 @property
221 def is_legacy_advertising(self) -> bool:
222 """Check if this is a legacy advertising PDU."""
223 return self.pdu_type.is_legacy_advertising
225 @property
226 def pdu_name(self) -> str:
227 """Get human-readable PDU type name."""
228 return self.pdu_type.name