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

47 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-18 11:17 +0000

1"""Current Elapsed Time characteristic implementation. 

2 

3Implements the Current 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 

42 

43class ElapsedTimeFlags(IntFlag): 

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

45 

46 TICK_COUNTER = 1 << 0 

47 UTC = 1 << 1 

48 TZ_DST_USED = 1 << 4 

49 CURRENT_TIMELINE = 1 << 5 

50 

51 

52class TimeResolution(IntEnum): 

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

54 

55 ONE_SECOND = 0 

56 HUNDRED_MILLISECONDS = 1 

57 ONE_MILLISECOND = 2 

58 HUNDRED_MICROSECONDS = 3 

59 

60 

61class CurrentElapsedTimeData(msgspec.Struct, frozen=True, kw_only=True): 

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

63 

64 Attributes: 

65 flags: Interpretation flags. 

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

67 time_resolution: Resolution of the time value. 

68 is_tick_counter: True if time_value is a relative counter. 

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

70 tz_dst_used: True if tz_dst_offset is meaningful. 

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

72 sync_source_type: Time synchronisation source type. 

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

74 

75 """ 

76 

77 flags: ElapsedTimeFlags 

78 time_value: int 

79 time_resolution: TimeResolution 

80 is_tick_counter: bool 

81 is_utc: bool 

82 tz_dst_used: bool 

83 is_current_timeline: bool 

84 sync_source_type: TimeSource 

85 tz_dst_offset: int 

86 

87 

88class CurrentElapsedTimeCharacteristic(BaseCharacteristic[CurrentElapsedTimeData]): 

89 """Current Elapsed Time characteristic (0x2BF2). 

90 

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

92 Fixed 9-byte structure. 

93 """ 

94 

95 expected_type = CurrentElapsedTimeData 

96 min_length: int = 9 

97 

98 def _decode_value( 

99 self, 

100 data: bytearray, 

101 ctx: CharacteristicContext | None = None, 

102 *, 

103 validate: bool = True, 

104 ) -> CurrentElapsedTimeData: 

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

106 

107 Args: 

108 data: Raw bytearray from BLE characteristic (9 bytes). 

109 ctx: Optional context (unused). 

110 validate: Whether to validate ranges. 

111 

112 Returns: 

113 CurrentElapsedTimeData with parsed time information. 

114 

115 """ 

116 flags_raw = data[0] 

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

118 

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

120 

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

122 sync_source_type = TimeSource(data[7]) 

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

124 

125 return CurrentElapsedTimeData( 

126 flags=flags, 

127 time_value=time_value, 

128 time_resolution=time_resolution, 

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

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

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

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

133 sync_source_type=sync_source_type, 

134 tz_dst_offset=tz_dst_offset, 

135 ) 

136 

137 def _encode_value(self, data: CurrentElapsedTimeData) -> bytearray: 

138 """Encode CurrentElapsedTimeData back to BLE bytes. 

139 

140 Args: 

141 data: CurrentElapsedTimeData instance. 

142 

143 Returns: 

144 Encoded bytearray (9 bytes). 

145 

146 """ 

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

148 result = bytearray([flags_raw]) 

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

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

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

152 return result