Coverage for src / bluetooth_sig / gatt / characteristics / position_quality.py: 96%
91 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"""Position Quality characteristic implementation."""
3from __future__ import annotations
5from enum import IntFlag
7import msgspec
9from ...types.location import PositionStatus
10from ..context import CharacteristicContext
11from .base import BaseCharacteristic
12from .utils import DataParser
15class PositionQualityFlags(IntFlag):
16 """Position Quality flags as per Bluetooth SIG specification."""
18 NUMBER_OF_BEACONS_IN_SOLUTION_PRESENT = 0x0001
19 NUMBER_OF_BEACONS_IN_VIEW_PRESENT = 0x0002
20 TIME_TO_FIRST_FIX_PRESENT = 0x0004
21 EHPE_PRESENT = 0x0008
22 EVPE_PRESENT = 0x0010
23 HDOP_PRESENT = 0x0020
24 VDOP_PRESENT = 0x0040
27class PositionQualityData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods,too-many-instance-attributes
28 """Parsed data from Position Quality characteristic."""
30 flags: PositionQualityFlags
31 number_of_beacons_in_solution: int | None = None
32 number_of_beacons_in_view: int | None = None
33 time_to_first_fix: float | None = None
34 ehpe: float | None = None
35 evpe: float | None = None
36 hdop: float | None = None
37 vdop: float | None = None
38 position_status: PositionStatus | None = None
41class PositionQualityCharacteristic(BaseCharacteristic[PositionQualityData]):
42 """Position Quality characteristic.
44 Used to represent data related to the quality of a position measurement.
45 """
47 min_length = 2 # Flags(2) minimum
48 max_length = 16 # Flags(2) + NumberOfBeaconsInSolution(1) + NumberOfBeaconsInView(1) +
49 # TimeToFirstFix(2) + EHPE(4) + EVPE(4) + HDOP(1) + VDOP(1) maximum
50 allow_variable_length: bool = True # Variable optional fields
52 # Bit masks and shifts for status information in flags
53 POSITION_STATUS_MASK = 0x0180
54 POSITION_STATUS_SHIFT = 7
56 # Maximum valid enum value for PositionStatus
57 _MAX_POSITION_STATUS_VALUE = 3
59 def _decode_value(
60 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
61 ) -> PositionQualityData: # pylint: disable=too-many-locals
62 """Parse position quality data according to Bluetooth specification.
64 Format: Flags(2) + [Number of Beacons in Solution(1)] + [Number of Beacons in View(1)] +
65 [Time to First Fix(2)] + [EHPE(4)] + [EVPE(4)] + [HDOP(1)] + [VDOP(1)].
67 Args:
68 data: Raw bytearray from BLE characteristic
69 ctx: Optional context providing surrounding context (may be None)
70 validate: Whether to validate ranges (default True)
72 Returns:
73 PositionQualityData containing parsed position quality data
75 """
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 = (
81 PositionStatus(position_status_bits) if position_status_bits <= self._MAX_POSITION_STATUS_VALUE else None
82 )
84 # Parse optional fields
85 number_of_beacons_in_solution: int | None = None
86 number_of_beacons_in_view: int | None = None
87 time_to_first_fix: float | None = None
88 ehpe: float | None = None
89 evpe: float | None = None
90 hdop: float | None = None
91 vdop: float | None = None
92 offset = 2
94 if (flags & PositionQualityFlags.NUMBER_OF_BEACONS_IN_SOLUTION_PRESENT) and len(data) >= offset + 1:
95 number_of_beacons_in_solution = data[offset]
96 offset += 1
98 if (flags & PositionQualityFlags.NUMBER_OF_BEACONS_IN_VIEW_PRESENT) and len(data) >= offset + 1:
99 number_of_beacons_in_view = data[offset]
100 offset += 1
102 if (flags & PositionQualityFlags.TIME_TO_FIRST_FIX_PRESENT) and len(data) >= offset + 2:
103 # Unit is 1/10 seconds
104 time_to_first_fix = DataParser.parse_int16(data, offset, signed=False) / 10.0
105 offset += 2
107 if (flags & PositionQualityFlags.EHPE_PRESENT) and len(data) >= offset + 4:
108 # Unit is 1/100 m
109 ehpe = DataParser.parse_int32(data, offset, signed=False) / 100.0
110 offset += 4
112 if (flags & PositionQualityFlags.EVPE_PRESENT) and len(data) >= offset + 4:
113 # Unit is 1/100 m
114 evpe = DataParser.parse_int32(data, offset, signed=False) / 100.0
115 offset += 4
117 if (flags & PositionQualityFlags.HDOP_PRESENT) and len(data) >= offset + 1:
118 # Unit is 2*10^-1
119 hdop = data[offset] / 2.0
120 offset += 1
122 if (flags & PositionQualityFlags.VDOP_PRESENT) and len(data) >= offset + 1:
123 # Unit is 2*10^-1
124 vdop = data[offset] / 2.0
126 return PositionQualityData(
127 flags=flags,
128 number_of_beacons_in_solution=number_of_beacons_in_solution,
129 number_of_beacons_in_view=number_of_beacons_in_view,
130 time_to_first_fix=time_to_first_fix,
131 ehpe=ehpe,
132 evpe=evpe,
133 hdop=hdop,
134 vdop=vdop,
135 position_status=position_status,
136 )
138 def _encode_value(self, data: PositionQualityData) -> bytearray:
139 """Encode PositionQualityData back to bytes.
141 Args:
142 data: PositionQualityData instance to encode
144 Returns:
145 Encoded bytes representing the position quality data
147 """
148 result = bytearray()
150 flags = int(data.flags)
152 # Set status bits in flags
153 if data.position_status is not None:
154 flags |= data.position_status.value << self.POSITION_STATUS_SHIFT
156 result.extend(DataParser.encode_int16(flags, signed=False))
158 if data.number_of_beacons_in_solution is not None:
159 result.append(data.number_of_beacons_in_solution)
161 if data.number_of_beacons_in_view is not None:
162 result.append(data.number_of_beacons_in_view)
164 if data.time_to_first_fix is not None:
165 time_value = int(data.time_to_first_fix * 10)
166 result.extend(DataParser.encode_int16(time_value, signed=False))
168 if data.ehpe is not None:
169 ehpe_value = int(data.ehpe * 100)
170 result.extend(DataParser.encode_int32(ehpe_value, signed=False))
172 if data.evpe is not None:
173 evpe_value = int(data.evpe * 100)
174 result.extend(DataParser.encode_int32(evpe_value, signed=False))
176 if data.hdop is not None:
177 hdop_value = int(data.hdop * 2)
178 result.append(hdop_value)
180 if data.vdop is not None:
181 vdop_value = int(data.vdop * 2)
182 result.append(vdop_value)
184 return result