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}