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
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""Blood Pressure Record characteristic implementation.
3Implements the Blood Pressure Record characteristic (0x2B36). This is a
4segmented record container that wraps another characteristic value identified
5by a 16-bit UUID.
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
17References:
18 Bluetooth SIG Blood Pressure Service 1.1
19 org.bluetooth.characteristic.blood_pressure_record (GSS YAML)
20"""
22from __future__ import annotations
24import msgspec
26from bluetooth_sig.types.uuid import BluetoothUUID
28from ..context import CharacteristicContext
29from .base import BaseCharacteristic
30from .utils import DataParser
32_SEGMENT_COUNTER_SHIFT = 2
33_SEGMENT_COUNTER_MASK = 0x3F # bits 2-7 = 6 bits
36class BloodPressureRecordData(msgspec.Struct, frozen=True, kw_only=True):
37 """Parsed data from Blood Pressure Record characteristic.
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.
48 """
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
59class BloodPressureRecordCharacteristic(BaseCharacteristic[BloodPressureRecordData]):
60 """Blood Pressure Record characteristic (0x2B36).
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 """
67 expected_type = BloodPressureRecordData
68 min_length: int = 5 # header(1) + sequence(2) + uuid(2)
69 allow_variable_length: bool = True
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.
80 Args:
81 data: Raw bytearray from BLE characteristic.
82 ctx: Optional context (unused).
83 validate: Whether to validate ranges.
85 Returns:
86 BloodPressureRecordData with segmentation info and raw recorded data.
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
94 sequence_number = DataParser.parse_int16(data, 1, signed=False)
95 uuid = BluetoothUUID(DataParser.parse_int16(data, 3, signed=False))
96 offset = 5
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:])
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 )
116 def _encode_value(self, data: BloodPressureRecordData) -> bytearray:
117 """Encode BloodPressureRecordData back to BLE bytes.
119 Args:
120 data: BloodPressureRecordData instance.
122 Returns:
123 Encoded bytearray matching the BLE wire format.
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
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)
138 if data.e2e_crc is not None:
139 result.extend(DataParser.encode_int16(data.e2e_crc, signed=False))
141 return result