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
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-03 16:41 +0000
1"""Elapsed Time characteristic implementation.
3Implements the Elapsed Time characteristic (0x2BF2).
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
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.
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
23References:
24 Bluetooth SIG Generic Sensor Service
25 org.bluetooth.characteristic.elapsed_time (GSS YAML)
26"""
28from __future__ import annotations
30from enum import IntEnum, IntFlag
32import msgspec
34from ..context import CharacteristicContext
35from .base import BaseCharacteristic
36from .reference_time_information import TimeSource
37from .utils import DataParser
39_TIME_RESOLUTION_MASK = 0x0C
40_TIME_RESOLUTION_SHIFT = 2
41_READ_LENGTH = 11 # 9 (Elapsed Time struct) + 1 (Clock Status) + 1 (Clock Capabilities)
44class ElapsedTimeFlags(IntFlag):
45 """Flags for the Elapsed Time characteristic."""
47 TICK_COUNTER = 1 << 0
48 UTC = 1 << 1
49 TZ_DST_USED = 1 << 4
50 CURRENT_TIMELINE = 1 << 5
53class TimeResolution(IntEnum):
54 """Time resolution values (bits 2-3 of flags)."""
56 ONE_SECOND = 0
57 HUNDRED_MILLISECONDS = 1
58 ONE_MILLISECOND = 2
59 HUNDRED_MICROSECONDS = 3
62class ElapsedTimeData(msgspec.Struct, frozen=True, kw_only=True):
63 """Parsed data from Current Elapsed Time characteristic.
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).
79 """
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
95class ElapsedTimeCharacteristic(BaseCharacteristic[ElapsedTimeData]):
96 """Elapsed Time characteristic (0x2BF2).
98 Reports the current time of a clock or tick counter.
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 """
104 expected_type = ElapsedTimeData
105 min_length: int = 9
106 max_length: int = 11
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.
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.
122 Returns:
123 ElapsedTimeData with parsed time information.
125 """
126 flags_raw = data[0]
127 flags = ElapsedTimeFlags(flags_raw & 0x33) # Mask out resolution bits + reserved
129 time_resolution = TimeResolution((flags_raw & _TIME_RESOLUTION_MASK) >> _TIME_RESOLUTION_SHIFT)
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)
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)
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 )
160 def _encode_value(self, data: ElapsedTimeData) -> bytearray:
161 """Encode ElapsedTimeData back to BLE bytes.
163 Produces the full 11-byte read/indicate format including Clock Status
164 and Clock Capabilities.
166 Args:
167 data: ElapsedTimeData instance.
169 Returns:
170 Encoded bytearray (11 bytes).
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))
179 clock_status = int(data.clock_needs_set)
180 result.append(clock_status)
182 clock_caps = int(data.clock_applies_dst) | (int(data.clock_manages_tz) << 1)
183 result.append(clock_caps)
185 return result