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

1"""BLE Advertising PDU types and header structures. 

2 

3Core PDU-level definitions following Bluetooth Core Spec Vol 6, Part B. 

4""" 

5 

6from __future__ import annotations 

7 

8from enum import IntEnum, IntFlag 

9 

10import msgspec 

11 

12from bluetooth_sig.types.advertising.extended import ( 

13 AdvertisingDataInfo, 

14 AuxiliaryPointer, 

15 CTEInfo, 

16 SyncInfo, 

17) 

18 

19 

20class PDUType(IntEnum): 

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

22 

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 

32 

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) 

37 

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 ) 

50 

51 

52class PDUHeaderFlags(IntFlag): 

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

54 

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

56 eliminating the need for shifts during extraction. 

57 """ 

58 

59 TYPE_MASK = 0x0F 

60 RFU_BIT_4 = 0x10 

61 RFU_BIT_5 = 0x20 

62 TX_ADD_MASK = 0x40 

63 RX_ADD_MASK = 0x80 

64 

65 @classmethod 

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

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

68 

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) 

77 

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 

87 

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

92 

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

97 

98 

99class PDULayout: 

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

101 

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

103 following Bluetooth Core Spec Vol 6, Part B. 

104 """ 

105 

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 

116 

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 

128 

129 

130class ExtendedHeaderFlags(IntEnum): 

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

132 

133 Each flag indicates whether a corresponding field is present 

134 in the extended advertising header. 

135 """ 

136 

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 

145 

146 

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

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

149 

150 extended_header_length: int = 0 

151 adv_mode: int = 0 

152 

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

161 

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) 

166 

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) 

171 

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) 

176 

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) 

181 

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) 

186 

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) 

191 

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) 

196 

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) 

201 

202 

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

204 """BLE Advertising PDU structure.""" 

205 

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 

214 

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 

219 

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 

224 

225 @property 

226 def pdu_name(self) -> str: 

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

228 return self.pdu_type.name