Skip to main content

nros_core/
clock.rs

1//! Clock API for nros
2//!
3//! This module provides clock abstraction for different time sources:
4//! - **SystemTime**: Wall clock time (affected by system time changes)
5//! - **SteadyTime**: Monotonic time (not affected by system time changes)
6//! - **RosTime**: Simulation time (can be paused/scaled)
7//!
8//! # Example
9//!
10//! ```text
11//! use nros::clock::{Clock, ClockType};
12//!
13//! // Create a system clock
14//! let clock = Clock::system();
15//! let now = clock.now();
16//! println!("Current time: {} sec", now.sec);
17//!
18//! // Create a steady clock for measuring durations
19//! let clock = Clock::steady();
20//! let start = clock.now();
21//! // ... do work ...
22//! let elapsed = clock.now() - start;
23//! ```
24//!
25//! # no_std Support
26//!
27//! Without the `std` feature, clocks return time based on an internal
28//! counter that must be updated manually via `update_time()`. This is
29//! suitable for embedded systems with RTIC or bare-metal polling loops.
30
31use crate::time::Time;
32
33// AtomicI64 is not available on all platforms (e.g., thumbv7em-none-eabihf)
34// Use AtomicI64 when available, otherwise use a simpler approach
35#[cfg(target_has_atomic = "64")]
36use core::sync::atomic::{AtomicI64, Ordering};
37
38// For platforms without 64-bit atomics, use two 32-bit values
39#[cfg(not(target_has_atomic = "64"))]
40use core::sync::atomic::{AtomicI32, Ordering};
41
42/// Type of clock to use for time queries
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44pub enum ClockType {
45    /// System time (wall clock)
46    ///
47    /// This clock reflects the system's real-time clock and may be
48    /// affected by NTP adjustments, user changes, or daylight saving time.
49    /// Use for timestamps that need to correlate with real-world time.
50    #[default]
51    SystemTime,
52
53    /// Steady/monotonic time
54    ///
55    /// This clock is guaranteed to be monotonically increasing and is
56    /// not affected by system time changes. Use for measuring durations
57    /// and timeouts.
58    SteadyTime,
59
60    /// ROS time (simulation time)
61    ///
62    /// This clock can be overridden for simulation purposes. When a
63    /// ROS time override is active, `now()` returns the overridden time.
64    /// Otherwise, it falls back to system time.
65    RosTime,
66}
67
68// On platforms with 64-bit atomics, use AtomicI64 directly
69#[cfg(target_has_atomic = "64")]
70mod atomic_time {
71    use super::*;
72
73    /// Global ROS time override (nanoseconds since epoch)
74    /// When set to a non-negative value, `Clock::now()` for `RosTime` clocks
75    /// will return this value instead of system time.
76    pub(super) static ROS_TIME_OVERRIDE_NANOS: AtomicI64 = AtomicI64::new(-1);
77
78    /// Global steady time counter (nanoseconds)
79    /// For `no_std` environments, this counter must be updated manually.
80    pub(super) static STEADY_TIME_NANOS: AtomicI64 = AtomicI64::new(0);
81
82    pub(super) fn get_ros_override() -> i64 {
83        ROS_TIME_OVERRIDE_NANOS.load(Ordering::Relaxed)
84    }
85
86    pub(super) fn set_ros_override(nanos: i64) {
87        ROS_TIME_OVERRIDE_NANOS.store(nanos, Ordering::Relaxed);
88    }
89
90    pub(super) fn get_steady() -> i64 {
91        STEADY_TIME_NANOS.load(Ordering::Relaxed)
92    }
93
94    pub(super) fn set_steady(nanos: i64) {
95        STEADY_TIME_NANOS.store(nanos, Ordering::Relaxed);
96    }
97
98    pub(super) fn add_steady(delta: i64) {
99        STEADY_TIME_NANOS.fetch_add(delta, Ordering::Relaxed);
100    }
101}
102
103// On platforms without 64-bit atomics, use split 32-bit values
104// Note: This is not fully atomic but works for single-threaded embedded contexts
105#[cfg(not(target_has_atomic = "64"))]
106mod atomic_time {
107    use super::*;
108
109    // Split into high and low 32-bit parts
110    static ROS_TIME_OVERRIDE_LOW: AtomicI32 = AtomicI32::new(-1);
111    static ROS_TIME_OVERRIDE_HIGH: AtomicI32 = AtomicI32::new(-1);
112    static STEADY_TIME_LOW: AtomicI32 = AtomicI32::new(0);
113    static STEADY_TIME_HIGH: AtomicI32 = AtomicI32::new(0);
114
115    pub(super) fn get_ros_override() -> i64 {
116        let high = ROS_TIME_OVERRIDE_HIGH.load(Ordering::Relaxed);
117        let low = ROS_TIME_OVERRIDE_LOW.load(Ordering::Relaxed);
118        if high < 0 {
119            -1
120        } else {
121            ((high as i64) << 32) | (low as u32 as i64)
122        }
123    }
124
125    pub(super) fn set_ros_override(nanos: i64) {
126        if nanos < 0 {
127            ROS_TIME_OVERRIDE_HIGH.store(-1, Ordering::Relaxed);
128            ROS_TIME_OVERRIDE_LOW.store(-1, Ordering::Relaxed);
129        } else {
130            ROS_TIME_OVERRIDE_HIGH.store((nanos >> 32) as i32, Ordering::Relaxed);
131            ROS_TIME_OVERRIDE_LOW.store(nanos as i32, Ordering::Relaxed);
132        }
133    }
134
135    pub(super) fn get_steady() -> i64 {
136        let high = STEADY_TIME_HIGH.load(Ordering::Relaxed);
137        let low = STEADY_TIME_LOW.load(Ordering::Relaxed);
138        ((high as i64) << 32) | (low as u32 as i64)
139    }
140
141    pub(super) fn set_steady(nanos: i64) {
142        STEADY_TIME_HIGH.store((nanos >> 32) as i32, Ordering::Relaxed);
143        STEADY_TIME_LOW.store(nanos as i32, Ordering::Relaxed);
144    }
145
146    pub(super) fn add_steady(delta: i64) {
147        let current = get_steady();
148        set_steady(current.saturating_add(delta));
149    }
150}
151
152/// A clock for querying time
153///
154/// Clocks provide access to different time sources. Each node typically
155/// has an associated clock, but you can also create standalone clocks.
156#[derive(Debug, Clone, Copy)]
157pub struct Clock {
158    clock_type: ClockType,
159}
160
161impl Default for Clock {
162    fn default() -> Self {
163        Self::system()
164    }
165}
166
167/// The platform's wall clock, or `None` when this build has no port to ask.
168///
169/// phase-359 W10 (backend tier). `nros-core` sits BELOW `nros-platform`, so it
170/// cannot depend on it — it declares the two ABI symbols directly, exactly as
171/// `nros-node` already does for `nros_platform_clock_ns` ("every platform port
172/// exports it through the same linkage contract"). The feature is what promises
173/// a port is linked; without it this is `None` and the caller keeps the counter
174/// it had.
175///
176/// ONE symbol since issue 0532 item 5 collapsed the wall clock; this function
177/// was named there as the one place that would change, and it was.
178// phase-359 W10 follow-up — NOT `not(std)`. This used to be gated away on a
179// `std` build, so an image with a port linked read `SystemTime` from
180// `Clock::system()` and `nros_platform_time_now_ns` from the executor's epoch
181// source: two wall clocks, one image. They agree on POSIX by coincidence (both
182// are CLOCK_REALTIME) and stop agreeing the moment a port has an opinion — an
183// RTC-backed or simulated one — because only the port is authoritative and only
184// one of the two readers asked it. The rule W10 set in `nros-node` applies
185// here: when a port is linked it IS the clock, and `std` is what a build
186// without one falls back to.
187#[cfg(feature = "platform-clock")]
188fn platform_wall_clock() -> Option<Time> {
189    unsafe extern "C" {
190        fn nros_platform_time_now_ns() -> u64;
191    }
192    // SAFETY: a bare wall-clock read, no pointer arguments, guaranteed by
193    // whichever port linked the binary — the same contract `nros-node` relies
194    // on for `nros_platform_clock_ns`.
195    //
196    // ONE symbol, so one sample: issue 0532 collapsed the former
197    // `time_since_epoch_{secs,nanos}` pair, which this was written against and
198    // which needed a bounded re-read to survive a second boundary landing
199    // between the two calls. That loop is what the collapse deletes.
200    let ns = unsafe { nros_platform_time_now_ns() };
201    // A port with no RTC returns 0. Reporting the Unix epoch as "now" would be
202    // a wrong answer stated confidently, so say nothing and let the caller's
203    // counter fallback stand.
204    if ns == 0 {
205        return None;
206    }
207    Some(Time::new(
208        (ns / 1_000_000_000) as i32,
209        (ns % 1_000_000_000) as u32,
210    ))
211}
212
213#[cfg(not(feature = "platform-clock"))]
214fn platform_wall_clock() -> Option<Time> {
215    None
216}
217
218impl Clock {
219    /// Create a new clock of the specified type
220    pub const fn new(clock_type: ClockType) -> Self {
221        Self { clock_type }
222    }
223
224    /// Create a system time clock
225    ///
226    /// System time reflects the real-world wall clock time.
227    pub const fn system() -> Self {
228        Self {
229            clock_type: ClockType::SystemTime,
230        }
231    }
232
233    /// Create a steady (monotonic) time clock
234    ///
235    /// Steady time is guaranteed to only increase and is not affected
236    /// by system time changes.
237    pub const fn steady() -> Self {
238        Self {
239            clock_type: ClockType::SteadyTime,
240        }
241    }
242
243    /// Create a ROS time clock
244    ///
245    /// ROS time can be overridden for simulation. When no override is
246    /// active, it returns system time.
247    pub const fn ros_time() -> Self {
248        Self {
249            clock_type: ClockType::RosTime,
250        }
251    }
252
253    /// Get the clock type
254    pub const fn clock_type(&self) -> ClockType {
255        self.clock_type
256    }
257
258    /// Get the current time from this clock
259    ///
260    /// # Platform behavior
261    ///
262    /// - **A platform port linked** (`platform-clock`): the port's wall clock,
263    ///   on either flavour — it is the authority, and an image must not hold
264    ///   two answers to "what time is it".
265    /// - **No port**: the internal counter, which the caller advances with
266    ///   `update_steady_time()`.
267    ///
268    /// phase-359 W10 — there is no `std::time` arm. The platform API IS the
269    /// clock: a build with a port reads it, a build without one has no clock to
270    /// read and says so with the counter it was given. `SystemTime` used to sit
271    /// here as a third answer for hosted builds, which made "what time is it"
272    /// depend on whether some crate in the graph happened to name `std`.
273    ///
274    /// `SteadyTime` is the counter on every flavour, deliberately: it is
275    /// advanced by its owner rather than read from a source, and the port's
276    /// monotonic export (`nros_platform_clock_ns`) is the executor's business,
277    /// not this type's.
278    pub fn now(&self) -> Time {
279        match self.clock_type {
280            ClockType::SystemTime => {
281                // phase-359 W10 (backend tier) — a wall clock that does not
282                // need the `std` FEATURE. Before this, `SystemTime` here fell
283                // back to the STEADY counter: the same value a monotonic clock
284                // returns, presented as time since the Unix epoch. That is not
285                // a degraded wall clock, it is a different quantity, and the
286                // only thing standing between a build and it was whether some
287                // crate in the graph happened to name `std`.
288                if let Some(t) = platform_wall_clock() {
289                    return t;
290                }
291                let nanos = atomic_time::get_steady();
292                Time::from_nanos(nanos)
293            }
294            ClockType::SteadyTime => {
295                let nanos = atomic_time::get_steady();
296                Time::from_nanos(nanos)
297            }
298            ClockType::RosTime => {
299                let override_nanos = atomic_time::get_ros_override();
300                if override_nanos >= 0 {
301                    Time::from_nanos(override_nanos)
302                } else {
303                    let nanos = atomic_time::get_steady();
304                    Time::from_nanos(nanos)
305                }
306            }
307        }
308    }
309
310    /// Set a ROS time override
311    ///
312    /// When set, all `RosTime` clocks will return this time instead of
313    /// system time. This is useful for simulation.
314    ///
315    /// # Arguments
316    /// * `nanos` - Nanoseconds since epoch
317    pub fn set_ros_time_override(nanos: i64) {
318        atomic_time::set_ros_override(nanos);
319    }
320
321    /// Set a ROS time override from a Time value
322    pub fn set_ros_time_override_time(time: Time) {
323        Self::set_ros_time_override(time.to_nanos());
324    }
325
326    /// Clear the ROS time override
327    ///
328    /// After clearing, `RosTime` clocks will return system time again.
329    pub fn clear_ros_time_override() {
330        atomic_time::set_ros_override(-1);
331    }
332
333    /// Check if a ROS time override is active
334    pub fn is_ros_time_override_active() -> bool {
335        atomic_time::get_ros_override() >= 0
336    }
337
338    /// Get the current ROS time override value (if active)
339    pub fn get_ros_time_override() -> Option<Time> {
340        let nanos = atomic_time::get_ros_override();
341        if nanos >= 0 {
342            Some(Time::from_nanos(nanos))
343        } else {
344            None
345        }
346    }
347
348    /// Update the steady time counter
349    ///
350    /// For `no_std` environments, call this periodically from your
351    /// main loop or RTIC task to advance the steady clock.
352    ///
353    /// # Arguments
354    /// * `delta_nanos` - Nanoseconds elapsed since last call
355    pub fn update_steady_time(delta_nanos: i64) {
356        atomic_time::add_steady(delta_nanos);
357    }
358
359    /// Update the steady time counter (milliseconds version)
360    ///
361    /// Convenience method for RTIC tasks using millisecond intervals.
362    ///
363    /// # Arguments
364    /// * `delta_ms` - Milliseconds elapsed since last call
365    pub fn update_steady_time_ms(delta_ms: u64) {
366        let delta_nanos = delta_ms as i64 * 1_000_000;
367        Self::update_steady_time(delta_nanos);
368    }
369
370    /// Set the steady time counter to a specific value
371    ///
372    /// Use this to initialize the clock or synchronize with an external
373    /// time source.
374    pub fn set_steady_time(nanos: i64) {
375        atomic_time::set_steady(nanos);
376    }
377
378    /// Get the current steady time counter value
379    pub fn get_steady_time_nanos() -> i64 {
380        atomic_time::get_steady()
381    }
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387
388    #[test]
389    fn test_clock_type_default() {
390        let clock_type = ClockType::default();
391        assert_eq!(clock_type, ClockType::SystemTime);
392    }
393
394    #[test]
395    fn test_clock_constructors() {
396        let system = Clock::system();
397        assert_eq!(system.clock_type(), ClockType::SystemTime);
398
399        let steady = Clock::steady();
400        assert_eq!(steady.clock_type(), ClockType::SteadyTime);
401
402        let ros = Clock::ros_time();
403        assert_eq!(ros.clock_type(), ClockType::RosTime);
404
405        let custom = Clock::new(ClockType::SteadyTime);
406        assert_eq!(custom.clock_type(), ClockType::SteadyTime);
407    }
408
409    #[test]
410    fn test_clock_default() {
411        let clock = Clock::default();
412        assert_eq!(clock.clock_type(), ClockType::SystemTime);
413    }
414
415    #[test]
416    fn test_ros_time_override() {
417        // Clear any existing override
418        Clock::clear_ros_time_override();
419        assert!(!Clock::is_ros_time_override_active());
420        assert!(Clock::get_ros_time_override().is_none());
421
422        // Set override
423        let override_time = Time::new(1234567890, 123456789);
424        Clock::set_ros_time_override_time(override_time);
425        assert!(Clock::is_ros_time_override_active());
426        assert_eq!(Clock::get_ros_time_override(), Some(override_time));
427
428        // ROS clock should return override time
429        let ros_clock = Clock::ros_time();
430        let now = ros_clock.now();
431        assert_eq!(now, override_time);
432
433        // Clear override
434        Clock::clear_ros_time_override();
435        assert!(!Clock::is_ros_time_override_active());
436    }
437
438    #[test]
439    fn test_steady_time_update() {
440        // Reset steady time
441        Clock::set_steady_time(0);
442        assert_eq!(Clock::get_steady_time_nanos(), 0);
443
444        // Update by milliseconds
445        Clock::update_steady_time_ms(100);
446        assert_eq!(Clock::get_steady_time_nanos(), 100_000_000);
447
448        // Update by nanoseconds
449        Clock::update_steady_time(500_000_000);
450        assert_eq!(Clock::get_steady_time_nanos(), 600_000_000);
451
452        // Steady clock should return updated time
453        let steady_clock = Clock::steady();
454        let now = steady_clock.now();
455        assert_eq!(now.to_nanos(), 600_000_000);
456    }
457
458    #[test]
459    #[cfg(all(feature = "std", not(feature = "platform-clock")))]
460    #[cfg_attr(miri, ignore)] // Miri doesn't support clock_gettime with isolation
461    fn test_system_clock_returns_nonzero() {
462        let clock = Clock::system();
463        let now = clock.now();
464        // System time should be after Unix epoch (positive)
465        assert!(now.sec > 0);
466    }
467
468    /// phase-359 W10 follow-up — a linked port OUTRANKS `std` for the wall
469    /// clock, on a `std` build too.
470    ///
471    /// This is the one configuration where the two disagree observably, and it
472    /// is why the test defines the port symbol itself: the value returned is
473    /// nothing like a real `SystemTime`, so a `Clock::system()` that answered
474    /// with `SystemTime::now()` — which is what this file did before, because
475    /// `platform_wall_clock` was gated `not(std)` — fails loudly rather than
476    /// coincidentally passing. On POSIX both sources are CLOCK_REALTIME, so
477    /// nothing short of a port with an opinion can tell them apart.
478    #[test]
479    #[cfg(all(feature = "std", feature = "platform-clock"))]
480    fn platform_port_outranks_std_for_the_wall_clock() {
481        /// 2001-09-09T01:46:40Z — a fixed instant no real clock will return.
482        const FAKE_NS: u64 = 1_000_000_000 * 1_000_000_000;
483
484        #[unsafe(no_mangle)]
485        extern "C" fn nros_platform_time_now_ns() -> u64 {
486            FAKE_NS
487        }
488
489        let now = Clock::system().now();
490        assert_eq!(
491            now.sec, 1_000_000_000,
492            "Clock::system() must read the linked port, not SystemTime"
493        );
494        assert_eq!(now.nanosec, 0);
495    }
496}