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

1"""Position Quality characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5from enum import IntFlag 

6 

7import msgspec 

8 

9from ...types.location import PositionStatus 

10from ..context import CharacteristicContext 

11from .base import BaseCharacteristic 

12from .utils import DataParser 

13 

14 

15class PositionQualityFlags(IntFlag): 

16 """Position Quality flags as per Bluetooth SIG specification.""" 

17 

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 

25 

26 

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.""" 

29 

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 

39 

40 

41class PositionQualityCharacteristic(BaseCharacteristic[PositionQualityData]): 

42 """Position Quality characteristic. 

43 

44 Used to represent data related to the quality of a position measurement. 

45 """ 

46 

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 

51 

52 # Bit masks and shifts for status information in flags 

53 POSITION_STATUS_MASK = 0x0180 

54 POSITION_STATUS_SHIFT = 7 

55 

56 # Maximum valid enum value for PositionStatus 

57 _MAX_POSITION_STATUS_VALUE = 3 

58 

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. 

63 

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)]. 

66 

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) 

71 

72 Returns: 

73 PositionQualityData containing parsed position quality data 

74 

75 """ 

76 flags = PositionQualityFlags(DataParser.parse_int16(data, 0, signed=False)) 

77 

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 ) 

83 

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 

93 

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 

97 

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 

101 

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 

106 

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 

111 

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 

116 

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 

121 

122 if (flags & PositionQualityFlags.VDOP_PRESENT) and len(data) >= offset + 1: 

123 # Unit is 2*10^-1 

124 vdop = data[offset] / 2.0 

125 

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 ) 

137 

138 def _encode_value(self, data: PositionQualityData) -> bytearray: 

139 """Encode PositionQualityData back to bytes. 

140 

141 Args: 

142 data: PositionQualityData instance to encode 

143 

144 Returns: 

145 Encoded bytes representing the position quality data 

146 

147 """ 

148 result = bytearray() 

149 

150 flags = int(data.flags) 

151 

152 # Set status bits in flags 

153 if data.position_status is not None: 

154 flags |= data.position_status.value << self.POSITION_STATUS_SHIFT 

155 

156 result.extend(DataParser.encode_int16(flags, signed=False)) 

157 

158 if data.number_of_beacons_in_solution is not None: 

159 result.append(data.number_of_beacons_in_solution) 

160 

161 if data.number_of_beacons_in_view is not None: 

162 result.append(data.number_of_beacons_in_view) 

163 

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)) 

167 

168 if data.ehpe is not None: 

169 ehpe_value = int(data.ehpe * 100) 

170 result.extend(DataParser.encode_int32(ehpe_value, signed=False)) 

171 

172 if data.evpe is not None: 

173 evpe_value = int(data.evpe * 100) 

174 result.extend(DataParser.encode_int32(evpe_value, signed=False)) 

175 

176 if data.hdop is not None: 

177 hdop_value = int(data.hdop * 2) 

178 result.append(hdop_value) 

179 

180 if data.vdop is not None: 

181 vdop_value = int(data.vdop * 2) 

182 result.append(vdop_value) 

183 

184 return result