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
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""CGM Feature characteristic implementation.
3Implements the CGM Feature characteristic (0x2AA8). Fixed-size structure of
46 bytes: 24-bit feature flags + packed nibble type/location + 16-bit E2E-CRC.
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
11References:
12 Bluetooth SIG Continuous Glucose Monitoring Service
13 org.bluetooth.characteristic.cgm_feature (GSS YAML)
14"""
16from __future__ import annotations
18from enum import IntEnum, IntFlag
20import msgspec
22from ..context import CharacteristicContext
23from .base import BaseCharacteristic
24from .utils import DataParser
26_NIBBLE_MASK = 0x0F
27_NIBBLE_SHIFT = 4
30class CGMFeatureFlags(IntFlag):
31 """CGM Feature flags (24-bit)."""
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
52class CGMType(IntEnum):
53 """CGM sample type (lower nibble)."""
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
67class CGMSampleLocation(IntEnum):
68 """CGM sample location (upper nibble)."""
70 FINGER = 0x1
71 ALTERNATE_SITE_TEST = 0x2
72 EARLOBE = 0x3
73 CONTROL_SOLUTION = 0x4
74 SUBCUTANEOUS_TISSUE = 0x5
75 NOT_AVAILABLE = 0xF
78class CGMFeatureData(msgspec.Struct, frozen=True, kw_only=True):
79 """Parsed data from CGM Feature characteristic.
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.
87 """
89 features: CGMFeatureFlags
90 cgm_type: CGMType
91 sample_location: CGMSampleLocation
92 e2e_crc: int
95class CGMFeatureCharacteristic(BaseCharacteristic[CGMFeatureData]):
96 """CGM Feature characteristic (0x2AA8).
98 Reports the supported features, sample type, sample location, and
99 E2E-CRC for a CGM sensor. Fixed 6-byte structure.
100 """
102 expected_type = CGMFeatureData
103 expected_length: int = 6
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.
114 Args:
115 data: Raw bytearray from BLE characteristic (6 bytes).
116 ctx: Optional context (unused).
117 validate: Whether to validate ranges.
119 Returns:
120 CGMFeatureData with parsed feature flags, type, and location.
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)
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)
132 e2e_crc = DataParser.parse_int16(data, 4, signed=False)
134 return CGMFeatureData(
135 features=features,
136 cgm_type=cgm_type,
137 sample_location=sample_location,
138 e2e_crc=e2e_crc,
139 )
141 def _encode_value(self, data: CGMFeatureData) -> bytearray:
142 """Encode CGMFeatureData back to BLE bytes.
144 Args:
145 data: CGMFeatureData instance.
147 Returns:
148 Encoded bytearray (6 bytes).
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 )
160 type_location = (data.cgm_type & _NIBBLE_MASK) | ((data.sample_location & _NIBBLE_MASK) << _NIBBLE_SHIFT)
161 result.append(type_location)
163 result.extend(DataParser.encode_int16(data.e2e_crc, signed=False))
164 return result