Coverage for src / bluetooth_sig / gatt / characteristics / glucose_feature.py: 89%

101 statements  

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

1"""Glucose Feature characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5from enum import IntFlag 

6 

7import msgspec 

8 

9from ..context import CharacteristicContext 

10from .base import BaseCharacteristic 

11from .utils import DataParser 

12 

13 

14class GlucoseFeatures(IntFlag): 

15 """Glucose Feature flags according to Bluetooth SIG specification.""" 

16 

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 

28 

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}") 

45 

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 

53 

54 

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.""" 

57 

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 

72 

73 

74class GlucoseFeatureCharacteristic(BaseCharacteristic[GlucoseFeatureData]): 

75 """Glucose Feature characteristic (0x2A51). 

76 

77 Used to expose the supported features of a glucose monitoring 

78 device. Indicates which optional fields and capabilities are 

79 available. 

80 """ 

81 

82 _characteristic_name: str = "Glucose Feature" 

83 _manual_unit: str = "bitmap" # Feature bitmap 

84 

85 expected_length: int = 2 

86 min_length: int = 2 # Features(2) fixed length 

87 max_length: int = 2 # Features(2) fixed length 

88 allow_variable_length: bool = False # Fixed length 

89 

90 def _decode_value( # pylint: disable=too-many-locals 

91 self, data: bytearray, ctx: CharacteristicContext | None = None 

92 ) -> GlucoseFeatureData: 

93 """Parse glucose feature data according to Bluetooth specification. 

94 

95 Format: Features(2) - 16-bit bitmap indicating supported features 

96 

97 Args: 

98 data: Raw bytearray from BLE characteristic 

99 ctx: Optional context information 

100 

101 Returns: 

102 GlucoseFeatureData containing parsed feature bitmap and details 

103 

104 Raises: 

105 ValueError: If data format is invalid 

106 

107 """ 

108 features_bitmap = DataParser.parse_int16(data, 0, signed=False) 

109 features = GlucoseFeatures(features_bitmap) 

110 

111 # Extract individual feature flags using enum 

112 low_battery_detection = bool(features & GlucoseFeatures.LOW_BATTERY_DETECTION) 

113 sensor_malfunction_detection = bool(features & GlucoseFeatures.SENSOR_MALFUNCTION_DETECTION) 

114 sensor_sample_size = bool(features & GlucoseFeatures.SENSOR_SAMPLE_SIZE) 

115 sensor_strip_insertion_error = bool(features & GlucoseFeatures.SENSOR_STRIP_INSERTION_ERROR) 

116 sensor_strip_type_error = bool(features & GlucoseFeatures.SENSOR_STRIP_TYPE_ERROR) 

117 sensor_result_high_low = bool(features & GlucoseFeatures.SENSOR_RESULT_HIGH_LOW) 

118 sensor_temperature_high_low = bool(features & GlucoseFeatures.SENSOR_TEMPERATURE_HIGH_LOW) 

119 sensor_read_interrupt = bool(features & GlucoseFeatures.SENSOR_READ_INTERRUPT) 

120 general_device_fault = bool(features & GlucoseFeatures.GENERAL_DEVICE_FAULT) 

121 time_fault = bool(features & GlucoseFeatures.TIME_FAULT) 

122 multiple_bond_support = bool(features & GlucoseFeatures.MULTIPLE_BOND_SUPPORT) 

123 

124 # Get enabled features using the enum method 

125 enabled_features = tuple(features.get_enabled_features()) 

126 

127 return GlucoseFeatureData( 

128 features_bitmap=features, 

129 low_battery_detection=low_battery_detection, 

130 sensor_malfunction_detection=sensor_malfunction_detection, 

131 sensor_sample_size=sensor_sample_size, 

132 sensor_strip_insertion_error=sensor_strip_insertion_error, 

133 sensor_strip_type_error=sensor_strip_type_error, 

134 sensor_result_high_low=sensor_result_high_low, 

135 sensor_temperature_high_low=sensor_temperature_high_low, 

136 sensor_read_interrupt=sensor_read_interrupt, 

137 general_device_fault=general_device_fault, 

138 time_fault=time_fault, 

139 multiple_bond_support=multiple_bond_support, 

140 enabled_features=enabled_features, 

141 feature_count=len(enabled_features), 

142 ) 

143 

144 def _encode_value(self, data: GlucoseFeatureData) -> bytearray: 

145 """Encode GlucoseFeatureData back to bytes. 

146 

147 Args: 

148 data: GlucoseFeatureData instance to encode 

149 

150 Returns: 

151 Encoded bytes representing the glucose features 

152 

153 """ 

154 # Reconstruct the features bitmap from individual flags using enum values 

155 features_bitmap = 0 

156 if data.low_battery_detection: 

157 features_bitmap |= GlucoseFeatures.LOW_BATTERY_DETECTION 

158 if data.sensor_malfunction_detection: 

159 features_bitmap |= GlucoseFeatures.SENSOR_MALFUNCTION_DETECTION 

160 if data.sensor_sample_size: 

161 features_bitmap |= GlucoseFeatures.SENSOR_SAMPLE_SIZE 

162 if data.sensor_strip_insertion_error: 

163 features_bitmap |= GlucoseFeatures.SENSOR_STRIP_INSERTION_ERROR 

164 if data.sensor_strip_type_error: 

165 features_bitmap |= GlucoseFeatures.SENSOR_STRIP_TYPE_ERROR 

166 if data.sensor_result_high_low: 

167 features_bitmap |= GlucoseFeatures.SENSOR_RESULT_HIGH_LOW 

168 if data.sensor_temperature_high_low: 

169 features_bitmap |= GlucoseFeatures.SENSOR_TEMPERATURE_HIGH_LOW 

170 if data.sensor_read_interrupt: 

171 features_bitmap |= GlucoseFeatures.SENSOR_READ_INTERRUPT 

172 if data.general_device_fault: 

173 features_bitmap |= GlucoseFeatures.GENERAL_DEVICE_FAULT 

174 if data.time_fault: 

175 features_bitmap |= GlucoseFeatures.TIME_FAULT 

176 if data.multiple_bond_support: 

177 features_bitmap |= GlucoseFeatures.MULTIPLE_BOND_SUPPORT 

178 

179 # Pack as little-endian 16-bit integer 

180 return DataParser.encode_int16(features_bitmap, signed=False) 

181 

182 def get_feature_description(self, feature_bit: int) -> str: 

183 """Get description for a specific feature bit. 

184 

185 Args: 

186 feature_bit: Bit position (0-15) 

187 

188 Returns: 

189 Human-readable description of the feature 

190 

191 """ 

192 # Accept either a flag value (power-of-two) or a bit index 

193 if feature_bit <= 0: 

194 return f"Reserved feature bit {feature_bit}" 

195 

196 # If caller passed a power-of-two flag value (e.g., 0x0001), use it 

197 if feature_bit & (feature_bit - 1) == 0: 

198 feature_value = feature_bit 

199 else: 

200 # Otherwise treat as bit index (0..15) 

201 feature_value = 1 << feature_bit 

202 

203 try: 

204 feature = GlucoseFeatures(feature_value) 

205 return str(feature) 

206 except ValueError: 

207 return f"Reserved feature bit {feature_bit}"