Skip to main content

zerodds_time_service/
time_base.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! `TimeBase`-Modul aus OMG Time Service 1.1 §1.3.2.
4//!
5//! ```idl
6//! module TimeBase {
7//!     typedef unsigned long long TimeT;
8//!     typedef TimeT             InaccuracyT;
9//!     typedef short             TdfT;
10//!     struct UtcT {
11//!         TimeT       time;        // 8 octets
12//!         unsigned long inacclo;   // 4 octets
13//!         unsigned short inacchi;  // 2 octets
14//!         TdfT        tdf;         // 2 octets
15//!         // total 16 octets.
16//!     };
17//!     struct IntervalT {
18//!         TimeT       lower_bound;
19//!         TimeT       upper_bound;
20//!     };
21//! };
22//! ```
23
24/// `TimeBase::TimeT` (Spec §1.3.2.1) — 64-bit Tick-Counter mit
25/// 100-Nanosekunden-Aufloesung. Fuer absolute Zeit ist die Basis
26/// `15 October 1582 00:00:00` (Gregorianischer Kalender). Fuer
27/// relative Zeit ist die Basis kontext-abhaengig.
28pub type TimeT = u64;
29
30/// `TimeBase::InaccuracyT` (Spec §1.3.2.2) — 48-bit Wert in
31/// 100-Nanosekunden-Einheiten. Wir packen ihn als `u64` und
32/// erzwingen Range bei `pack`/`unpack` (siehe [`UtcT::set_inaccuracy`]).
33pub type InaccuracyT = u64;
34
35/// `TimeBase::TdfT` (Spec §1.3.2.3) — 16-bit signed short, gibt
36/// die Time-Displacement-Factor in Minuten an (Greenwich = 0,
37/// East = positiv, West = negativ).
38pub type TdfT = i16;
39
40/// 100-Nanosekunden-Schritte zwischen 15 October 1582 00:00:00 (UTC)
41/// und 1 January 1970 00:00:00 (UNIX-Epoch).
42///
43/// Wert berechnet aus 141,427 Tagen: 141_427 * 24 * 60 * 60 * 10_000_000.
44pub const UTC_EPOCH_TO_UNIX_TICKS: TimeT = 122_192_928_000_000_000;
45
46/// Anzahl 100ns-Ticks pro Sekunde.
47pub const TICKS_PER_SECOND: u64 = 10_000_000;
48
49/// `TimeBase::UtcT` (Spec §1.3.2.4) — Universal-Time-Coordinated
50/// Struct mit 16 Bytes Wire-Format.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
52pub struct UtcT {
53    /// Time-Wert (TimeT). 100ns-Ticks since 1582 fuer absolute Zeit
54    /// bzw. relativer Wert fuer Duration-Form.
55    pub time: TimeT,
56    /// Niedrige 32 Bit der Inaccuracy (`inacclo`).
57    pub inacclo: u32,
58    /// Hohe 16 Bit der Inaccuracy (`inacchi`).
59    pub inacchi: u16,
60    /// Time-Displacement-Factor (`tdf`).
61    pub tdf: TdfT,
62}
63
64impl UtcT {
65    /// Konstruktor mit allen Spec-Feldern. Inaccuracy wird auf 48 bit
66    /// gekappt (Spec §1.3.2.4).
67    #[must_use]
68    pub const fn new(time: TimeT, inaccuracy: InaccuracyT, tdf: TdfT) -> Self {
69        let inacc = inaccuracy & 0x0000_FFFF_FFFF_FFFF;
70        Self {
71            time,
72            inacclo: (inacc & 0xFFFF_FFFF) as u32,
73            inacchi: ((inacc >> 32) & 0xFFFF) as u16,
74            tdf,
75        }
76    }
77
78    /// Liefert die Inaccuracy als 48-bit-zusammengesetzten Wert
79    /// (`inacchi` << 32 | `inacclo`). Spec §1.3.2.2 + §1.3.2.4.
80    #[must_use]
81    pub const fn inaccuracy(self) -> InaccuracyT {
82        ((self.inacchi as u64) << 32) | (self.inacclo as u64)
83    }
84
85    /// Setzt Inaccuracy (kappt auf 48 bit).
86    pub const fn set_inaccuracy(&mut self, value: InaccuracyT) {
87        let v = value & 0x0000_FFFF_FFFF_FFFF;
88        self.inacclo = (v & 0xFFFF_FFFF) as u32;
89        self.inacchi = ((v >> 32) & 0xFFFF) as u16;
90    }
91
92    /// Spec §1.3.2.4 — UTC-time + tdf*600,000,000 ergibt die lokale
93    /// Zeit (in 100ns-Ticks). Liefert die lokale Zeit-TimeT.
94    /// 600_000_000 = 60 sec * 10_000_000 ticks/sec.
95    #[must_use]
96    pub const fn local_time(self) -> TimeT {
97        let tdf_ticks = (self.tdf as i64) * 600_000_000;
98        // Sicheres `wrapping_add` analog Spec-Vorgabe (saturate ist
99        // Spec-fremd; wir folgen exakt der Formel).
100        let signed = self.time as i64;
101        signed.wrapping_add(tdf_ticks) as TimeT
102    }
103
104    /// Encode als 16-Byte Wire-Form (TimeT 8B little-endian, inacclo 4B,
105    /// inacchi 2B, tdf 2B). Reihenfolge gemaess Spec-Struct-Layout.
106    #[must_use]
107    pub fn to_wire(self) -> [u8; 16] {
108        let mut buf = [0u8; 16];
109        buf[0..8].copy_from_slice(&self.time.to_le_bytes());
110        buf[8..12].copy_from_slice(&self.inacclo.to_le_bytes());
111        buf[12..14].copy_from_slice(&self.inacchi.to_le_bytes());
112        buf[14..16].copy_from_slice(&self.tdf.to_le_bytes());
113        buf
114    }
115
116    /// Decode aus 16-Byte Wire-Form.
117    #[must_use]
118    pub fn from_wire(buf: [u8; 16]) -> Self {
119        let mut time_b = [0u8; 8];
120        time_b.copy_from_slice(&buf[0..8]);
121        let mut lo = [0u8; 4];
122        lo.copy_from_slice(&buf[8..12]);
123        let mut hi = [0u8; 2];
124        hi.copy_from_slice(&buf[12..14]);
125        let mut tdf = [0u8; 2];
126        tdf.copy_from_slice(&buf[14..16]);
127        Self {
128            time: TimeT::from_le_bytes(time_b),
129            inacclo: u32::from_le_bytes(lo),
130            inacchi: u16::from_le_bytes(hi),
131            tdf: TdfT::from_le_bytes(tdf),
132        }
133    }
134}
135
136/// `TimeBase::IntervalT` (Spec §1.3.2.5) — Zeit-Intervall mit
137/// `lower_bound` und `upper_bound`. Lower > Upper ist invalid und
138/// wird beim Konstruieren rejected.
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
140pub struct IntervalT {
141    /// Untere Grenze.
142    pub lower_bound: TimeT,
143    /// Obere Grenze.
144    pub upper_bound: TimeT,
145}
146
147impl IntervalT {
148    /// Spec §1.3.2.5: `lower_bound > upper_bound` ist invalid.
149    /// Liefert `None` in diesem Fall.
150    #[must_use]
151    pub const fn new(lower_bound: TimeT, upper_bound: TimeT) -> Option<Self> {
152        if lower_bound > upper_bound {
153            None
154        } else {
155            Some(Self {
156                lower_bound,
157                upper_bound,
158            })
159        }
160    }
161
162    /// Encode als 16-Byte (2x TimeT little-endian).
163    #[must_use]
164    pub fn to_wire(self) -> [u8; 16] {
165        let mut buf = [0u8; 16];
166        buf[0..8].copy_from_slice(&self.lower_bound.to_le_bytes());
167        buf[8..16].copy_from_slice(&self.upper_bound.to_le_bytes());
168        buf
169    }
170
171    /// Decode aus 16-Byte Wire.
172    #[must_use]
173    pub fn from_wire(buf: [u8; 16]) -> Self {
174        let mut lo = [0u8; 8];
175        lo.copy_from_slice(&buf[0..8]);
176        let mut hi = [0u8; 8];
177        hi.copy_from_slice(&buf[8..16]);
178        Self {
179            lower_bound: TimeT::from_le_bytes(lo),
180            upper_bound: TimeT::from_le_bytes(hi),
181        }
182    }
183}
184
185/// Hilfsfunktion: liefert die aktuelle Zeit als `TimeT`-Wert (100ns-
186/// Ticks since 1582). Auf `std`-Plattformen via `SystemTime`. Auf
187/// `no_std` ist diese Funktion nicht verfuegbar.
188#[cfg(feature = "std")]
189#[must_use]
190pub fn current_time() -> TimeT {
191    use std::time::{SystemTime, UNIX_EPOCH};
192    match SystemTime::now().duration_since(UNIX_EPOCH) {
193        Ok(d) => {
194            let nanos = d.as_secs() as u128 * 1_000_000_000 + d.subsec_nanos() as u128;
195            let ticks_since_unix = (nanos / 100) as u64;
196            UTC_EPOCH_TO_UNIX_TICKS.wrapping_add(ticks_since_unix)
197        }
198        Err(_) => 0,
199    }
200}
201
202/// `no_std`-Stub: liefert immer 0. Auf `no_std` gibt es keine
203/// Wall-Clock; eine echte Zeitquelle wird vom Embedding via
204/// `TimeService::with_source(...)` injiziert. Wer `current_time()`
205/// direkt aufruft, bekommt `0`, was im [`TimeService::universal_time`]
206/// zu `TimeUnavailable` fuehrt.
207#[cfg(not(feature = "std"))]
208#[must_use]
209pub fn current_time() -> TimeT {
210    0
211}
212
213#[cfg(test)]
214#[allow(clippy::expect_used)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn utct_size_is_16_octets() {
220        // Spec §1.3.2.4 — UtcT-Layout-Total-Groesse ist 16 Octets.
221        let utc = UtcT::new(0x0123_4567_89AB_CDEF, 0xFFFF_FFFF_FFFF, 60);
222        let wire = utc.to_wire();
223        assert_eq!(wire.len(), 16);
224    }
225
226    #[test]
227    fn intervalt_size_is_16_octets() {
228        // Spec §1.3.2.5 — IntervalT = 2x TimeT = 16 Octets.
229        let i = IntervalT::new(0, 0).expect("ok");
230        let wire = i.to_wire();
231        assert_eq!(wire.len(), 16);
232    }
233
234    #[test]
235    fn inaccuracy_caps_at_48_bits() {
236        // Spec §1.3.2.2 + §1.3.2.4 — Inaccuracy hat 48 bit.
237        let utc = UtcT::new(0, u64::MAX, 0);
238        assert_eq!(utc.inaccuracy(), 0x0000_FFFF_FFFF_FFFF);
239    }
240
241    #[test]
242    fn utct_wire_roundtrip_preserves_all_fields() {
243        let original = UtcT::new(123_456_789_012_345_678, 0xAABB_CCDD_EEFF, -480);
244        let wire = original.to_wire();
245        let decoded = UtcT::from_wire(wire);
246        assert_eq!(decoded, original);
247    }
248
249    #[test]
250    fn intervalt_wire_roundtrip_preserves_bounds() {
251        let i = IntervalT::new(100, 200).expect("ok");
252        let decoded = IntervalT::from_wire(i.to_wire());
253        assert_eq!(decoded.lower_bound, 100);
254        assert_eq!(decoded.upper_bound, 200);
255    }
256
257    #[test]
258    fn intervalt_rejects_lower_greater_than_upper() {
259        // Spec §1.3.2.5: lower > upper ist invalid.
260        assert!(IntervalT::new(200, 100).is_none());
261    }
262
263    #[test]
264    fn local_time_applies_tdf() {
265        // Spec §1.3.2.4: utc.time + utc.tdf * 600_000_000 == lokale Zeit.
266        // tdf = 60 (Berlin Sommerzeit, +60 Minuten); ticks/min =
267        // 60 sec * 10_000_000 = 600_000_000.
268        let utc = UtcT::new(1_000_000_000, 0, 60);
269        let expected = 1_000_000_000_i64 + 60 * 600_000_000;
270        assert_eq!(utc.local_time(), expected as TimeT);
271    }
272
273    #[test]
274    fn local_time_negative_tdf_west_of_greenwich() {
275        // Spec §1.3.2.3 — westlich Greenwich = negativ.
276        let utc = UtcT::new(1_000_000_000_000, 0, -480); // PST (-8h)
277        let expected = 1_000_000_000_000_i64 + (-480) * 600_000_000;
278        assert_eq!(utc.local_time(), expected as TimeT);
279    }
280
281    #[cfg(feature = "std")]
282    #[test]
283    fn current_time_is_recent_century() {
284        // Sanity: current_time gibt Wert in plausiblem Bereich
285        // (nach 2020, vor 2200). Tick-Range fuer 2020-01-01 ist
286        // (2020 - 1582 = 438 Jahre) * 365.25 * 24 * 60 * 60 * 10_000_000
287        // ≈ 1.38e17.
288        let t = current_time();
289        assert!(t > 130_000_000_000_000_000);
290        assert!(t < 200_000_000_000_000_000);
291    }
292}