Coverage for src / bluetooth_sig / gatt / characteristics / utils / translators.py: 78%

87 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 20:14 +0000

1"""Value translators for the BLE encoding/decoding pipeline. 

2 

3This module provides the translation layer that converts raw integers to domain 

4values (and back). Translators have a single responsibility: value interpretation 

5and scaling. 

6 

7The translation layer is the second stage of the decode pipeline: 

8 bytes → [Extractor] → raw_int → [Translator] → typed_value 

9 

10Translators do NOT handle: 

11- Byte extraction (that's the extractor's job) 

12- Special value detection (that's a pipeline interception point) 

13- Validation (that's a separate validation layer) 

14""" 

15 

16from __future__ import annotations 

17 

18import struct 

19from abc import ABC, abstractmethod 

20from typing import Generic, TypeVar 

21 

22from .ieee11073_parser import IEEE11073Parser 

23 

24T = TypeVar("T") 

25 

26 

27class ValueTranslator(ABC, Generic[T]): 

28 """Protocol for raw-to-value translation. 

29 

30 Translators convert raw integer values to typed domain values and back. 

31 They handle scaling, offset, and type conversion but NOT: 

32 - Byte extraction (use RawExtractor) 

33 - Special value handling (pipeline intercepts before translation) 

34 - Validation (separate validation layer) 

35 

36 Type parameter T is the output type (int, float, etc.). 

37 """ 

38 

39 __slots__ = () 

40 

41 @abstractmethod 

42 def translate(self, raw: int) -> T: 

43 """Convert raw integer to domain value. 

44 

45 Args: 

46 raw: Raw integer from extractor. 

47 

48 Returns: 

49 Typed domain value. 

50 """ 

51 

52 @abstractmethod 

53 def untranslate(self, value: T) -> int: 

54 """Convert domain value back to raw integer. 

55 

56 Args: 

57 value: Typed domain value. 

58 

59 Returns: 

60 Raw integer for encoder. 

61 

62 Raises: 

63 ValueError: If value cannot be converted to raw integer. 

64 """ 

65 

66 

67class IdentityTranslator(ValueTranslator[int]): 

68 """Pass-through translator for values needing no conversion. 

69 

70 Used for simple integer types where raw == domain value. 

71 """ 

72 

73 __slots__ = () 

74 

75 def translate(self, raw: int) -> int: 

76 """Return raw value unchanged.""" 

77 return raw 

78 

79 def untranslate(self, value: int) -> int: 

80 """Return value unchanged.""" 

81 return value 

82 

83 

84class LinearTranslator(ValueTranslator[float]): 

85 """Linear scaling translator: value = (raw + offset) * scale_factor. 

86 

87 This implements the Bluetooth SIG M, d, b formula: 

88 actual_value = M * 10^d * (raw + b) 

89 

90 Where: 

91 scale_factor = M * 10^d 

92 offset = b 

93 

94 Examples: 

95 Temperature 0.01°C resolution: LinearTranslator(scale_factor=0.01, offset=0) 

96 Humidity 0.01% resolution: LinearTranslator(scale_factor=0.01, offset=0) 

97 """ 

98 

99 __slots__ = ("_scale_factor", "_offset") 

100 

101 def __init__(self, scale_factor: float = 1.0, offset: int = 0) -> None: 

102 """Initialize with scaling parameters. 

103 

104 Args: 

105 scale_factor: Multiplier applied after offset (M * 10^d). 

106 offset: Value added to raw before scaling (b). 

107 """ 

108 self._scale_factor = scale_factor 

109 self._offset = offset 

110 

111 @classmethod 

112 def from_mdb(cls, m: int, d: int, b: int) -> LinearTranslator: 

113 """Create from Bluetooth SIG M, d, b parameters. 

114 

115 Args: 

116 m: Multiplier factor. 

117 d: Decimal exponent (10^d). 

118 b: Offset added to raw value. 

119 

120 Returns: 

121 Configured LinearTranslator instance. 

122 

123 Examples: 

124 Temperature 0.01°C: from_mdb(1, -2, 0) 

125 Percentage 0.5%: from_mdb(5, -1, 0) 

126 """ 

127 scale_factor = m * (10**d) 

128 return cls(scale_factor=scale_factor, offset=b) 

129 

130 @property 

131 def scale_factor(self) -> float: 

132 """Get the scale factor (M * 10^d).""" 

133 return self._scale_factor 

134 

135 @property 

136 def offset(self) -> int: 

137 """Return the offset value (b parameter).""" 

138 return self._offset 

139 

140 def translate(self, raw: int) -> float: 

141 """Apply linear scaling: (raw + offset) * scale_factor.""" 

142 return (raw + self._offset) * self._scale_factor 

143 

144 def untranslate(self, value: float) -> int: 

145 """Reverse linear scaling: (value / scale_factor) - offset.""" 

146 return int((value / self._scale_factor) - self._offset) 

147 

148 

149class PercentageTranslator(ValueTranslator[int]): 

150 """Translator for percentage values (0-100). 

151 

152 Simple pass-through since percentage is typically stored as uint8 0-100. 

153 Provides semantic clarity in the pipeline. 

154 """ 

155 

156 __slots__ = () 

157 

158 def translate(self, raw: int) -> int: 

159 """Return percentage value.""" 

160 return raw 

161 

162 def untranslate(self, value: int) -> int: 

163 """Return percentage as raw.""" 

164 return value 

165 

166 

167class SfloatTranslator(ValueTranslator[float]): 

168 """Translator for IEEE 11073 16-bit SFLOAT format. 

169 

170 SFLOAT is used by many Bluetooth Health profiles (blood pressure, 

171 glucose, weight scale, etc.). It provides a compact representation 

172 with ~3 significant digits. 

173 

174 Special value handling: 

175 - 0x07FF: NaN 

176 - 0x0800: NRes (Not at this resolution) 

177 - 0x07FE: +INFINITY 

178 - 0x0802: -INFINITY 

179 """ 

180 

181 __slots__ = () 

182 

183 NAN = 0x07FF 

184 NRES = 0x0800 

185 POSITIVE_INFINITY = 0x07FE 

186 NEGATIVE_INFINITY = 0x0802 

187 

188 def translate(self, raw: int) -> float: 

189 """Convert raw SFLOAT bits to float value. 

190 

191 Args: 

192 raw: Raw 16-bit integer from extractor. 

193 

194 Returns: 

195 Decoded float value, or NaN/Inf for special values. 

196 """ 

197 raw_bytes = raw.to_bytes(2, byteorder="little", signed=False) 

198 return IEEE11073Parser.parse_sfloat(raw_bytes, offset=0) 

199 

200 def untranslate(self, value: float) -> int: 

201 """Encode float to SFLOAT raw bits. 

202 

203 Args: 

204 value: Float value to encode. 

205 

206 Returns: 

207 Raw 16-bit integer for extractor. 

208 """ 

209 encoded = IEEE11073Parser.encode_sfloat(value) 

210 return int.from_bytes(encoded, byteorder="little", signed=False) 

211 

212 

213class Float32IEEETranslator(ValueTranslator[float]): 

214 """Translator for IEEE 11073 32-bit FLOAT format (medfloat32). 

215 

216 Used by medical device profiles for higher precision measurements. 

217 """ 

218 

219 __slots__ = () 

220 

221 # Per IEEE 11073-20601 and Bluetooth GSS special values (exponent=0) 

222 NAN = 0x007FFFFF # Mantissa 0x7FFFFF 

223 NRES = 0x00800000 # Mantissa 0x800000 

224 RFU = 0x00800001 # Mantissa 0x800001 (Reserved for Future Use) 

225 POSITIVE_INFINITY = 0x007FFFFE # Mantissa 0x7FFFFE 

226 NEGATIVE_INFINITY = 0x00800002 # Mantissa 0x800002 

227 

228 def translate(self, raw: int) -> float: 

229 """Convert raw FLOAT32 bits to float value.""" 

230 raw_bytes = raw.to_bytes(4, byteorder="little", signed=False) 

231 return IEEE11073Parser.parse_float32(raw_bytes, offset=0) 

232 

233 def untranslate(self, value: float) -> int: 

234 """Encode float to FLOAT32 raw bits.""" 

235 encoded = IEEE11073Parser.encode_float32(value) 

236 return int.from_bytes(encoded, byteorder="little", signed=False) 

237 

238 

239class Float32IEEE754Translator(ValueTranslator[float]): 

240 """Translator for standard IEEE-754 32-bit float (not IEEE 11073). 

241 

242 For characteristics using standard single-precision floats rather 

243 than the medical device FLOAT32 format. 

244 """ 

245 

246 __slots__ = () 

247 

248 def translate(self, raw: int) -> float: 

249 """Convert raw bits to IEEE-754 float.""" 

250 raw_bytes = raw.to_bytes(4, byteorder="little", signed=False) 

251 return float(struct.unpack("<f", raw_bytes)[0]) 

252 

253 def untranslate(self, value: float) -> int: 

254 """Encode IEEE-754 float to raw bits.""" 

255 raw_bytes = struct.pack("<f", value) 

256 return int.from_bytes(raw_bytes, byteorder="little", signed=False) 

257 

258 

259# Singleton instances for stateless translators 

260IDENTITY = IdentityTranslator() 

261PERCENTAGE = PercentageTranslator() 

262SFLOAT = SfloatTranslator() 

263FLOAT32_IEEE11073 = Float32IEEETranslator() 

264FLOAT32_IEEE754 = Float32IEEE754Translator() 

265 

266 

267def create_linear_translator( 

268 scale_factor: float | None = None, 

269 offset: int = 0, 

270 m: int | None = None, 

271 d: int | None = None, 

272 b: int | None = None, 

273) -> LinearTranslator: 

274 """Factory for creating LinearTranslator instances. 

275 

276 Can be configured either with scale_factor/offset or M/d/b parameters. 

277 

278 Args: 

279 scale_factor: Direct scale factor (takes precedence over M/d). 

280 offset: Direct offset (takes precedence over b). 

281 m: Bluetooth SIG multiplier. 

282 d: Bluetooth SIG decimal exponent. 

283 b: Bluetooth SIG offset. 

284 

285 Returns: 

286 Configured LinearTranslator. 

287 

288 Examples: 

289 >>> temp_translator = create_linear_translator(scale_factor=0.01) 

290 >>> temp_translator = create_linear_translator(m=1, d=-2, b=0) 

291 """ 

292 if scale_factor is not None: 

293 return LinearTranslator(scale_factor=scale_factor, offset=offset) 

294 

295 if m is not None and d is not None: 

296 return LinearTranslator.from_mdb(m, d, b or 0) 

297 

298 return LinearTranslator(scale_factor=1.0, offset=offset)