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

44 statements  

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

1"""Blood Pressure Record characteristic implementation. 

2 

3Implements the Blood Pressure Record characteristic (0x2B36). This is a 

4segmented record container that wraps another characteristic value identified 

5by a 16-bit UUID. 

6 

7Structure (from GSS YAML): 

8 Segmentation Header (1 byte): 

9 Bit 0: First Segment 

10 Bit 1: Last Segment 

11 Bits 2-7: Rolling Segment Counter (0-63) 

12 Sequence Number (uint16) 

13 UUID (uint16) -- identifies the recorded characteristic 

14 Recorded Characteristic (variable) -- raw bytes of the inner characteristic 

15 E2E-CRC (uint16, optional) -- presence defined by service 

16 

17References: 

18 Bluetooth SIG Blood Pressure Service 1.1 

19 org.bluetooth.characteristic.blood_pressure_record (GSS YAML) 

20""" 

21 

22from __future__ import annotations 

23 

24import msgspec 

25 

26from bluetooth_sig.types.uuid import BluetoothUUID 

27 

28from ..context import CharacteristicContext 

29from .base import BaseCharacteristic 

30from .utils import DataParser 

31 

32_SEGMENT_COUNTER_SHIFT = 2 

33_SEGMENT_COUNTER_MASK = 0x3F # bits 2-7 = 6 bits 

34 

35 

36class BloodPressureRecordData(msgspec.Struct, frozen=True, kw_only=True): 

37 """Parsed data from Blood Pressure Record characteristic. 

38 

39 Attributes: 

40 first_segment: Whether this is the first segment of the record. 

41 last_segment: Whether this is the last segment of the record. 

42 segment_counter: Rolling segment counter (0-63). 

43 sequence_number: Sequence number identifying this record. 

44 uuid: 16-bit UUID of the recorded characteristic. 

45 recorded_data: Raw bytes of the recorded characteristic value. 

46 e2e_crc: End-to-end CRC value. None if absent. 

47 

48 """ 

49 

50 first_segment: bool 

51 last_segment: bool 

52 segment_counter: int 

53 sequence_number: int 

54 uuid: BluetoothUUID 

55 recorded_data: bytes 

56 e2e_crc: int | None = None 

57 

58 

59class BloodPressureRecordCharacteristic(BaseCharacteristic[BloodPressureRecordData]): 

60 """Blood Pressure Record characteristic (0x2B36). 

61 

62 A segmented record container that wraps another characteristic value. 

63 The inner characteristic is identified by the UUID field and its raw 

64 bytes are stored in ``recorded_data``. 

65 """ 

66 

67 expected_type = BloodPressureRecordData 

68 min_length: int = 5 # header(1) + sequence(2) + uuid(2) 

69 allow_variable_length: bool = True 

70 

71 def _decode_value( 

72 self, 

73 data: bytearray, 

74 ctx: CharacteristicContext | None = None, 

75 *, 

76 validate: bool = True, 

77 ) -> BloodPressureRecordData: 

78 """Parse Blood Pressure Record from raw BLE bytes. 

79 

80 Args: 

81 data: Raw bytearray from BLE characteristic. 

82 ctx: Optional context (unused). 

83 validate: Whether to validate ranges. 

84 

85 Returns: 

86 BloodPressureRecordData with segmentation info and raw recorded data. 

87 

88 """ 

89 header = data[0] 

90 first_segment = bool(header & 0x01) 

91 last_segment = bool(header & 0x02) 

92 segment_counter = (header >> _SEGMENT_COUNTER_SHIFT) & _SEGMENT_COUNTER_MASK 

93 

94 sequence_number = DataParser.parse_int16(data, 1, signed=False) 

95 uuid = BluetoothUUID(DataParser.parse_int16(data, 3, signed=False)) 

96 offset = 5 

97 

98 # Determine if E2E-CRC is present. 

99 # The recorded data occupies everything between offset and the end, 

100 # except for the optional 2-byte CRC at the very end. 

101 # We cannot deterministically know whether CRC is present without 

102 # service-level context, so we expose all remaining bytes as recorded 

103 # data. When the caller knows CRC is enabled, they can split the 

104 # last 2 bytes themselves. 

105 recorded_data = bytes(data[offset:]) 

106 

107 return BloodPressureRecordData( 

108 first_segment=first_segment, 

109 last_segment=last_segment, 

110 segment_counter=segment_counter, 

111 sequence_number=sequence_number, 

112 uuid=uuid, 

113 recorded_data=recorded_data, 

114 ) 

115 

116 def _encode_value(self, data: BloodPressureRecordData) -> bytearray: 

117 """Encode BloodPressureRecordData back to BLE bytes. 

118 

119 Args: 

120 data: BloodPressureRecordData instance. 

121 

122 Returns: 

123 Encoded bytearray matching the BLE wire format. 

124 

125 """ 

126 header = 0 

127 if data.first_segment: 

128 header |= 0x01 

129 if data.last_segment: 

130 header |= 0x02 

131 header |= (data.segment_counter & _SEGMENT_COUNTER_MASK) << _SEGMENT_COUNTER_SHIFT 

132 

133 result = bytearray([header]) 

134 result.extend(DataParser.encode_int16(data.sequence_number, signed=False)) 

135 result.extend(DataParser.encode_int16(int(data.uuid.short_form, 16), signed=False)) 

136 result.extend(data.recorded_data) 

137 

138 if data.e2e_crc is not None: 

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

140 

141 return result