Coverage for src / bluetooth_sig / gatt / characteristics / cgm_specific_ops_control_point.py: 98%
60 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-03 16:41 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-03 16:41 +0000
1"""CGM Specific Ops Control Point characteristic (0x2AAC).
3Control point for CGM-specific procedures: communication interval,
4calibration, alert levels, session control.
6References:
7 Bluetooth SIG Continuous Glucose Monitoring Service
8 org.bluetooth.characteristic.cgm_specific_ops_control_point (GSS YAML)
9"""
11from __future__ import annotations
13from enum import IntEnum
15import msgspec
17from ..context import CharacteristicContext
18from .base import BaseCharacteristic
19from .utils import DataParser
22class CGMSpecificOpsOpCode(IntEnum):
23 """CGM Specific Ops Control Point Op Codes."""
25 SET_CGM_COMMUNICATION_INTERVAL = 0x01
26 GET_CGM_COMMUNICATION_INTERVAL = 0x02
27 CGM_COMMUNICATION_INTERVAL_RESPONSE = 0x03
28 SET_GLUCOSE_CALIBRATION_VALUE = 0x04
29 GET_GLUCOSE_CALIBRATION_VALUE = 0x05
30 GLUCOSE_CALIBRATION_VALUE_RESPONSE = 0x06
31 SET_PATIENT_HIGH_ALERT_LEVEL = 0x07
32 GET_PATIENT_HIGH_ALERT_LEVEL = 0x08
33 PATIENT_HIGH_ALERT_LEVEL_RESPONSE = 0x09
34 SET_PATIENT_LOW_ALERT_LEVEL = 0x0A
35 GET_PATIENT_LOW_ALERT_LEVEL = 0x0B
36 PATIENT_LOW_ALERT_LEVEL_RESPONSE = 0x0C
37 SET_HYPO_ALERT_LEVEL = 0x0D
38 GET_HYPO_ALERT_LEVEL = 0x0E
39 HYPO_ALERT_LEVEL_RESPONSE = 0x0F
40 SET_HYPER_ALERT_LEVEL = 0x10
41 GET_HYPER_ALERT_LEVEL = 0x11
42 HYPER_ALERT_LEVEL_RESPONSE = 0x12
43 SET_RATE_OF_DECREASE_ALERT_LEVEL = 0x13
44 GET_RATE_OF_DECREASE_ALERT_LEVEL = 0x14
45 RATE_OF_DECREASE_ALERT_LEVEL_RESPONSE = 0x15
46 SET_RATE_OF_INCREASE_ALERT_LEVEL = 0x16
47 GET_RATE_OF_INCREASE_ALERT_LEVEL = 0x17
48 RATE_OF_INCREASE_ALERT_LEVEL_RESPONSE = 0x18
49 RESET_DEVICE_SPECIFIC_ALERT = 0x19
50 START_THE_SESSION = 0x1A
51 STOP_THE_SESSION = 0x1B
52 RESPONSE_CODE = 0x1C
55class CGMSpecificOpsResponseCode(IntEnum):
56 """CGM Specific Ops response codes."""
58 SUCCESS = 0x01
59 OP_CODE_NOT_SUPPORTED = 0x02
60 INVALID_OPERAND = 0x03
61 PROCEDURE_NOT_COMPLETED = 0x04
62 PARAMETER_OUT_OF_RANGE = 0x05
65class CGMSpecificOpsControlPointData(msgspec.Struct, frozen=True, kw_only=True):
66 """Parsed data from CGM Specific Ops Control Point.
68 Attributes:
69 opcode: The operation code.
70 operand: Raw operand bytes (variable per opcode). Empty if none.
71 e2e_crc: Optional E2E-CRC (present if CGM Feature indicates support).
73 """
75 opcode: CGMSpecificOpsOpCode
76 operand: bytes = b""
77 e2e_crc: int | None = None
80class CGMSpecificOpsControlPointCharacteristic(BaseCharacteristic[CGMSpecificOpsControlPointData]):
81 """CGM Specific Ops Control Point characteristic (0x2AAC).
83 org.bluetooth.characteristic.cgm_specific_ops_control_point
85 Used to enable procedures related to a continuous glucose monitor.
86 ROLE: CONTROL
87 """
89 _manual_role = None # Let classifier infer CONTROL from name
90 min_length = 1
91 allow_variable_length = True
93 def _decode_value(
94 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
95 ) -> CGMSpecificOpsControlPointData:
96 """Parse CGM Specific Ops Control Point data.
98 Format: OpCode (uint8) + Operand (variable) + optional E2E-CRC (uint16).
100 The E2E-CRC, if present, occupies the last 2 bytes. Without external
101 knowledge of CRC support we cannot distinguish a 2-byte operand tail
102 from a CRC, so we always treat the last 2 bytes as CRC when >= 3 bytes
103 total and the opcode is a response type or the data length suggests it.
104 For simplicity, CRC is *not* split out here — consumers should use
105 CGM Feature to decide whether CRC is present.
106 """
107 opcode = CGMSpecificOpsOpCode(DataParser.parse_int8(data, 0, signed=False))
108 operand = bytes(data[1:])
110 return CGMSpecificOpsControlPointData(
111 opcode=opcode,
112 operand=operand,
113 )
115 def _encode_value(self, data: CGMSpecificOpsControlPointData) -> bytearray:
116 """Encode CGM Specific Ops Control Point data."""
117 result = bytearray()
118 result.extend(DataParser.encode_int8(int(data.opcode), signed=False))
119 result.extend(data.operand)
120 if data.e2e_crc is not None:
121 result.extend(DataParser.encode_int16(data.e2e_crc, signed=False))
122 return result