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
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""CGM Status characteristic implementation.
3Implements the CGM Status characteristic (0x2AA9). Reports the current
4status of a CGM sensor.
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
11The 24-bit status uses the same bit definitions as CGM Measurement's
12Sensor Status Annunciation (Status + Cal/Temp + Warning).
14References:
15 Bluetooth SIG Continuous Glucose Monitoring Service
16 org.bluetooth.characteristic.cgm_status (GSS YAML)
17"""
19from __future__ import annotations
21from enum import IntFlag
23import msgspec
25from ..context import CharacteristicContext
26from .base import BaseCharacteristic
27from .utils import DataParser
30class CGMStatusFlags(IntFlag):
31 """CGM Status flags (24-bit).
33 Combined Status (bits 0-7), Cal/Temp (bits 8-15), and Warning (bits 16-23).
34 """
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
62class CGMStatusData(msgspec.Struct, frozen=True, kw_only=True):
63 """Parsed data from CGM Status characteristic.
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.
70 """
72 time_offset: int
73 status: CGMStatusFlags
74 e2e_crc: int | None = None
77class CGMStatusCharacteristic(BaseCharacteristic[CGMStatusData]):
78 """CGM Status characteristic (0x2AA9).
80 Reports current CGM sensor status with 24-bit status flags
81 and optional E2E-CRC.
82 """
84 expected_type = CGMStatusData
85 min_length: int = 5 # time_offset(2) + status(3)
86 allow_variable_length: bool = True # optional E2E-CRC
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.
97 Args:
98 data: Raw bytearray from BLE characteristic (5 or 7 bytes).
99 ctx: Optional context (unused).
100 validate: Whether to validate ranges.
102 Returns:
103 CGMStatusData with time offset and status flags.
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)
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)
115 return CGMStatusData(
116 time_offset=time_offset,
117 status=status,
118 e2e_crc=e2e_crc,
119 )
121 def _encode_value(self, data: CGMStatusData) -> bytearray:
122 """Encode CGMStatusData back to BLE bytes.
124 Args:
125 data: CGMStatusData instance.
127 Returns:
128 Encoded bytearray (5 or 7 bytes).
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