Coverage for src / bluetooth_sig / gatt / characteristics / elapsed_time.py: 100%

65 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-03 16:41 +0000

1"""Elapsed Time characteristic implementation. 

2 

3Implements the Elapsed Time characteristic (0x2BF2). 

4 

5Structure (from GSS YAML - org.bluetooth.characteristic.elapsed_time): 

6 Flags (uint8, 1 byte) -- interpretation flags 

7 Time Value (uint48, 6 bytes) -- counter in time-resolution units 

8 Time Sync Source Type (uint8, 1 byte) -- sync source enum 

9 TZ/DST Offset (sint8, 1 byte) -- combined offset in 15-minute units 

10 

11Note: The GSS YAML identifier is ``elapsed_time`` but the UUID registry 

12identifier is ``current_elapsed_time`` (0x2BF2). File is named to match 

13the UUID registry for auto-discovery. 

14 

15Flag bits: 

16 0: Tick counter (0=time of day, 1=relative counter) 

17 1: UTC (0=local time, 1=UTC) — meaningless for tick counter 

18 2-3: Time resolution (00=1s, 01=100ms, 10=1ms, 11=100µs) 

19 4: TZ/DST offset used (0=not used, 1=used) 

20 5: Current timeline (0=not current, 1=current) 

21 6-7: Reserved 

22 

23References: 

24 Bluetooth SIG Generic Sensor Service 

25 org.bluetooth.characteristic.elapsed_time (GSS YAML) 

26""" 

27 

28from __future__ import annotations 

29 

30from enum import IntEnum, IntFlag 

31 

32import msgspec 

33 

34from ..context import CharacteristicContext 

35from .base import BaseCharacteristic 

36from .reference_time_information import TimeSource 

37from .utils import DataParser 

38 

39_TIME_RESOLUTION_MASK = 0x0C 

40_TIME_RESOLUTION_SHIFT = 2 

41_READ_LENGTH = 11 # 9 (Elapsed Time struct) + 1 (Clock Status) + 1 (Clock Capabilities) 

42 

43 

44class ElapsedTimeFlags(IntFlag): 

45 """Flags for the Elapsed Time characteristic.""" 

46 

47 TICK_COUNTER = 1 << 0 

48 UTC = 1 << 1 

49 TZ_DST_USED = 1 << 4 

50 CURRENT_TIMELINE = 1 << 5 

51 

52 

53class TimeResolution(IntEnum): 

54 """Time resolution values (bits 2-3 of flags).""" 

55 

56 ONE_SECOND = 0 

57 HUNDRED_MILLISECONDS = 1 

58 ONE_MILLISECOND = 2 

59 HUNDRED_MICROSECONDS = 3 

60 

61 

62class ElapsedTimeData(msgspec.Struct, frozen=True, kw_only=True): 

63 """Parsed data from Current Elapsed Time characteristic. 

64 

65 Attributes: 

66 flags: Interpretation flags. 

67 time_value: Counter value in the resolution defined by flags. 

68 time_resolution: Resolution of the time value. 

69 is_tick_counter: True if time_value is a relative counter. 

70 is_utc: True if time_value reports UTC (only meaningful if not tick counter). 

71 tz_dst_used: True if tz_dst_offset is meaningful. 

72 is_current_timeline: True if time stamp is from the current timeline. 

73 sync_source_type: Time synchronisation source type. 

74 tz_dst_offset: Combined TZ/DST offset from UTC in 15-minute units. 

75 clock_needs_set: Server requests client to set the clock (Clock Status bit 0). 

76 clock_applies_dst: Clock autonomously updates DST offset (Clock Capabilities bit 0). 

77 clock_manages_tz: Clock autonomously updates TZ offset (Clock Capabilities bit 1). 

78 

79 """ 

80 

81 flags: ElapsedTimeFlags 

82 time_value: int 

83 time_resolution: TimeResolution 

84 is_tick_counter: bool 

85 is_utc: bool 

86 tz_dst_used: bool 

87 is_current_timeline: bool 

88 sync_source_type: TimeSource 

89 tz_dst_offset: int 

90 clock_needs_set: bool = False 

91 clock_applies_dst: bool = False 

92 clock_manages_tz: bool = False 

93 

94 

95class ElapsedTimeCharacteristic(BaseCharacteristic[ElapsedTimeData]): 

96 """Elapsed Time characteristic (0x2BF2). 

97 

98 Reports the current time of a clock or tick counter. 

99 

100 Read/indicate format: 11 bytes (9-byte Elapsed Time struct + Clock Status 

101 + Clock Capabilities). Write format: 9 bytes (Elapsed Time struct only). 

102 """ 

103 

104 expected_type = ElapsedTimeData 

105 min_length: int = 9 

106 max_length: int = 11 

107 

108 def _decode_value( 

109 self, 

110 data: bytearray, 

111 ctx: CharacteristicContext | None = None, 

112 *, 

113 validate: bool = True, 

114 ) -> ElapsedTimeData: 

115 """Parse Current Elapsed Time from raw BLE bytes. 

116 

117 Args: 

118 data: Raw bytearray (9 bytes for write echo, 11 bytes for read/indicate). 

119 ctx: Optional context (unused). 

120 validate: Whether to validate ranges. 

121 

122 Returns: 

123 ElapsedTimeData with parsed time information. 

124 

125 """ 

126 flags_raw = data[0] 

127 flags = ElapsedTimeFlags(flags_raw & 0x33) # Mask out resolution bits + reserved 

128 

129 time_resolution = TimeResolution((flags_raw & _TIME_RESOLUTION_MASK) >> _TIME_RESOLUTION_SHIFT) 

130 

131 time_value = DataParser.parse_int48(data, 1, signed=False) 

132 sync_source_type = TimeSource(data[7]) 

133 tz_dst_offset = DataParser.parse_int8(data, 8, signed=True) 

134 

135 clock_needs_set = False 

136 clock_applies_dst = False 

137 clock_manages_tz = False 

138 if len(data) >= _READ_LENGTH: 

139 clock_status = data[9] 

140 clock_needs_set = bool(clock_status & 0x01) 

141 clock_caps = data[10] 

142 clock_applies_dst = bool(clock_caps & 0x01) 

143 clock_manages_tz = bool(clock_caps & 0x02) 

144 

145 return ElapsedTimeData( 

146 flags=flags, 

147 time_value=time_value, 

148 time_resolution=time_resolution, 

149 is_tick_counter=bool(flags & ElapsedTimeFlags.TICK_COUNTER), 

150 is_utc=bool(flags & ElapsedTimeFlags.UTC), 

151 tz_dst_used=bool(flags & ElapsedTimeFlags.TZ_DST_USED), 

152 is_current_timeline=bool(flags & ElapsedTimeFlags.CURRENT_TIMELINE), 

153 sync_source_type=sync_source_type, 

154 tz_dst_offset=tz_dst_offset, 

155 clock_needs_set=clock_needs_set, 

156 clock_applies_dst=clock_applies_dst, 

157 clock_manages_tz=clock_manages_tz, 

158 ) 

159 

160 def _encode_value(self, data: ElapsedTimeData) -> bytearray: 

161 """Encode ElapsedTimeData back to BLE bytes. 

162 

163 Produces the full 11-byte read/indicate format including Clock Status 

164 and Clock Capabilities. 

165 

166 Args: 

167 data: ElapsedTimeData instance. 

168 

169 Returns: 

170 Encoded bytearray (11 bytes). 

171 

172 """ 

173 flags_raw = int(data.flags) | (data.time_resolution << _TIME_RESOLUTION_SHIFT) 

174 result = bytearray([flags_raw]) 

175 result.extend(DataParser.encode_int48(data.time_value, signed=False)) 

176 result.append(int(data.sync_source_type)) 

177 result.extend(DataParser.encode_int8(data.tz_dst_offset, signed=True)) 

178 

179 clock_status = int(data.clock_needs_set) 

180 result.append(clock_status) 

181 

182 clock_caps = int(data.clock_applies_dst) | (int(data.clock_manages_tz) << 1) 

183 result.append(clock_caps) 

184 

185 return result