Coverage for src/bluetooth_sig/gatt/characteristics/intermediate_cuff_pressure.py: 42%

38 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-30 00:10 +0000

1"""Intermediate Cuff Pressure characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5import msgspec 

6 

7from bluetooth_sig.types.units import PressureUnit 

8 

9from ..context import CharacteristicContext 

10from .blood_pressure_common import ( 

11 BLOOD_PRESSURE_MAX_KPA, 

12 BLOOD_PRESSURE_MAX_MMHG, 

13 BaseBloodPressureCharacteristic, 

14 BloodPressureFlags, 

15 BloodPressureOptionalFields, 

16) 

17from .utils import IEEE11073Parser 

18 

19 

20class IntermediateCuffPressureData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods,too-many-instance-attributes 

21 """Parsed data from Intermediate Cuff Pressure characteristic.""" 

22 

23 current_cuff_pressure: float 

24 unit: PressureUnit 

25 optional_fields: BloodPressureOptionalFields = BloodPressureOptionalFields() 

26 flags: BloodPressureFlags = BloodPressureFlags(0) 

27 

28 def __post_init__(self) -> None: 

29 """Validate intermediate cuff pressure data.""" 

30 if self.unit not in (PressureUnit.MMHG, PressureUnit.KPA): 

31 raise ValueError(f"Cuff pressure unit must be MMHG or KPA, got {self.unit}") 

32 

33 if self.unit == PressureUnit.MMHG: 

34 valid_range = (0, BLOOD_PRESSURE_MAX_MMHG) 

35 else: # kPa 

36 valid_range = (0, BLOOD_PRESSURE_MAX_KPA) 

37 

38 if not valid_range[0] <= self.current_cuff_pressure <= valid_range[1]: 

39 raise ValueError( 

40 f"Current cuff pressure {self.current_cuff_pressure} {self.unit.value} is outside valid range " 

41 f"({valid_range[0]}-{valid_range[1]} {self.unit.value})" 

42 ) 

43 

44 

45class IntermediateCuffPressureCharacteristic(BaseBloodPressureCharacteristic): 

46 """Intermediate Cuff Pressure characteristic (0x2A36). 

47 

48 Used to transmit intermediate cuff pressure values during a blood 

49 pressure measurement process. 

50 

51 SIG Specification Pattern: 

52 This characteristic can use Blood Pressure Feature (0x2A49) to interpret 

53 which status flags are supported by the device. 

54 """ 

55 

56 _is_base_class = False # This is a concrete characteristic class 

57 

58 def decode_value(self, data: bytearray, ctx: CharacteristicContext | None = None) -> IntermediateCuffPressureData: # pylint: disable=too-many-locals 

59 """Parse intermediate cuff pressure data according to Bluetooth specification. 

60 

61 Format: Flags(1) + Current Cuff Pressure(2) + Unused(2) + Unused(2) + [Timestamp(7)] + 

62 [Pulse Rate(2)] + [User ID(1)] + [Measurement Status(2)]. 

63 All pressure values are IEEE-11073 16-bit SFLOAT. Unused fields are set to NaN. 

64 

65 Args: 

66 data: Raw bytearray from BLE characteristic 

67 ctx: Optional context providing access to Blood Pressure Feature characteristic 

68 for validating which measurement status flags are supported 

69 

70 Returns: 

71 IntermediateCuffPressureData containing parsed cuff pressure data with metadata 

72 

73 SIG Pattern: 

74 When context is available, can validate that measurement status flags are 

75 within the device's supported features as indicated by Blood Pressure Feature. 

76 

77 """ 

78 if len(data) < 7: 

79 raise ValueError("Intermediate Cuff Pressure data must be at least 7 bytes") 

80 

81 flags = self._parse_blood_pressure_flags(data) 

82 

83 # Parse required fields 

84 current_cuff_pressure = IEEE11073Parser.parse_sfloat(data, 1) 

85 unit = self._parse_blood_pressure_unit(flags) 

86 

87 # Skip unused fields (bytes 3-6, should be NaN but we don't validate here) 

88 

89 # Parse optional fields 

90 timestamp, pulse_rate, user_id, measurement_status = self._parse_optional_fields(data, flags) 

91 

92 # Create immutable struct with all values 

93 return IntermediateCuffPressureData( 

94 current_cuff_pressure=current_cuff_pressure, 

95 unit=unit, 

96 optional_fields=BloodPressureOptionalFields( 

97 timestamp=timestamp, 

98 pulse_rate=pulse_rate, 

99 user_id=user_id, 

100 measurement_status=measurement_status, 

101 ), 

102 flags=flags, 

103 ) 

104 

105 def encode_value(self, data: IntermediateCuffPressureData) -> bytearray: 

106 """Encode IntermediateCuffPressureData back to bytes. 

107 

108 Args: 

109 data: IntermediateCuffPressureData instance to encode 

110 

111 Returns: 

112 Encoded bytes representing the intermediate cuff pressure 

113 

114 """ 

115 result = bytearray() 

116 

117 flags = self._encode_blood_pressure_flags(data, data.optional_fields) 

118 result.append(flags) 

119 

120 result.extend(IEEE11073Parser.encode_sfloat(data.current_cuff_pressure)) 

121 # Add unused fields as NaN 

122 result.extend(IEEE11073Parser.encode_sfloat(float("nan"))) 

123 result.extend(IEEE11073Parser.encode_sfloat(float("nan"))) 

124 

125 self._encode_optional_fields(result, data.optional_fields) 

126 

127 return result