Coverage for src/bluetooth_sig/gatt/characteristics/glucose_feature.py: 89%
102 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-30 00:10 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-30 00:10 +0000
1"""Glucose Feature characteristic implementation."""
3from __future__ import annotations
5from enum import IntFlag
7import msgspec
9from ..context import CharacteristicContext
10from .base import BaseCharacteristic
11from .utils import DataParser
14class GlucoseFeatures(IntFlag):
15 """Glucose Feature flags according to Bluetooth SIG specification."""
17 LOW_BATTERY_DETECTION = 0x0001
18 SENSOR_MALFUNCTION_DETECTION = 0x0002
19 SENSOR_SAMPLE_SIZE = 0x0004
20 SENSOR_STRIP_INSERTION_ERROR = 0x0008
21 SENSOR_STRIP_TYPE_ERROR = 0x0010
22 SENSOR_RESULT_HIGH_LOW = 0x0020
23 SENSOR_TEMPERATURE_HIGH_LOW = 0x0040
24 SENSOR_READ_INTERRUPT = 0x0080
25 GENERAL_DEVICE_FAULT = 0x0100
26 TIME_FAULT = 0x0200
27 MULTIPLE_BOND_SUPPORT = 0x0400
29 def __str__(self) -> str:
30 """Get human-readable description for a feature."""
31 descriptions = {
32 self.LOW_BATTERY_DETECTION.value: "Low Battery Detection During Measurement Supported",
33 self.SENSOR_MALFUNCTION_DETECTION.value: "Sensor Malfunction Detection Supported",
34 self.SENSOR_SAMPLE_SIZE.value: "Sensor Sample Size Supported",
35 self.SENSOR_STRIP_INSERTION_ERROR.value: "Sensor Strip Insertion Error Detection Supported",
36 self.SENSOR_STRIP_TYPE_ERROR.value: "Sensor Strip Type Error Detection Supported",
37 self.SENSOR_RESULT_HIGH_LOW.value: "Sensor Result High-Low Detection Supported",
38 self.SENSOR_TEMPERATURE_HIGH_LOW.value: "Sensor Temperature High-Low Detection Supported",
39 self.SENSOR_READ_INTERRUPT.value: "Sensor Read Interrupt Detection Supported",
40 self.GENERAL_DEVICE_FAULT.value: "General Device Fault Supported",
41 self.TIME_FAULT.value: "Time Fault Supported",
42 self.MULTIPLE_BOND_SUPPORT.value: "Multiple Bond Supported",
43 }
44 return descriptions.get(self.value, f"Reserved feature bit {self.value:04x}")
46 def get_enabled_features(self) -> list[GlucoseFeatures]:
47 """Get list of human-readable enabled features."""
48 enabled = list[GlucoseFeatures]()
49 for feature in GlucoseFeatures:
50 if self & feature:
51 enabled.append(feature)
52 return enabled
55class GlucoseFeatureData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods,too-many-instance-attributes
56 """Parsed data from Glucose Feature characteristic."""
58 features_bitmap: GlucoseFeatures
59 low_battery_detection: bool
60 sensor_malfunction_detection: bool
61 sensor_sample_size: bool
62 sensor_strip_insertion_error: bool
63 sensor_strip_type_error: bool
64 sensor_result_high_low: bool
65 sensor_temperature_high_low: bool
66 sensor_read_interrupt: bool
67 general_device_fault: bool
68 time_fault: bool
69 multiple_bond_support: bool
70 enabled_features: tuple[GlucoseFeatures, ...]
71 feature_count: int
74class GlucoseFeatureCharacteristic(BaseCharacteristic):
75 """Glucose Feature characteristic (0x2A51).
77 Used to expose the supported features of a glucose monitoring
78 device. Indicates which optional fields and capabilities are
79 available.
80 """
82 _characteristic_name: str = "Glucose Feature"
83 _manual_unit: str = "bitmap" # Feature bitmap
85 min_length: int = 2 # Features(2) fixed length
86 max_length: int = 2 # Features(2) fixed length
87 allow_variable_length: bool = False # Fixed length
89 def decode_value( # pylint: disable=too-many-locals
90 self, data: bytearray, _ctx: CharacteristicContext | None = None
91 ) -> GlucoseFeatureData:
92 """Parse glucose feature data according to Bluetooth specification.
94 Format: Features(2) - 16-bit bitmap indicating supported features
96 Args:
97 data: Raw bytearray from BLE characteristic
98 ctx: Optional context information
100 Returns:
101 GlucoseFeatureData containing parsed feature bitmap and details
103 Raises:
104 ValueError: If data format is invalid
106 """
107 if len(data) < 2:
108 raise ValueError("Glucose Feature data must be at least 2 bytes")
110 features_bitmap = DataParser.parse_int16(data, 0, signed=False)
111 features = GlucoseFeatures(features_bitmap)
113 # Extract individual feature flags using enum
114 low_battery_detection = bool(features & GlucoseFeatures.LOW_BATTERY_DETECTION)
115 sensor_malfunction_detection = bool(features & GlucoseFeatures.SENSOR_MALFUNCTION_DETECTION)
116 sensor_sample_size = bool(features & GlucoseFeatures.SENSOR_SAMPLE_SIZE)
117 sensor_strip_insertion_error = bool(features & GlucoseFeatures.SENSOR_STRIP_INSERTION_ERROR)
118 sensor_strip_type_error = bool(features & GlucoseFeatures.SENSOR_STRIP_TYPE_ERROR)
119 sensor_result_high_low = bool(features & GlucoseFeatures.SENSOR_RESULT_HIGH_LOW)
120 sensor_temperature_high_low = bool(features & GlucoseFeatures.SENSOR_TEMPERATURE_HIGH_LOW)
121 sensor_read_interrupt = bool(features & GlucoseFeatures.SENSOR_READ_INTERRUPT)
122 general_device_fault = bool(features & GlucoseFeatures.GENERAL_DEVICE_FAULT)
123 time_fault = bool(features & GlucoseFeatures.TIME_FAULT)
124 multiple_bond_support = bool(features & GlucoseFeatures.MULTIPLE_BOND_SUPPORT)
126 # Get enabled features using the enum method
127 enabled_features = tuple(features.get_enabled_features())
129 return GlucoseFeatureData(
130 features_bitmap=features,
131 low_battery_detection=low_battery_detection,
132 sensor_malfunction_detection=sensor_malfunction_detection,
133 sensor_sample_size=sensor_sample_size,
134 sensor_strip_insertion_error=sensor_strip_insertion_error,
135 sensor_strip_type_error=sensor_strip_type_error,
136 sensor_result_high_low=sensor_result_high_low,
137 sensor_temperature_high_low=sensor_temperature_high_low,
138 sensor_read_interrupt=sensor_read_interrupt,
139 general_device_fault=general_device_fault,
140 time_fault=time_fault,
141 multiple_bond_support=multiple_bond_support,
142 enabled_features=enabled_features,
143 feature_count=len(enabled_features),
144 )
146 def encode_value(self, data: GlucoseFeatureData) -> bytearray:
147 """Encode GlucoseFeatureData back to bytes.
149 Args:
150 data: GlucoseFeatureData instance to encode
152 Returns:
153 Encoded bytes representing the glucose features
155 """
156 # Reconstruct the features bitmap from individual flags using enum values
157 features_bitmap = 0
158 if data.low_battery_detection:
159 features_bitmap |= GlucoseFeatures.LOW_BATTERY_DETECTION
160 if data.sensor_malfunction_detection:
161 features_bitmap |= GlucoseFeatures.SENSOR_MALFUNCTION_DETECTION
162 if data.sensor_sample_size:
163 features_bitmap |= GlucoseFeatures.SENSOR_SAMPLE_SIZE
164 if data.sensor_strip_insertion_error:
165 features_bitmap |= GlucoseFeatures.SENSOR_STRIP_INSERTION_ERROR
166 if data.sensor_strip_type_error:
167 features_bitmap |= GlucoseFeatures.SENSOR_STRIP_TYPE_ERROR
168 if data.sensor_result_high_low:
169 features_bitmap |= GlucoseFeatures.SENSOR_RESULT_HIGH_LOW
170 if data.sensor_temperature_high_low:
171 features_bitmap |= GlucoseFeatures.SENSOR_TEMPERATURE_HIGH_LOW
172 if data.sensor_read_interrupt:
173 features_bitmap |= GlucoseFeatures.SENSOR_READ_INTERRUPT
174 if data.general_device_fault:
175 features_bitmap |= GlucoseFeatures.GENERAL_DEVICE_FAULT
176 if data.time_fault:
177 features_bitmap |= GlucoseFeatures.TIME_FAULT
178 if data.multiple_bond_support:
179 features_bitmap |= GlucoseFeatures.MULTIPLE_BOND_SUPPORT
181 # Pack as little-endian 16-bit integer
182 return DataParser.encode_int16(features_bitmap, signed=False)
184 def get_feature_description(self, feature_bit: int) -> str:
185 """Get description for a specific feature bit.
187 Args:
188 feature_bit: Bit position (0-15)
190 Returns:
191 Human-readable description of the feature
193 """
194 # Accept either a flag value (power-of-two) or a bit index
195 if feature_bit <= 0:
196 return f"Reserved feature bit {feature_bit}"
198 # If caller passed a power-of-two flag value (e.g., 0x0001), use it
199 if feature_bit & (feature_bit - 1) == 0:
200 feature_value = feature_bit
201 else:
202 # Otherwise treat as bit index (0..15)
203 feature_value = 1 << feature_bit
205 try:
206 feature = GlucoseFeatures(feature_value)
207 return str(feature)
208 except ValueError:
209 return f"Reserved feature bit {feature_bit}"