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

52 statements  

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

1"""CGM Status characteristic implementation. 

2 

3Implements the CGM Status characteristic (0x2AA9). Reports the current 

4status of a CGM sensor. 

5 

6Structure (from GSS YAML): 

7 Time Offset (uint16) -- minutes since session start 

8 CGM Status (3 bytes, boolean[24]) -- always 3 octets 

9 E2E-CRC (uint16, optional) -- present if E2E-CRC Supported 

10 

11The 24-bit status uses the same bit definitions as CGM Measurement's 

12Sensor Status Annunciation (Status + Cal/Temp + Warning). 

13 

14References: 

15 Bluetooth SIG Continuous Glucose Monitoring Service 

16 org.bluetooth.characteristic.cgm_status (GSS YAML) 

17""" 

18 

19from __future__ import annotations 

20 

21from enum import IntFlag 

22 

23import msgspec 

24 

25from ..context import CharacteristicContext 

26from .base import BaseCharacteristic 

27from .utils import DataParser 

28 

29 

30class CGMStatusFlags(IntFlag): 

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

32 

33 Combined Status (bits 0-7), Cal/Temp (bits 8-15), and Warning (bits 16-23). 

34 """ 

35 

36 # Status octet (bits 0-7) 

37 SESSION_STOPPED = 0x000001 

38 DEVICE_BATTERY_LOW = 0x000002 

39 SENSOR_TYPE_INCORRECT = 0x000004 

40 SENSOR_MALFUNCTION = 0x000008 

41 DEVICE_SPECIFIC_ALERT = 0x000010 

42 GENERAL_DEVICE_FAULT = 0x000020 

43 # Cal/Temp octet (bits 8-15) 

44 TIME_SYNC_REQUIRED = 0x000100 

45 CALIBRATION_NOT_ALLOWED = 0x000200 

46 CALIBRATION_RECOMMENDED = 0x000400 

47 CALIBRATION_REQUIRED = 0x000800 

48 SENSOR_TEMP_TOO_HIGH = 0x001000 

49 SENSOR_TEMP_TOO_LOW = 0x002000 

50 CALIBRATION_PENDING = 0x004000 

51 # Warning octet (bits 16-23) 

52 RESULT_LOWER_THAN_PATIENT_LOW = 0x010000 

53 RESULT_HIGHER_THAN_PATIENT_HIGH = 0x020000 

54 RESULT_LOWER_THAN_HYPO = 0x040000 

55 RESULT_HIGHER_THAN_HYPER = 0x080000 

56 RATE_OF_DECREASE_EXCEEDED = 0x100000 

57 RATE_OF_INCREASE_EXCEEDED = 0x200000 

58 RESULT_LOWER_THAN_DEVICE_CAN_PROCESS = 0x400000 

59 RESULT_HIGHER_THAN_DEVICE_CAN_PROCESS = 0x800000 

60 

61 

62class CGMStatusData(msgspec.Struct, frozen=True, kw_only=True): 

63 """Parsed data from CGM Status characteristic. 

64 

65 Attributes: 

66 time_offset: Minutes since session start. 

67 status: 24-bit combined status flags. 

68 e2e_crc: E2E-CRC value. None if absent. 

69 

70 """ 

71 

72 time_offset: int 

73 status: CGMStatusFlags 

74 e2e_crc: int | None = None 

75 

76 

77class CGMStatusCharacteristic(BaseCharacteristic[CGMStatusData]): 

78 """CGM Status characteristic (0x2AA9). 

79 

80 Reports current CGM sensor status with 24-bit status flags 

81 and optional E2E-CRC. 

82 """ 

83 

84 expected_type = CGMStatusData 

85 min_length: int = 5 # time_offset(2) + status(3) 

86 allow_variable_length: bool = True # optional E2E-CRC 

87 

88 def _decode_value( 

89 self, 

90 data: bytearray, 

91 ctx: CharacteristicContext | None = None, 

92 *, 

93 validate: bool = True, 

94 ) -> CGMStatusData: 

95 """Parse CGM Status from raw BLE bytes. 

96 

97 Args: 

98 data: Raw bytearray from BLE characteristic (5 or 7 bytes). 

99 ctx: Optional context (unused). 

100 validate: Whether to validate ranges. 

101 

102 Returns: 

103 CGMStatusData with time offset and status flags. 

104 

105 """ 

106 time_offset = DataParser.parse_int16(data, 0, signed=False) 

107 status_raw = data[2] | (data[3] << 8) | (data[4] << 16) 

108 status = CGMStatusFlags(status_raw) 

109 

110 _min_length_with_crc = 7 

111 e2e_crc: int | None = None 

112 if len(data) >= _min_length_with_crc: 

113 e2e_crc = DataParser.parse_int16(data, 5, signed=False) 

114 

115 return CGMStatusData( 

116 time_offset=time_offset, 

117 status=status, 

118 e2e_crc=e2e_crc, 

119 ) 

120 

121 def _encode_value(self, data: CGMStatusData) -> bytearray: 

122 """Encode CGMStatusData back to BLE bytes. 

123 

124 Args: 

125 data: CGMStatusData instance. 

126 

127 Returns: 

128 Encoded bytearray (5 or 7 bytes). 

129 

130 """ 

131 result = DataParser.encode_int16(data.time_offset, signed=False) 

132 status_int = int(data.status) 

133 result.extend( 

134 bytearray( 

135 [ 

136 status_int & 0xFF, 

137 (status_int >> 8) & 0xFF, 

138 (status_int >> 16) & 0xFF, 

139 ] 

140 ) 

141 ) 

142 if data.e2e_crc is not None: 

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

144 return result