Coverage for src / bluetooth_sig / gatt / characteristics / blood_pressure_measurement.py: 93%
45 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 20:14 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 20:14 +0000
1"""Blood Pressure Measurement characteristic implementation."""
3from __future__ import annotations
5from enum import IntFlag
7import msgspec
9from bluetooth_sig.types.units import PressureUnit
11from ..context import CharacteristicContext
12from .blood_pressure_common import (
13 BLOOD_PRESSURE_MAX_KPA,
14 BLOOD_PRESSURE_MAX_MMHG,
15 BaseBloodPressureCharacteristic,
16 BloodPressureFlags,
17 BloodPressureOptionalFields,
18)
19from .utils import IEEE11073Parser
22class BloodPressureMeasurementStatus(IntFlag):
23 """Blood Pressure Measurement Status flags as per Bluetooth SIG specification."""
25 BODY_MOVEMENT_DETECTED = 0x0001
26 CUFF_TOO_LOOSE = 0x0002
27 IRREGULAR_PULSE_DETECTED = 0x0004
28 PULSE_RATE_OUT_OF_RANGE = 0x0008
29 IMPROPER_MEASUREMENT_POSITION = 0x0010
32class BloodPressureData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods,too-many-instance-attributes
33 """Parsed data from Blood Pressure Measurement characteristic."""
35 systolic: float
36 diastolic: float
37 mean_arterial_pressure: float
38 unit: PressureUnit
39 optional_fields: BloodPressureOptionalFields = BloodPressureOptionalFields()
40 flags: BloodPressureFlags = BloodPressureFlags(0)
42 def __post_init__(self) -> None:
43 """Validate blood pressure data."""
44 if self.unit not in (PressureUnit.MMHG, PressureUnit.KPA):
45 raise ValueError(f"Blood pressure unit must be MMHG or KPA, got {self.unit}")
47 if self.unit == PressureUnit.MMHG:
48 valid_range = (0, BLOOD_PRESSURE_MAX_MMHG)
49 else: # kPa
50 valid_range = (0, BLOOD_PRESSURE_MAX_KPA)
52 for name, value in [
53 ("systolic", self.systolic),
54 ("diastolic", self.diastolic),
55 ("mean_arterial_pressure", self.mean_arterial_pressure),
56 ]:
57 if not valid_range[0] <= value <= valid_range[1]:
58 raise ValueError(
59 f"{name} pressure {value} {self.unit.value} is outside valid range "
60 f"({valid_range[0]}-{valid_range[1]} {self.unit.value})"
61 )
64class BloodPressureMeasurementCharacteristic(BaseBloodPressureCharacteristic):
65 """Blood Pressure Measurement characteristic (0x2A35).
67 Used to transmit blood pressure measurements with systolic,
68 diastolic and mean arterial pressure.
70 SIG Specification Pattern:
71 This characteristic can use Blood Pressure Feature (0x2A49) to interpret
72 which status flags are supported by the device.
73 """
75 _is_base_class = False # This is a concrete characteristic class
76 min_length: int = 7 # Flags(1) + Systolic(2) + Diastolic(2) + MAP(2)
77 allow_variable_length: bool = True # Optional timestamp, pulse rate, user ID, status
79 def _decode_value(self, data: bytearray, ctx: CharacteristicContext | None = None) -> BloodPressureData: # pylint: disable=too-many-locals
80 """Parse blood pressure measurement data according to Bluetooth specification.
82 Format: Flags(1) + Systolic(2) + Diastolic(2) + MAP(2) + [Timestamp(7)] +
83 [Pulse Rate(2)] + [User ID(1)] + [Measurement Status(2)].
84 All pressure values are IEEE-11073 16-bit SFLOAT.
86 Args:
87 data: Raw bytearray from BLE characteristic
88 ctx: Optional context providing access to Blood Pressure Feature characteristic
89 for validating which measurement status flags are supported
91 Returns:
92 BloodPressureData containing parsed blood pressure data with metadata
94 SIG Pattern:
95 When context is available, can validate that measurement status flags are
96 within the device's supported features as indicated by Blood Pressure Feature.
98 """
99 if len(data) < 7:
100 raise ValueError("Blood Pressure Measurement data must be at least 7 bytes")
102 flags = self._parse_blood_pressure_flags(data)
104 # Parse required fields
105 systolic = IEEE11073Parser.parse_sfloat(data, 1)
106 diastolic = IEEE11073Parser.parse_sfloat(data, 3)
107 mean_arterial_pressure = IEEE11073Parser.parse_sfloat(data, 5)
108 unit = self._parse_blood_pressure_unit(flags)
110 # Parse optional fields
111 timestamp, pulse_rate, user_id, measurement_status = self._parse_optional_fields(data, flags)
113 # Create immutable struct with all values
114 return BloodPressureData( # pylint: disable=duplicate-code # Similar structure in intermediate_cuff_pressure (same optional fields by spec)
115 systolic=systolic,
116 diastolic=diastolic,
117 mean_arterial_pressure=mean_arterial_pressure,
118 unit=unit,
119 optional_fields=BloodPressureOptionalFields(
120 timestamp=timestamp,
121 pulse_rate=pulse_rate,
122 user_id=user_id,
123 measurement_status=measurement_status,
124 ),
125 flags=flags,
126 )
128 def _encode_value(self, data: BloodPressureData) -> bytearray:
129 """Encode BloodPressureData back to bytes.
131 Args:
132 data: BloodPressureData instance to encode
134 Returns:
135 Encoded bytes representing the blood pressure measurement
137 """
138 return self._encode_blood_pressure_base(
139 data,
140 data.optional_fields,
141 [data.systolic, data.diastolic, data.mean_arterial_pressure],
142 )