Coverage for src / bluetooth_sig / gatt / characteristics / cgm_feature.py: 100%

67 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-18 11:17 +0000

1"""CGM Feature characteristic implementation. 

2 

3Implements the CGM Feature characteristic (0x2AA8). Fixed-size structure of 

46 bytes: 24-bit feature flags + packed nibble type/location + 16-bit E2E-CRC. 

5 

6Structure (from GSS YAML): 

7 CGM Feature (3 bytes, boolean[24]) 

8 CGM Type-Sample Location (1 byte, two 4-bit nibbles packed) 

9 E2E-CRC (2 bytes, uint16) -- always present per spec 

10 

11References: 

12 Bluetooth SIG Continuous Glucose Monitoring Service 

13 org.bluetooth.characteristic.cgm_feature (GSS YAML) 

14""" 

15 

16from __future__ import annotations 

17 

18from enum import IntEnum, IntFlag 

19 

20import msgspec 

21 

22from ..context import CharacteristicContext 

23from .base import BaseCharacteristic 

24from .utils import DataParser 

25 

26_NIBBLE_MASK = 0x0F 

27_NIBBLE_SHIFT = 4 

28 

29 

30class CGMFeatureFlags(IntFlag): 

31 """CGM Feature flags (24-bit).""" 

32 

33 CALIBRATION_SUPPORTED = 0x000001 

34 PATIENT_HIGH_LOW_ALERTS = 0x000002 

35 HYPO_ALERTS = 0x000004 

36 HYPER_ALERTS = 0x000008 

37 RATE_ALERTS = 0x000010 

38 DEVICE_SPECIFIC_ALERT = 0x000020 

39 SENSOR_MALFUNCTION_DETECTION = 0x000040 

40 SENSOR_TEMP_HIGH_LOW_DETECTION = 0x000080 

41 SENSOR_RESULT_HIGH_LOW_DETECTION = 0x000100 

42 LOW_BATTERY_DETECTION = 0x000200 

43 SENSOR_TYPE_ERROR_DETECTION = 0x000400 

44 GENERAL_DEVICE_FAULT = 0x000800 

45 E2E_CRC_SUPPORTED = 0x001000 

46 MULTIPLE_BOND_SUPPORTED = 0x002000 

47 MULTIPLE_SESSIONS_SUPPORTED = 0x004000 

48 CGM_TREND_INFORMATION_SUPPORTED = 0x008000 

49 CGM_QUALITY_SUPPORTED = 0x010000 

50 

51 

52class CGMType(IntEnum): 

53 """CGM sample type (lower nibble).""" 

54 

55 CAPILLARY_WHOLE_BLOOD = 0x1 

56 CAPILLARY_PLASMA = 0x2 

57 VENOUS_WHOLE_BLOOD = 0x3 

58 VENOUS_PLASMA = 0x4 

59 ARTERIAL_WHOLE_BLOOD = 0x5 

60 ARTERIAL_PLASMA = 0x6 

61 UNDETERMINED_WHOLE_BLOOD = 0x7 

62 UNDETERMINED_PLASMA = 0x8 

63 INTERSTITIAL_FLUID = 0x9 

64 CONTROL_SOLUTION = 0xA 

65 

66 

67class CGMSampleLocation(IntEnum): 

68 """CGM sample location (upper nibble).""" 

69 

70 FINGER = 0x1 

71 ALTERNATE_SITE_TEST = 0x2 

72 EARLOBE = 0x3 

73 CONTROL_SOLUTION = 0x4 

74 SUBCUTANEOUS_TISSUE = 0x5 

75 NOT_AVAILABLE = 0xF 

76 

77 

78class CGMFeatureData(msgspec.Struct, frozen=True, kw_only=True): 

79 """Parsed data from CGM Feature characteristic. 

80 

81 Attributes: 

82 features: 24-bit CGM feature flags. 

83 cgm_type: CGM sample type. 

84 sample_location: CGM sample location. 

85 e2e_crc: E2E-CRC value. 

86 

87 """ 

88 

89 features: CGMFeatureFlags 

90 cgm_type: CGMType 

91 sample_location: CGMSampleLocation 

92 e2e_crc: int 

93 

94 

95class CGMFeatureCharacteristic(BaseCharacteristic[CGMFeatureData]): 

96 """CGM Feature characteristic (0x2AA8). 

97 

98 Reports the supported features, sample type, sample location, and 

99 E2E-CRC for a CGM sensor. Fixed 6-byte structure. 

100 """ 

101 

102 expected_type = CGMFeatureData 

103 expected_length: int = 6 

104 

105 def _decode_value( 

106 self, 

107 data: bytearray, 

108 ctx: CharacteristicContext | None = None, 

109 *, 

110 validate: bool = True, 

111 ) -> CGMFeatureData: 

112 """Parse CGM Feature from raw BLE bytes. 

113 

114 Args: 

115 data: Raw bytearray from BLE characteristic (6 bytes). 

116 ctx: Optional context (unused). 

117 validate: Whether to validate ranges. 

118 

119 Returns: 

120 CGMFeatureData with parsed feature flags, type, and location. 

121 

122 """ 

123 # 24-bit feature flags (little-endian, 3 bytes) 

124 features_raw = data[0] | (data[1] << 8) | (data[2] << 16) 

125 features = CGMFeatureFlags(features_raw) 

126 

127 # Type-Sample Location: lower nibble = type, upper nibble = location 

128 type_location_byte = data[3] 

129 cgm_type = CGMType(type_location_byte & _NIBBLE_MASK) 

130 sample_location = CGMSampleLocation((type_location_byte >> _NIBBLE_SHIFT) & _NIBBLE_MASK) 

131 

132 e2e_crc = DataParser.parse_int16(data, 4, signed=False) 

133 

134 return CGMFeatureData( 

135 features=features, 

136 cgm_type=cgm_type, 

137 sample_location=sample_location, 

138 e2e_crc=e2e_crc, 

139 ) 

140 

141 def _encode_value(self, data: CGMFeatureData) -> bytearray: 

142 """Encode CGMFeatureData back to BLE bytes. 

143 

144 Args: 

145 data: CGMFeatureData instance. 

146 

147 Returns: 

148 Encoded bytearray (6 bytes). 

149 

150 """ 

151 features_int = int(data.features) 

152 result = bytearray( 

153 [ 

154 features_int & 0xFF, 

155 (features_int >> 8) & 0xFF, 

156 (features_int >> 16) & 0xFF, 

157 ] 

158 ) 

159 

160 type_location = (data.cgm_type & _NIBBLE_MASK) | ((data.sample_location & _NIBBLE_MASK) << _NIBBLE_SHIFT) 

161 result.append(type_location) 

162 

163 result.extend(DataParser.encode_int16(data.e2e_crc, signed=False)) 

164 return result