Coverage for src / bluetooth_sig / gatt / characteristics / utils / ieee11073_parser.py: 95%

169 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-18 11:17 +0000

1"""IEEE 11073 medical device format support utilities.""" 

2 

3from __future__ import annotations 

4 

5import math 

6import struct 

7from datetime import MAXYEAR, datetime 

8 

9from ...exceptions import InsufficientDataError, ValueRangeError 

10from .bit_field_utils import BitFieldUtils 

11 

12 

13class IEEE11073Parser: 

14 """Utility class for IEEE-11073 medical device format support.""" 

15 

16 # IEEE-11073 SFLOAT (16-bit) constants 

17 SFLOAT_MANTISSA_MASK = 0x0FFF 

18 SFLOAT_MANTISSA_SIGN_BIT = 0x0800 

19 SFLOAT_MANTISSA_CONVERSION = 0x1000 

20 SFLOAT_EXPONENT_MASK = 0x0F 

21 SFLOAT_EXPONENT_SIGN_BIT = 0x08 

22 SFLOAT_EXPONENT_CONVERSION = 0x10 

23 SFLOAT_MANTISSA_MAX = 2048 

24 SFLOAT_EXPONENT_MIN = -8 

25 SFLOAT_EXPONENT_MAX = 7 

26 SFLOAT_EXPONENT_BIAS = 8 

27 # Bit field positions and widths (no magic numbers) 

28 SFLOAT_MANTISSA_START_BIT = 0 

29 SFLOAT_MANTISSA_BIT_WIDTH = 12 

30 SFLOAT_EXPONENT_START_BIT = 12 

31 SFLOAT_EXPONENT_BIT_WIDTH = 4 

32 

33 # IEEE-11073 SFLOAT (16-bit) special values 

34 SFLOAT_NAN = 0x07FF 

35 SFLOAT_NRES = 0x0800 # NRes (Not a valid result) - treated as NaN 

36 SFLOAT_POSITIVE_INFINITY = 0x07FE 

37 SFLOAT_NEGATIVE_INFINITY = 0x0802 

38 

39 # IEEE-11073 FLOAT32 (32-bit) special values 

40 # Per IEEE 11073-20601 and Bluetooth GSS, exponent=0 with reserved mantissas: 

41 # Mantissa: 0x7FFFFE (+Inf), 0x7FFFFF (NaN), 0x800000 (NRes), 0x800001 (RFU), 0x800002 (-Inf) 

42 FLOAT32_NAN = 0x007FFFFF # Exponent 0, mantissa 0x7FFFFF 

43 FLOAT32_POSITIVE_INFINITY = 0x007FFFFE # Exponent 0, mantissa 0x7FFFFE 

44 FLOAT32_NEGATIVE_INFINITY = 0x00800002 # Exponent 0, mantissa 0x800002 

45 FLOAT32_NRES = 0x00800000 # Exponent 0, mantissa 0x800000 

46 FLOAT32_RFU = 0x00800001 # Exponent 0, mantissa 0x800001 (Reserved) 

47 FLOAT32_MANTISSA_MASK = 0x00FFFFFF 

48 FLOAT32_MANTISSA_SIGN_BIT = 0x00800000 

49 FLOAT32_MANTISSA_CONVERSION = 0x01000000 

50 FLOAT32_EXPONENT_MASK = 0xFF 

51 FLOAT32_EXPONENT_SIGN_BIT = 0x80 

52 FLOAT32_EXPONENT_CONVERSION = 0x100 

53 FLOAT32_MANTISSA_MAX = 8388608 # 2^23 

54 FLOAT32_EXPONENT_MIN = -128 

55 FLOAT32_EXPONENT_MAX = 127 

56 FLOAT32_EXPONENT_BIAS = 128 

57 # Bit field positions and widths (no magic numbers) 

58 FLOAT32_MANTISSA_START_BIT = 0 

59 FLOAT32_MANTISSA_BIT_WIDTH = 24 

60 FLOAT32_EXPONENT_START_BIT = 24 

61 FLOAT32_EXPONENT_BIT_WIDTH = 8 

62 

63 # IEEE-11073 timestamp validation constants 

64 IEEE11073_MIN_YEAR = 1582 # Gregorian calendar adoption year (per Bluetooth SIG spec) 

65 MONTH_MIN = 1 

66 MONTH_MAX = 12 

67 DAY_MIN = 1 

68 DAY_MAX = 31 # Simplified validation - actual days depend on month/year 

69 HOUR_MIN = 0 

70 HOUR_MAX = 23 # 24-hour format 

71 MINUTE_MIN = 0 

72 MINUTE_MAX = 59 

73 SECOND_MIN = 0 

74 SECOND_MAX = 59 

75 

76 # Common constants 

77 TIMESTAMP_LENGTH = 7 

78 

79 @staticmethod 

80 def parse_sfloat(data: bytes | bytearray, offset: int = 0) -> float: 

81 """Parse IEEE 11073 16-bit SFLOAT. 

82 

83 Args: 

84 data: Raw bytes/bytearray 

85 offset: Offset in the data 

86 

87 """ 

88 if len(data) < offset + 2: 

89 raise InsufficientDataError("IEEE 11073 SFLOAT", data[offset:], 2) 

90 raw_value = int.from_bytes(data[offset : offset + 2], byteorder="little") 

91 

92 # Handle special values 

93 if raw_value == IEEE11073Parser.SFLOAT_NAN: 

94 return float("nan") # NaN 

95 if raw_value == IEEE11073Parser.SFLOAT_NRES: 

96 return float("nan") # NRes (Not a valid result) 

97 if raw_value == IEEE11073Parser.SFLOAT_POSITIVE_INFINITY: 

98 return float("inf") # +INFINITY 

99 if raw_value == IEEE11073Parser.SFLOAT_NEGATIVE_INFINITY: 

100 return float("-inf") # -INFINITY 

101 

102 # Extract mantissa and exponent 

103 mantissa = BitFieldUtils.extract_bits(raw_value, IEEE11073Parser.SFLOAT_MANTISSA_MASK) 

104 if mantissa >= IEEE11073Parser.SFLOAT_MANTISSA_SIGN_BIT: # Negative mantissa 

105 mantissa = mantissa - IEEE11073Parser.SFLOAT_MANTISSA_CONVERSION 

106 

107 exponent = BitFieldUtils.extract_bit_field_from_mask( 

108 raw_value, 

109 IEEE11073Parser.SFLOAT_EXPONENT_MASK, 

110 IEEE11073Parser.SFLOAT_EXPONENT_START_BIT, 

111 ) 

112 exponent = exponent - IEEE11073Parser.SFLOAT_EXPONENT_BIAS 

113 

114 return float(mantissa * (10.0**exponent)) 

115 

116 @staticmethod 

117 def parse_float32(data: bytes | bytearray, offset: int = 0) -> float: 

118 """Parse IEEE 11073 32-bit FLOAT.""" 

119 if len(data) < offset + 4: 

120 raise InsufficientDataError("IEEE 11073 FLOAT32", data[offset:], 4) 

121 

122 raw_value = int.from_bytes(data[offset : offset + 4], byteorder="little") 

123 

124 # Handle special values (similar to SFLOAT but 32-bit) 

125 if raw_value == IEEE11073Parser.FLOAT32_NAN: 

126 return float("nan") 

127 if raw_value == IEEE11073Parser.FLOAT32_POSITIVE_INFINITY: 

128 return float("inf") 

129 if raw_value == IEEE11073Parser.FLOAT32_NEGATIVE_INFINITY: 

130 return float("-inf") 

131 if raw_value == IEEE11073Parser.FLOAT32_NRES: 

132 return float("nan") # NRes (Not a valid result) 

133 

134 # Extract mantissa (24-bit) and exponent (8-bit) 

135 mantissa = BitFieldUtils.extract_bits(raw_value, IEEE11073Parser.FLOAT32_MANTISSA_MASK) 

136 if mantissa >= IEEE11073Parser.FLOAT32_MANTISSA_SIGN_BIT: # Negative mantissa 

137 mantissa = mantissa - IEEE11073Parser.FLOAT32_MANTISSA_CONVERSION 

138 

139 exponent = BitFieldUtils.extract_bit_field_from_mask( 

140 raw_value, 

141 IEEE11073Parser.FLOAT32_EXPONENT_MASK, 

142 IEEE11073Parser.FLOAT32_EXPONENT_START_BIT, 

143 ) 

144 exponent = exponent - IEEE11073Parser.FLOAT32_EXPONENT_BIAS 

145 

146 return float(mantissa * (10**exponent)) 

147 

148 @staticmethod 

149 def encode_sfloat(value: float) -> bytearray: 

150 """Encode float to IEEE 11073 16-bit SFLOAT.""" 

151 if math.isnan(value): 

152 return bytearray(IEEE11073Parser.SFLOAT_NAN.to_bytes(2, byteorder="little")) 

153 if math.isinf(value): 

154 if value > 0: 

155 return bytearray(IEEE11073Parser.SFLOAT_POSITIVE_INFINITY.to_bytes(2, byteorder="little")) 

156 return bytearray(IEEE11073Parser.SFLOAT_NEGATIVE_INFINITY.to_bytes(2, byteorder="little")) 

157 

158 # Find best exponent and mantissa representation 

159 exponent = 0 

160 mantissa = value 

161 

162 while abs(mantissa) >= IEEE11073Parser.SFLOAT_MANTISSA_MAX and exponent < IEEE11073Parser.SFLOAT_EXPONENT_MAX: 

163 mantissa /= 10 

164 exponent += 1 

165 

166 while abs(mantissa) < 1 and mantissa != 0 and exponent > IEEE11073Parser.SFLOAT_EXPONENT_MIN: 

167 mantissa *= 10 

168 exponent -= 1 

169 

170 mantissa_int = round(mantissa) 

171 

172 # Pack into 16-bit value 

173 exponent += IEEE11073Parser.SFLOAT_EXPONENT_BIAS # Add bias for storage 

174 if mantissa_int < 0: 

175 mantissa_int = mantissa_int + IEEE11073Parser.SFLOAT_MANTISSA_CONVERSION 

176 

177 raw_value = BitFieldUtils.merge_bit_fields( 

178 ( 

179 mantissa_int, 

180 IEEE11073Parser.SFLOAT_MANTISSA_START_BIT, 

181 IEEE11073Parser.SFLOAT_MANTISSA_BIT_WIDTH, 

182 ), 

183 ( 

184 exponent, 

185 IEEE11073Parser.SFLOAT_EXPONENT_START_BIT, 

186 IEEE11073Parser.SFLOAT_EXPONENT_BIT_WIDTH, 

187 ), 

188 ) 

189 return bytearray(raw_value.to_bytes(2, byteorder="little")) 

190 

191 @staticmethod 

192 def encode_float32(value: float) -> bytearray: 

193 """Encode float to IEEE 11073 32-bit FLOAT.""" 

194 if math.isnan(value): 

195 return bytearray(IEEE11073Parser.FLOAT32_NAN.to_bytes(4, byteorder="little")) 

196 if math.isinf(value): 

197 if value > 0: 

198 return bytearray(IEEE11073Parser.FLOAT32_POSITIVE_INFINITY.to_bytes(4, byteorder="little")) 

199 return bytearray(IEEE11073Parser.FLOAT32_NEGATIVE_INFINITY.to_bytes(4, byteorder="little")) 

200 

201 if value == 0.0: 

202 return bytearray([0x00, 0x00, 0x00, 0x00]) 

203 

204 # Find the best representation by trying different exponents 

205 # IEEE-11073 32-bit FLOAT: 24-bit signed mantissa + 8-bit signed exponent 

206 best_mantissa = 0 

207 best_exponent = 0 

208 best_error = float("inf") 

209 

210 # Try exponents from min to max (reasonable range for medical values) 

211 for exp in range( 

212 IEEE11073Parser.FLOAT32_EXPONENT_MIN, 

213 IEEE11073Parser.FLOAT32_EXPONENT_MAX + 1, 

214 ): 

215 # Calculate what mantissa would be with this exponent 

216 potential_mantissa = value * (10 ** (-exp)) 

217 

218 # Check if mantissa fits in 24-bit signed range 

219 if abs(potential_mantissa) < IEEE11073Parser.FLOAT32_MANTISSA_MAX: 

220 # Round to integer 

221 mantissa_int = round(potential_mantissa) 

222 

223 # Calculate the actual value this would represent 

224 actual_value = mantissa_int * (10**exp) 

225 error = abs(value - actual_value) 

226 

227 # Use this if it's better (less error) or if it's exact 

228 if error < best_error: 

229 best_error = error 

230 best_mantissa = mantissa_int 

231 best_exponent = exp 

232 

233 # If we found an exact representation, use it 

234 if error == 0.0: 

235 break 

236 

237 # Validate mantissa fits in 24-bit signed range 

238 if abs(best_mantissa) >= IEEE11073Parser.FLOAT32_MANTISSA_MAX: 

239 raise ValueRangeError( 

240 "IEEE-11073 FLOAT32 mantissa", 

241 best_mantissa, 

242 -(IEEE11073Parser.FLOAT32_MANTISSA_MAX - 1), 

243 IEEE11073Parser.FLOAT32_MANTISSA_MAX - 1, 

244 ) 

245 

246 # Pack into 32-bit value: mantissa (24-bit signed) + exponent (8-bit signed) 

247 # Convert signed values to unsigned for bit packing 

248 mantissa_unsigned = ( 

249 best_mantissa if best_mantissa >= 0 else best_mantissa + IEEE11073Parser.FLOAT32_MANTISSA_CONVERSION 

250 ) 

251 exponent_unsigned = best_exponent + IEEE11073Parser.FLOAT32_EXPONENT_BIAS # Add bias for storage 

252 

253 raw_value = BitFieldUtils.merge_bit_fields( 

254 ( 

255 mantissa_unsigned, 

256 IEEE11073Parser.FLOAT32_MANTISSA_START_BIT, 

257 IEEE11073Parser.FLOAT32_MANTISSA_BIT_WIDTH, 

258 ), 

259 ( 

260 exponent_unsigned, 

261 IEEE11073Parser.FLOAT32_EXPONENT_START_BIT, 

262 IEEE11073Parser.FLOAT32_EXPONENT_BIT_WIDTH, 

263 ), 

264 ) 

265 return bytearray(raw_value.to_bytes(4, byteorder="little")) 

266 

267 @staticmethod 

268 def parse_timestamp(data: bytearray, offset: int) -> datetime: 

269 """Parse IEEE-11073 timestamp format (7 bytes).""" 

270 if len(data) < offset + IEEE11073Parser.TIMESTAMP_LENGTH: 

271 raise InsufficientDataError("IEEE 11073 timestamp", data[offset:], IEEE11073Parser.TIMESTAMP_LENGTH) 

272 

273 timestamp_data = data[offset : offset + IEEE11073Parser.TIMESTAMP_LENGTH] 

274 year, month, day, hours, minutes, seconds = struct.unpack("<HBBBBB", timestamp_data) 

275 return datetime(year, month, day, hours, minutes, seconds) 

276 

277 @staticmethod 

278 def encode_timestamp(timestamp: datetime) -> bytearray: 

279 """Encode timestamp to IEEE-11073 7-byte format.""" 

280 # Validate ranges per IEEE-11073 specification 

281 if not IEEE11073Parser.IEEE11073_MIN_YEAR <= timestamp.year <= MAXYEAR: 

282 raise ValueRangeError("year", timestamp.year, IEEE11073Parser.IEEE11073_MIN_YEAR, MAXYEAR) 

283 if not IEEE11073Parser.MONTH_MIN <= timestamp.month <= IEEE11073Parser.MONTH_MAX: 

284 raise ValueRangeError( 

285 "month", 

286 timestamp.month, 

287 IEEE11073Parser.MONTH_MIN, 

288 IEEE11073Parser.MONTH_MAX, 

289 ) 

290 if not IEEE11073Parser.DAY_MIN <= timestamp.day <= IEEE11073Parser.DAY_MAX: 

291 raise ValueRangeError("day", timestamp.day, IEEE11073Parser.DAY_MIN, IEEE11073Parser.DAY_MAX) 

292 if not IEEE11073Parser.HOUR_MIN <= timestamp.hour <= IEEE11073Parser.HOUR_MAX: 

293 raise ValueRangeError( 

294 "hour", 

295 timestamp.hour, 

296 IEEE11073Parser.HOUR_MIN, 

297 IEEE11073Parser.HOUR_MAX, 

298 ) 

299 if not IEEE11073Parser.MINUTE_MIN <= timestamp.minute <= IEEE11073Parser.MINUTE_MAX: 

300 raise ValueRangeError( 

301 "minute", 

302 timestamp.minute, 

303 IEEE11073Parser.MINUTE_MIN, 

304 IEEE11073Parser.MINUTE_MAX, 

305 ) 

306 if not IEEE11073Parser.SECOND_MIN <= timestamp.second <= IEEE11073Parser.SECOND_MAX: 

307 raise ValueRangeError( 

308 "second", 

309 timestamp.second, 

310 IEEE11073Parser.SECOND_MIN, 

311 IEEE11073Parser.SECOND_MAX, 

312 ) 

313 

314 return bytearray( 

315 struct.pack( 

316 "<HBBBBB", 

317 timestamp.year, 

318 timestamp.month, 

319 timestamp.day, 

320 timestamp.hour, 

321 timestamp.minute, 

322 timestamp.second, 

323 ) 

324 )