Coverage for src / bluetooth_sig / gatt / characteristics / position_quality.py: 95%
94 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"""Position Quality characteristic implementation."""
3from __future__ import annotations
5from enum import IntFlag
7import msgspec
9from ...types.gatt_enums import ValueType
10from ...types.location import PositionStatus
11from ..context import CharacteristicContext
12from .base import BaseCharacteristic
13from .utils import DataParser
16class PositionQualityFlags(IntFlag):
17 """Position Quality flags as per Bluetooth SIG specification."""
19 NUMBER_OF_BEACONS_IN_SOLUTION_PRESENT = 0x0001
20 NUMBER_OF_BEACONS_IN_VIEW_PRESENT = 0x0002
21 TIME_TO_FIRST_FIX_PRESENT = 0x0004
22 EHPE_PRESENT = 0x0008
23 EVPE_PRESENT = 0x0010
24 HDOP_PRESENT = 0x0020
25 VDOP_PRESENT = 0x0040
28class PositionQualityData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods,too-many-instance-attributes
29 """Parsed data from Position Quality characteristic."""
31 flags: PositionQualityFlags
32 number_of_beacons_in_solution: int | None = None
33 number_of_beacons_in_view: int | None = None
34 time_to_first_fix: float | None = None
35 ehpe: float | None = None
36 evpe: float | None = None
37 hdop: float | None = None
38 vdop: float | None = None
39 position_status: PositionStatus | None = None
42class PositionQualityCharacteristic(BaseCharacteristic[PositionQualityData]):
43 """Position Quality characteristic.
45 Used to represent data related to the quality of a position measurement.
46 """
48 _manual_value_type: ValueType | str | None = ValueType.DICT # Override since decode_value returns dataclass
50 min_length = 2 # Flags(2) minimum
51 max_length = 16 # Flags(2) + NumberOfBeaconsInSolution(1) + NumberOfBeaconsInView(1) +
52 # TimeToFirstFix(2) + EHPE(4) + EVPE(4) + HDOP(1) + VDOP(1) maximum
53 allow_variable_length: bool = True # Variable optional fields
55 # Bit masks and shifts for status information in flags
56 POSITION_STATUS_MASK = 0x0180
57 POSITION_STATUS_SHIFT = 7
59 def _decode_value(self, data: bytearray, ctx: CharacteristicContext | None = None) -> PositionQualityData: # pylint: disable=too-many-locals
60 """Parse position quality data according to Bluetooth specification.
62 Format: Flags(2) + [Number of Beacons in Solution(1)] + [Number of Beacons in View(1)] +
63 [Time to First Fix(2)] + [EHPE(4)] + [EVPE(4)] + [HDOP(1)] + [VDOP(1)].
65 Args:
66 data: Raw bytearray from BLE characteristic
67 ctx: Optional context providing surrounding context (may be None)
69 Returns:
70 PositionQualityData containing parsed position quality data
72 """
73 if len(data) < 2:
74 raise ValueError("Position Quality data must be at least 2 bytes")
76 flags = PositionQualityFlags(DataParser.parse_int16(data, 0, signed=False))
78 # Extract status information from flags
79 position_status_bits = (flags & self.POSITION_STATUS_MASK) >> self.POSITION_STATUS_SHIFT
80 position_status = PositionStatus(position_status_bits) if position_status_bits <= 3 else None
82 # Parse optional fields
83 number_of_beacons_in_solution: int | None = None
84 number_of_beacons_in_view: int | None = None
85 time_to_first_fix: float | None = None
86 ehpe: float | None = None
87 evpe: float | None = None
88 hdop: float | None = None
89 vdop: float | None = None
90 offset = 2
92 if (flags & PositionQualityFlags.NUMBER_OF_BEACONS_IN_SOLUTION_PRESENT) and len(data) >= offset + 1:
93 number_of_beacons_in_solution = data[offset]
94 offset += 1
96 if (flags & PositionQualityFlags.NUMBER_OF_BEACONS_IN_VIEW_PRESENT) and len(data) >= offset + 1:
97 number_of_beacons_in_view = data[offset]
98 offset += 1
100 if (flags & PositionQualityFlags.TIME_TO_FIRST_FIX_PRESENT) and len(data) >= offset + 2:
101 # Unit is 1/10 seconds
102 time_to_first_fix = DataParser.parse_int16(data, offset, signed=False) / 10.0
103 offset += 2
105 if (flags & PositionQualityFlags.EHPE_PRESENT) and len(data) >= offset + 4:
106 # Unit is 1/100 m
107 ehpe = DataParser.parse_int32(data, offset, signed=False) / 100.0
108 offset += 4
110 if (flags & PositionQualityFlags.EVPE_PRESENT) and len(data) >= offset + 4:
111 # Unit is 1/100 m
112 evpe = DataParser.parse_int32(data, offset, signed=False) / 100.0
113 offset += 4
115 if (flags & PositionQualityFlags.HDOP_PRESENT) and len(data) >= offset + 1:
116 # Unit is 2*10^-1
117 hdop = data[offset] / 2.0
118 offset += 1
120 if (flags & PositionQualityFlags.VDOP_PRESENT) and len(data) >= offset + 1:
121 # Unit is 2*10^-1
122 vdop = data[offset] / 2.0
124 return PositionQualityData(
125 flags=flags,
126 number_of_beacons_in_solution=number_of_beacons_in_solution,
127 number_of_beacons_in_view=number_of_beacons_in_view,
128 time_to_first_fix=time_to_first_fix,
129 ehpe=ehpe,
130 evpe=evpe,
131 hdop=hdop,
132 vdop=vdop,
133 position_status=position_status,
134 )
136 def _encode_value(self, data: PositionQualityData) -> bytearray:
137 """Encode PositionQualityData back to bytes.
139 Args:
140 data: PositionQualityData instance to encode
142 Returns:
143 Encoded bytes representing the position quality data
145 """
146 result = bytearray()
148 flags = int(data.flags)
150 # Set status bits in flags
151 if data.position_status is not None:
152 flags |= data.position_status.value << self.POSITION_STATUS_SHIFT
154 result.extend(DataParser.encode_int16(flags, signed=False))
156 if data.number_of_beacons_in_solution is not None:
157 result.append(data.number_of_beacons_in_solution)
159 if data.number_of_beacons_in_view is not None:
160 result.append(data.number_of_beacons_in_view)
162 if data.time_to_first_fix is not None:
163 time_value = int(data.time_to_first_fix * 10)
164 result.extend(DataParser.encode_int16(time_value, signed=False))
166 if data.ehpe is not None:
167 ehpe_value = int(data.ehpe * 100)
168 result.extend(DataParser.encode_int32(ehpe_value, signed=False))
170 if data.evpe is not None:
171 evpe_value = int(data.evpe * 100)
172 result.extend(DataParser.encode_int32(evpe_value, signed=False))
174 if data.hdop is not None:
175 hdop_value = int(data.hdop * 2)
176 result.append(hdop_value)
178 if data.vdop is not None:
179 vdop_value = int(data.vdop * 2)
180 result.append(vdop_value)
182 return result