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

169 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 20:14 +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), 

42 # 0x800001 (RFU), 0x800002 (-Inf) 

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

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

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

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

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

48 FLOAT32_MANTISSA_MASK = 0x00FFFFFF 

49 FLOAT32_MANTISSA_SIGN_BIT = 0x00800000 

50 FLOAT32_MANTISSA_CONVERSION = 0x01000000 

51 FLOAT32_EXPONENT_MASK = 0xFF 

52 FLOAT32_EXPONENT_SIGN_BIT = 0x80 

53 FLOAT32_EXPONENT_CONVERSION = 0x100 

54 FLOAT32_MANTISSA_MAX = 8388608 # 2^23 

55 FLOAT32_EXPONENT_MIN = -128 

56 FLOAT32_EXPONENT_MAX = 127 

57 FLOAT32_EXPONENT_BIAS = 128 

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

59 FLOAT32_MANTISSA_START_BIT = 0 

60 FLOAT32_MANTISSA_BIT_WIDTH = 24 

61 FLOAT32_EXPONENT_START_BIT = 24 

62 FLOAT32_EXPONENT_BIT_WIDTH = 8 

63 

64 # IEEE-11073 timestamp validation constants 

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

66 MONTH_MIN = 1 

67 MONTH_MAX = 12 

68 DAY_MIN = 1 

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

70 HOUR_MIN = 0 

71 HOUR_MAX = 23 # 24-hour format 

72 MINUTE_MIN = 0 

73 MINUTE_MAX = 59 

74 SECOND_MIN = 0 

75 SECOND_MAX = 59 

76 

77 # Common constants 

78 TIMESTAMP_LENGTH = 7 

79 

80 @staticmethod 

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

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

83 

84 Args: 

85 data: Raw bytes/bytearray 

86 offset: Offset in the data 

87 

88 """ 

89 if len(data) < offset + 2: 

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

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

92 

93 # Handle special values 

94 if raw_value == IEEE11073Parser.SFLOAT_NAN: 

95 return float("nan") # NaN 

96 if raw_value == IEEE11073Parser.SFLOAT_NRES: 

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

98 if raw_value == IEEE11073Parser.SFLOAT_POSITIVE_INFINITY: 

99 return float("inf") # +INFINITY 

100 if raw_value == IEEE11073Parser.SFLOAT_NEGATIVE_INFINITY: 

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

102 

103 # Extract mantissa and exponent 

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

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

106 mantissa = mantissa - IEEE11073Parser.SFLOAT_MANTISSA_CONVERSION 

107 

108 exponent = BitFieldUtils.extract_bit_field_from_mask( 

109 raw_value, 

110 IEEE11073Parser.SFLOAT_EXPONENT_MASK, 

111 IEEE11073Parser.SFLOAT_EXPONENT_START_BIT, 

112 ) 

113 exponent = exponent - IEEE11073Parser.SFLOAT_EXPONENT_BIAS 

114 

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

116 

117 @staticmethod 

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

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

120 if len(data) < offset + 4: 

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

122 

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

124 

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

126 if raw_value == IEEE11073Parser.FLOAT32_NAN: 

127 return float("nan") 

128 if raw_value == IEEE11073Parser.FLOAT32_POSITIVE_INFINITY: 

129 return float("inf") 

130 if raw_value == IEEE11073Parser.FLOAT32_NEGATIVE_INFINITY: 

131 return float("-inf") 

132 if raw_value == IEEE11073Parser.FLOAT32_NRES: 

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

134 

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

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

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

138 mantissa = mantissa - IEEE11073Parser.FLOAT32_MANTISSA_CONVERSION 

139 

140 exponent = BitFieldUtils.extract_bit_field_from_mask( 

141 raw_value, 

142 IEEE11073Parser.FLOAT32_EXPONENT_MASK, 

143 IEEE11073Parser.FLOAT32_EXPONENT_START_BIT, 

144 ) 

145 exponent = exponent - IEEE11073Parser.FLOAT32_EXPONENT_BIAS 

146 

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

148 

149 @staticmethod 

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

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

152 if math.isnan(value): 

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

154 if math.isinf(value): 

155 if value > 0: 

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

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

158 

159 # Find best exponent and mantissa representation 

160 exponent = 0 

161 mantissa = value 

162 

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

164 mantissa /= 10 

165 exponent += 1 

166 

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

168 mantissa *= 10 

169 exponent -= 1 

170 

171 mantissa_int = int(round(mantissa)) 

172 

173 # Pack into 16-bit value 

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

175 if mantissa_int < 0: 

176 mantissa_int = mantissa_int + IEEE11073Parser.SFLOAT_MANTISSA_CONVERSION 

177 

178 raw_value = BitFieldUtils.merge_bit_fields( 

179 ( 

180 mantissa_int, 

181 IEEE11073Parser.SFLOAT_MANTISSA_START_BIT, 

182 IEEE11073Parser.SFLOAT_MANTISSA_BIT_WIDTH, 

183 ), 

184 ( 

185 exponent, 

186 IEEE11073Parser.SFLOAT_EXPONENT_START_BIT, 

187 IEEE11073Parser.SFLOAT_EXPONENT_BIT_WIDTH, 

188 ), 

189 ) 

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

191 

192 @staticmethod 

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

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

195 if math.isnan(value): 

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

197 if math.isinf(value): 

198 if value > 0: 

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

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

201 

202 if value == 0.0: 

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

204 

205 # Find the best representation by trying different exponents 

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

207 best_mantissa = 0 

208 best_exponent = 0 

209 best_error = float("inf") 

210 

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

212 for exp in range( 

213 IEEE11073Parser.FLOAT32_EXPONENT_MIN, 

214 IEEE11073Parser.FLOAT32_EXPONENT_MAX + 1, 

215 ): 

216 # Calculate what mantissa would be with this exponent 

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

218 

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

220 if abs(potential_mantissa) < IEEE11073Parser.FLOAT32_MANTISSA_MAX: 

221 # Round to integer 

222 mantissa_int = round(potential_mantissa) 

223 

224 # Calculate the actual value this would represent 

225 actual_value = mantissa_int * (10**exp) 

226 error = abs(value - actual_value) 

227 

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

229 if error < best_error: 

230 best_error = error 

231 best_mantissa = mantissa_int 

232 best_exponent = exp 

233 

234 # If we found an exact representation, use it 

235 if error == 0.0: 

236 break 

237 

238 # Validate mantissa fits in 24-bit signed range 

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

240 raise ValueRangeError( 

241 "IEEE-11073 FLOAT32 mantissa", 

242 best_mantissa, 

243 -(IEEE11073Parser.FLOAT32_MANTISSA_MAX - 1), 

244 IEEE11073Parser.FLOAT32_MANTISSA_MAX - 1, 

245 ) 

246 

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

248 # Convert signed values to unsigned for bit packing 

249 mantissa_unsigned = ( 

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

251 ) 

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

253 

254 raw_value = BitFieldUtils.merge_bit_fields( 

255 ( 

256 mantissa_unsigned, 

257 IEEE11073Parser.FLOAT32_MANTISSA_START_BIT, 

258 IEEE11073Parser.FLOAT32_MANTISSA_BIT_WIDTH, 

259 ), 

260 ( 

261 exponent_unsigned, 

262 IEEE11073Parser.FLOAT32_EXPONENT_START_BIT, 

263 IEEE11073Parser.FLOAT32_EXPONENT_BIT_WIDTH, 

264 ), 

265 ) 

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

267 

268 @staticmethod 

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

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

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

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

273 

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

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

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

277 

278 @staticmethod 

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

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

281 # Validate ranges per IEEE-11073 specification 

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

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

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

285 raise ValueRangeError( 

286 "month", 

287 timestamp.month, 

288 IEEE11073Parser.MONTH_MIN, 

289 IEEE11073Parser.MONTH_MAX, 

290 ) 

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

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

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

294 raise ValueRangeError( 

295 "hour", 

296 timestamp.hour, 

297 IEEE11073Parser.HOUR_MIN, 

298 IEEE11073Parser.HOUR_MAX, 

299 ) 

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

301 raise ValueRangeError( 

302 "minute", 

303 timestamp.minute, 

304 IEEE11073Parser.MINUTE_MIN, 

305 IEEE11073Parser.MINUTE_MAX, 

306 ) 

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

308 raise ValueRangeError( 

309 "second", 

310 timestamp.second, 

311 IEEE11073Parser.SECOND_MIN, 

312 IEEE11073Parser.SECOND_MAX, 

313 ) 

314 

315 return bytearray( 

316 struct.pack( 

317 "<HBBBBB", 

318 timestamp.year, 

319 timestamp.month, 

320 timestamp.day, 

321 timestamp.hour, 

322 timestamp.minute, 

323 timestamp.second, 

324 ) 

325 )