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

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): 

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

88 

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. 

93 

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

95 

96 Args: 

97 data: Raw bytearray from BLE characteristic 

98 ctx: Optional context information 

99 

100 Returns: 

101 GlucoseFeatureData containing parsed feature bitmap and details 

102 

103 Raises: 

104 ValueError: If data format is invalid 

105 

106 """ 

107 if len(data) < 2: 

108 raise ValueError("Glucose Feature data must be at least 2 bytes") 

109 

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

111 features = GlucoseFeatures(features_bitmap) 

112 

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) 

125 

126 # Get enabled features using the enum method 

127 enabled_features = tuple(features.get_enabled_features()) 

128 

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 ) 

145 

146 def encode_value(self, data: GlucoseFeatureData) -> bytearray: 

147 """Encode GlucoseFeatureData back to bytes. 

148 

149 Args: 

150 data: GlucoseFeatureData instance to encode 

151 

152 Returns: 

153 Encoded bytes representing the glucose features 

154 

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 

180 

181 # Pack as little-endian 16-bit integer 

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

183 

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

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

186 

187 Args: 

188 feature_bit: Bit position (0-15) 

189 

190 Returns: 

191 Human-readable description of the feature 

192 

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

197 

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 

204 

205 try: 

206 feature = GlucoseFeatures(feature_value) 

207 return str(feature) 

208 except ValueError: 

209 return f"Reserved feature bit {feature_bit}"