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
« 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.
3This module provides the translation layer that converts raw integers to domain
4values (and back). Translators have a single responsibility: value interpretation
5and scaling.
7The translation layer is the second stage of the decode pipeline:
8 bytes → [Extractor] → raw_int → [Translator] → typed_value
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"""
16from __future__ import annotations
18import struct
19from abc import ABC, abstractmethod
20from typing import Generic, TypeVar
22from .ieee11073_parser import IEEE11073Parser
24T = TypeVar("T")
27class ValueTranslator(ABC, Generic[T]):
28 """Protocol for raw-to-value translation.
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)
36 Type parameter T is the output type (int, float, etc.).
37 """
39 __slots__ = ()
41 @abstractmethod
42 def translate(self, raw: int) -> T:
43 """Convert raw integer to domain value.
45 Args:
46 raw: Raw integer from extractor.
48 Returns:
49 Typed domain value.
50 """
52 @abstractmethod
53 def untranslate(self, value: T) -> int:
54 """Convert domain value back to raw integer.
56 Args:
57 value: Typed domain value.
59 Returns:
60 Raw integer for encoder.
62 Raises:
63 ValueError: If value cannot be converted to raw integer.
64 """
67class IdentityTranslator(ValueTranslator[int]):
68 """Pass-through translator for values needing no conversion.
70 Used for simple integer types where raw == domain value.
71 """
73 __slots__ = ()
75 def translate(self, raw: int) -> int:
76 """Return raw value unchanged."""
77 return raw
79 def untranslate(self, value: int) -> int:
80 """Return value unchanged."""
81 return value
84class LinearTranslator(ValueTranslator[float]):
85 """Linear scaling translator: value = (raw + offset) * scale_factor.
87 This implements the Bluetooth SIG M, d, b formula:
88 actual_value = M * 10^d * (raw + b)
90 Where:
91 scale_factor = M * 10^d
92 offset = b
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 """
99 __slots__ = ("_scale_factor", "_offset")
101 def __init__(self, scale_factor: float = 1.0, offset: int = 0) -> None:
102 """Initialize with scaling parameters.
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
111 @classmethod
112 def from_mdb(cls, m: int, d: int, b: int) -> LinearTranslator:
113 """Create from Bluetooth SIG M, d, b parameters.
115 Args:
116 m: Multiplier factor.
117 d: Decimal exponent (10^d).
118 b: Offset added to raw value.
120 Returns:
121 Configured LinearTranslator instance.
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)
130 @property
131 def scale_factor(self) -> float:
132 """Get the scale factor (M * 10^d)."""
133 return self._scale_factor
135 @property
136 def offset(self) -> int:
137 """Return the offset value (b parameter)."""
138 return self._offset
140 def translate(self, raw: int) -> float:
141 """Apply linear scaling: (raw + offset) * scale_factor."""
142 return (raw + self._offset) * self._scale_factor
144 def untranslate(self, value: float) -> int:
145 """Reverse linear scaling: (value / scale_factor) - offset."""
146 return int((value / self._scale_factor) - self._offset)
149class PercentageTranslator(ValueTranslator[int]):
150 """Translator for percentage values (0-100).
152 Simple pass-through since percentage is typically stored as uint8 0-100.
153 Provides semantic clarity in the pipeline.
154 """
156 __slots__ = ()
158 def translate(self, raw: int) -> int:
159 """Return percentage value."""
160 return raw
162 def untranslate(self, value: int) -> int:
163 """Return percentage as raw."""
164 return value
167class SfloatTranslator(ValueTranslator[float]):
168 """Translator for IEEE 11073 16-bit SFLOAT format.
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.
174 Special value handling:
175 - 0x07FF: NaN
176 - 0x0800: NRes (Not at this resolution)
177 - 0x07FE: +INFINITY
178 - 0x0802: -INFINITY
179 """
181 __slots__ = ()
183 NAN = 0x07FF
184 NRES = 0x0800
185 POSITIVE_INFINITY = 0x07FE
186 NEGATIVE_INFINITY = 0x0802
188 def translate(self, raw: int) -> float:
189 """Convert raw SFLOAT bits to float value.
191 Args:
192 raw: Raw 16-bit integer from extractor.
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)
200 def untranslate(self, value: float) -> int:
201 """Encode float to SFLOAT raw bits.
203 Args:
204 value: Float value to encode.
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)
213class Float32IEEETranslator(ValueTranslator[float]):
214 """Translator for IEEE 11073 32-bit FLOAT format (medfloat32).
216 Used by medical device profiles for higher precision measurements.
217 """
219 __slots__ = ()
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
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)
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)
239class Float32IEEE754Translator(ValueTranslator[float]):
240 """Translator for standard IEEE-754 32-bit float (not IEEE 11073).
242 For characteristics using standard single-precision floats rather
243 than the medical device FLOAT32 format.
244 """
246 __slots__ = ()
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])
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)
259# Singleton instances for stateless translators
260IDENTITY = IdentityTranslator()
261PERCENTAGE = PercentageTranslator()
262SFLOAT = SfloatTranslator()
263FLOAT32_IEEE11073 = Float32IEEETranslator()
264FLOAT32_IEEE754 = Float32IEEE754Translator()
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.
276 Can be configured either with scale_factor/offset or M/d/b parameters.
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.
285 Returns:
286 Configured LinearTranslator.
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)
295 if m is not None and d is not None:
296 return LinearTranslator.from_mdb(m, d, b or 0)
298 return LinearTranslator(scale_factor=1.0, offset=offset)