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//! # Where the time comes from
26//!
27//! With a platform port linked (the `platform-clock` feature), every clock
28//! reads the PORT: `nros_platform_time_now_ns` for the wall, and
29//! `nros_platform_clock_ns` for the monotonic. Without one there is no clock
30//! to read, and all three types fall back to an internal counter the caller
31//! advances with `update_steady_time()` — suitable for a bare-metal RTIC or
32//! polling loop, which is the only build where that counter has an owner.
33
34use crate::time::Time;
35
36// AtomicI64 is not available on all platforms (e.g., thumbv7em-none-eabihf)
37// Use AtomicI64 when available, otherwise use a simpler approach
38#[cfg(target_has_atomic = "64")]
39use core::sync::atomic::{AtomicI64, Ordering};
40
41// For platforms without 64-bit atomics, use two 32-bit values
42#[cfg(not(target_has_atomic = "64"))]
43use core::sync::atomic::{AtomicI32, Ordering};
44
45/// Type of clock to use for time queries
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
47pub enum ClockType {
48 /// System time (wall clock)
49 ///
50 /// This clock reflects the system's real-time clock and may be
51 /// affected by NTP adjustments, user changes, or daylight saving time.
52 /// Use for timestamps that need to correlate with real-world time.
53 #[default]
54 SystemTime,
55
56 /// Steady/monotonic time
57 ///
58 /// This clock is guaranteed to be monotonically increasing and is
59 /// not affected by system time changes. Use for measuring durations
60 /// and timeouts. Reads the port's `nros_platform_clock_ns` where one is
61 /// linked, and the caller-advanced counter where none is.
62 SteadyTime,
63
64 /// ROS time (simulation time)
65 ///
66 /// This clock can be overridden for simulation purposes. When a
67 /// ROS time override is active — a simulator's or a bag player's
68 /// `/clock` — `now()` returns the overridden time. Otherwise it IS
69 /// [`SystemTime`](Self::SystemTime), which is what `rclcpp`'s
70 /// `ClockType::ROS_TIME` reads with `use_sim_time` false, so a node
71 /// written for simulation still runs standalone.
72 RosTime,
73}
74
75// On platforms with 64-bit atomics, use AtomicI64 directly
76#[cfg(target_has_atomic = "64")]
77mod atomic_time {
78 use super::*;
79
80 /// Global ROS time override (nanoseconds since epoch)
81 /// When set to a non-negative value, `Clock::now()` for `RosTime` clocks
82 /// will return this value instead of system time.
83 pub(super) static ROS_TIME_OVERRIDE_NANOS: AtomicI64 = AtomicI64::new(-1);
84
85 /// Global steady time counter (nanoseconds)
86 /// For `no_std` environments, this counter must be updated manually.
87 pub(super) static STEADY_TIME_NANOS: AtomicI64 = AtomicI64::new(0);
88
89 pub(super) fn get_ros_override() -> i64 {
90 ROS_TIME_OVERRIDE_NANOS.load(Ordering::Relaxed)
91 }
92
93 pub(super) fn set_ros_override(nanos: i64) {
94 ROS_TIME_OVERRIDE_NANOS.store(nanos, Ordering::Relaxed);
95 }
96
97 pub(super) fn get_steady() -> i64 {
98 STEADY_TIME_NANOS.load(Ordering::Relaxed)
99 }
100
101 pub(super) fn set_steady(nanos: i64) {
102 STEADY_TIME_NANOS.store(nanos, Ordering::Relaxed);
103 }
104
105 pub(super) fn add_steady(delta: i64) {
106 STEADY_TIME_NANOS.fetch_add(delta, Ordering::Relaxed);
107 }
108}
109
110// On platforms without 64-bit atomics, use split 32-bit values
111// Note: This is not fully atomic but works for single-threaded embedded contexts
112#[cfg(not(target_has_atomic = "64"))]
113mod atomic_time {
114 use super::*;
115
116 // Split into high and low 32-bit parts
117 static ROS_TIME_OVERRIDE_LOW: AtomicI32 = AtomicI32::new(-1);
118 static ROS_TIME_OVERRIDE_HIGH: AtomicI32 = AtomicI32::new(-1);
119 static STEADY_TIME_LOW: AtomicI32 = AtomicI32::new(0);
120 static STEADY_TIME_HIGH: AtomicI32 = AtomicI32::new(0);
121
122 pub(super) fn get_ros_override() -> i64 {
123 let high = ROS_TIME_OVERRIDE_HIGH.load(Ordering::Relaxed);
124 let low = ROS_TIME_OVERRIDE_LOW.load(Ordering::Relaxed);
125 if high < 0 {
126 -1
127 } else {
128 ((high as i64) << 32) | (low as u32 as i64)
129 }
130 }
131
132 pub(super) fn set_ros_override(nanos: i64) {
133 if nanos < 0 {
134 ROS_TIME_OVERRIDE_HIGH.store(-1, Ordering::Relaxed);
135 ROS_TIME_OVERRIDE_LOW.store(-1, Ordering::Relaxed);
136 } else {
137 ROS_TIME_OVERRIDE_HIGH.store((nanos >> 32) as i32, Ordering::Relaxed);
138 ROS_TIME_OVERRIDE_LOW.store(nanos as i32, Ordering::Relaxed);
139 }
140 }
141
142 pub(super) fn get_steady() -> i64 {
143 let high = STEADY_TIME_HIGH.load(Ordering::Relaxed);
144 let low = STEADY_TIME_LOW.load(Ordering::Relaxed);
145 ((high as i64) << 32) | (low as u32 as i64)
146 }
147
148 pub(super) fn set_steady(nanos: i64) {
149 STEADY_TIME_HIGH.store((nanos >> 32) as i32, Ordering::Relaxed);
150 STEADY_TIME_LOW.store(nanos as i32, Ordering::Relaxed);
151 }
152
153 pub(super) fn add_steady(delta: i64) {
154 let current = get_steady();
155 set_steady(current.saturating_add(delta));
156 }
157}
158
159/// A clock for querying time
160///
161/// Clocks provide access to different time sources. Each node typically
162/// has an associated clock, but you can also create standalone clocks.
163#[derive(Debug, Clone, Copy)]
164pub struct Clock {
165 clock_type: ClockType,
166}
167
168impl Default for Clock {
169 fn default() -> Self {
170 Self::system()
171 }
172}
173
174/// The platform's wall clock, or `None` when this build has no port to ask.
175///
176/// phase-359 W10 (backend tier). `nros-core` sits BELOW `nros-platform`, so it
177/// cannot depend on it — it declares the two ABI symbols directly, exactly as
178/// `nros-node` already does for `nros_platform_clock_ns` ("every platform port
179/// exports it through the same linkage contract"). The feature is what promises
180/// a port is linked; without it this is `None` and the caller keeps the counter
181/// it had.
182///
183/// ONE symbol since issue 0532 item 5 collapsed the wall clock; this function
184/// was named there as the one place that would change, and it was.
185// phase-359 W10 follow-up — NOT `not(std)`. This used to be gated away on a
186// `std` build, so an image with a port linked read `SystemTime` from
187// `Clock::system()` and `nros_platform_time_now_ns` from the executor's epoch
188// source: two wall clocks, one image. They agree on POSIX by coincidence (both
189// are CLOCK_REALTIME) and stop agreeing the moment a port has an opinion — an
190// RTC-backed or simulated one — because only the port is authoritative and only
191// one of the two readers asked it. The rule W10 set in `nros-node` applies
192// here: when a port is linked it IS the clock, and `std` is what a build
193// without one falls back to.
194#[cfg(feature = "platform-clock")]
195fn platform_wall_clock() -> Option<Time> {
196 unsafe extern "C" {
197 fn nros_platform_time_now_ns() -> u64;
198 }
199 // SAFETY: a bare wall-clock read, no pointer arguments, guaranteed by
200 // whichever port linked the binary — the same contract `nros-node` relies
201 // on for `nros_platform_clock_ns`.
202 //
203 // ONE symbol, so one sample: issue 0532 collapsed the former
204 // `time_since_epoch_{secs,nanos}` pair, which this was written against and
205 // which needed a bounded re-read to survive a second boundary landing
206 // between the two calls. That loop is what the collapse deletes.
207 let ns = unsafe { nros_platform_time_now_ns() };
208 // A port with no RTC returns 0. Reporting the Unix epoch as "now" would be
209 // a wrong answer stated confidently, so say nothing and let the caller's
210 // counter fallback stand.
211 if ns == 0 {
212 return None;
213 }
214 Some(Time::new(
215 (ns / 1_000_000_000) as i32,
216 (ns % 1_000_000_000) as u32,
217 ))
218}
219
220#[cfg(not(feature = "platform-clock"))]
221fn platform_wall_clock() -> Option<Time> {
222 None
223}
224
225/// The platform's MONOTONIC clock, or `None` when this build has no port to ask.
226///
227/// issue 1334 — the sibling of [`platform_wall_clock`], declared the same way
228/// and under the same feature, because it is the same promise: `platform-clock`
229/// means a port is linked, and every port exports both halves
230/// (`nros/platform.h` "Wall clock" and RFC-0073's monotonic export are
231/// MANDATORY, not optional). `nros-node`, `nros-log`, `nros-c` and the `nros`
232/// umbrella already hand-declare this symbol under exactly this contract.
233///
234/// Unlike the wall clock there is no "port with no RTC" case to detect: a
235/// monotonic counter reading 0 is a legitimate answer at boot, so a zero is
236/// passed through rather than read as "no clock".
237#[cfg(feature = "platform-clock")]
238fn platform_steady_clock() -> Option<Time> {
239 unsafe extern "C" {
240 fn nros_platform_clock_ns() -> u64;
241 }
242 // SAFETY: a bare counter read, no pointer arguments, guaranteed by
243 // whichever port linked the binary — the same contract `platform_wall_clock`
244 // above relies on for `nros_platform_time_now_ns`.
245 let ns = unsafe { nros_platform_clock_ns() };
246 Some(Time::new(
247 (ns / 1_000_000_000) as i32,
248 (ns % 1_000_000_000) as u32,
249 ))
250}
251
252#[cfg(not(feature = "platform-clock"))]
253fn platform_steady_clock() -> Option<Time> {
254 None
255}
256
257impl Clock {
258 /// Create a new clock of the specified type
259 ///
260 /// ADOPT-BOUNDED: rclrs's `Clock::new` returns `(Self, Option<ClockSource>)`,
261 /// handing back the source that drives a ROS-time clock. This returns the clock
262 /// alone. Simulated time is ONE process-wide override, set with the associated
263 /// [`Clock::set_ros_time_override`], not a per-clock source: a per-clock source
264 /// is a shared owner (`Arc`) the `core` targets do not have, and `const fn` is
265 /// what lets a `Clock` be a `static`.
266 pub const fn new(clock_type: ClockType) -> Self {
267 Self { clock_type }
268 }
269
270 /// Create a system time clock
271 ///
272 /// System time reflects the real-world wall clock time.
273 pub const fn system() -> Self {
274 Self {
275 clock_type: ClockType::SystemTime,
276 }
277 }
278
279 /// Create a steady (monotonic) time clock
280 ///
281 /// Steady time is guaranteed to only increase and is not affected
282 /// by system time changes.
283 pub const fn steady() -> Self {
284 Self {
285 clock_type: ClockType::SteadyTime,
286 }
287 }
288
289 /// Create a ROS time clock
290 ///
291 /// ROS time can be overridden for simulation. When no override is
292 /// active, it returns system time — the identical expression
293 /// [`Clock::system`] evaluates, so the two cannot drift (issue 1334).
294 pub const fn ros_time() -> Self {
295 Self {
296 clock_type: ClockType::RosTime,
297 }
298 }
299
300 /// Get the clock type
301 pub const fn clock_type(&self) -> ClockType {
302 self.clock_type
303 }
304
305 /// The wall clock, in ONE place — issue 1334.
306 ///
307 /// Two clock types answer "what is the wall time", and they must answer it
308 /// identically: `SystemTime` always, and `RosTime` whenever no `/clock`
309 /// override is installed. Before this they were two expressions, and only
310 /// the first was ever moved onto the port (phase-359 W10), so `RosTime`'s
311 /// fallback was left reading the counter — a value nothing in the tree
312 /// advances. One expression is what keeps the next move from splitting
313 /// them again.
314 ///
315 /// - **A platform port linked** (`platform-clock`): the port's wall clock.
316 /// It is the authority, and an image must not hold two answers to "what
317 /// time is it".
318 /// - **No port**: the internal counter, which the caller advances with
319 /// `update_steady_time()`. A build with no port has no clock to read, so
320 /// this is the honest answer rather than a fallback with an opinion.
321 fn wall_now() -> Time {
322 // phase-359 W10 (backend tier) — a wall clock that does not need the
323 // `std` FEATURE. Before this, `SystemTime` fell back to the STEADY
324 // counter unconditionally: the same value a monotonic clock returns,
325 // presented as time since the Unix epoch. That is not a degraded wall
326 // clock, it is a different quantity, and the only thing standing
327 // between a build and it was whether some crate in the graph happened
328 // to name `std`.
329 if let Some(t) = platform_wall_clock() {
330 return t;
331 }
332 Time::from_nanos(atomic_time::get_steady())
333 }
334
335 /// The monotonic clock, in ONE place — issue 1334's sibling site.
336 ///
337 /// Same rule as `Self::wall_now`, one clock over: a linked port IS the
338 /// monotonic clock, and the counter is what a build with no port was
339 /// given.
340 ///
341 /// This overturns half of phase-359 W10, which moved `SystemTime` onto the
342 /// port and left this line reading "`SteadyTime` is the counter on every
343 /// flavour, deliberately: it is advanced by its owner … and the port's
344 /// monotonic export is the executor's business, not this type's". The
345 /// owner never arrived — nothing outside this file's own tests has ever
346 /// called `update_steady_time` — so on every shipped image
347 /// `Clock::steady().now()` was the constant 0, while the C surface
348 /// answered the SAME question (`RCL_STEADY_TIME`) with
349 /// `nros_platform_clock_ns`. That is issue 1334's complaint exactly, one
350 /// clock over: two surfaces, one image, different answers, and the Rust
351 /// one is not a clock. W10's own rule settles it — when a port is linked
352 /// it IS the clock — and the counter keeps its documented RTIC meaning in
353 /// the one build where it is the only source.
354 fn steady_now() -> Time {
355 if let Some(t) = platform_steady_clock() {
356 return t;
357 }
358 Time::from_nanos(atomic_time::get_steady())
359 }
360
361 /// Get the current time from this clock
362 ///
363 /// # Platform behavior
364 ///
365 /// - **A platform port linked** (`platform-clock`): the port's clocks —
366 /// `nros_platform_time_now_ns` for the wall, `nros_platform_clock_ns`
367 /// for the monotonic. The port is the authority, and an image must not
368 /// hold two answers to "what time is it".
369 /// - **No port**: the internal counter, which the caller advances with
370 /// `update_steady_time()`.
371 ///
372 /// phase-359 W10 — there is no `std::time` arm. The platform API IS the
373 /// clock: a build with a port reads it, a build without one has no clock to
374 /// read and says so with the counter it was given. `SystemTime` used to sit
375 /// here as a third answer for hosted builds, which made "what time is it"
376 /// depend on whether some crate in the graph happened to name `std`.
377 ///
378 /// `RosTime` with no override is `SystemTime`, in EVERY build shape and by
379 /// construction — both arms call `Self::wall_now`. That is rclcpp's
380 /// contract (`ClockType::ROS_TIME` with `use_sim_time` false reads the
381 /// system clock), what five doc sites across three languages already
382 /// promised, and what the C surface already did; issue 1334 is the four
383 /// phases in which the Rust arm said something else.
384 pub fn now(&self) -> Time {
385 match self.clock_type {
386 ClockType::SystemTime => Self::wall_now(),
387 ClockType::SteadyTime => Self::steady_now(),
388 ClockType::RosTime => {
389 let override_nanos = atomic_time::get_ros_override();
390 if override_nanos >= 0 {
391 // A `/clock` source is driving simulated time.
392 Time::from_nanos(override_nanos)
393 } else {
394 // None is. rclcpp reads the system clock here and so do we
395 // — the same expression `Clock::system()` evaluates, not a
396 // copy of it (issue 1334).
397 Self::wall_now()
398 }
399 }
400 }
401 }
402
403 /// Set a ROS time override
404 ///
405 /// When set, all `RosTime` clocks will return this time instead of
406 /// system time. This is useful for simulation.
407 ///
408 /// # Arguments
409 /// * `nanos` - Nanoseconds since epoch
410 pub fn set_ros_time_override(nanos: i64) {
411 atomic_time::set_ros_override(nanos);
412 }
413
414 /// Set a ROS time override from a Time value
415 pub fn set_ros_time_override_time(time: Time) {
416 Self::set_ros_time_override(time.to_nanos());
417 }
418
419 /// Clear the ROS time override
420 ///
421 /// After clearing, `RosTime` clocks will return system time again.
422 pub fn clear_ros_time_override() {
423 atomic_time::set_ros_override(-1);
424 }
425
426 /// Check if a ROS time override is active
427 pub fn is_ros_time_override_active() -> bool {
428 atomic_time::get_ros_override() >= 0
429 }
430
431 /// Get the current ROS time override value (if active)
432 pub fn get_ros_time_override() -> Option<Time> {
433 let nanos = atomic_time::get_ros_override();
434 if nanos >= 0 {
435 Some(Time::from_nanos(nanos))
436 } else {
437 None
438 }
439 }
440
441 /// Update the steady time counter
442 ///
443 /// For environments with NO platform port, call this periodically from
444 /// your main loop or RTIC task to advance the clocks. With a port linked
445 /// this counter is not what any clock reads — the port is (issue 1334) —
446 /// and advancing it has no observable effect.
447 ///
448 /// # Arguments
449 /// * `delta_nanos` - Nanoseconds elapsed since last call
450 pub fn update_steady_time(delta_nanos: i64) {
451 atomic_time::add_steady(delta_nanos);
452 }
453
454 /// Update the steady time counter (milliseconds version)
455 ///
456 /// Convenience method for RTIC tasks using millisecond intervals.
457 ///
458 /// # Arguments
459 /// * `delta_ms` - Milliseconds elapsed since last call
460 pub fn update_steady_time_ms(delta_ms: u64) {
461 let delta_nanos = delta_ms as i64 * 1_000_000;
462 Self::update_steady_time(delta_nanos);
463 }
464
465 /// Set the steady time counter to a specific value
466 ///
467 /// Use this to initialize the clock or synchronize with an external
468 /// time source.
469 pub fn set_steady_time(nanos: i64) {
470 atomic_time::set_steady(nanos);
471 }
472
473 /// Get the current steady time counter value
474 pub fn get_steady_time_nanos() -> i64 {
475 atomic_time::get_steady()
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 /// Serialises every test that touches the process-global clock state —
484 /// issue 1334.
485 ///
486 /// The override, the counter and (below) the fake port are all statics, and
487 /// `cargo test` runs these in parallel threads: a test that SETS one and
488 /// reads it back is racing every sibling that writes the same static. That
489 /// was already true before this guard existed (`test_ros_time_override` and
490 /// `test_steady_time_update` write two different globals that `Clock::now`
491 /// reads together) and it becomes load-bearing here, because the new tests
492 /// move the fake port's hands while another test asserts where they point.
493 ///
494 /// A spin lock rather than `std::sync::Mutex`: this crate is `#![no_std]`
495 /// and `std` is a FEATURE, so a `Mutex` here would confine the guard — and
496 /// therefore every test using it — to the `std` flavour, which is not where
497 /// the `platform-clock` tests need to live.
498 struct ClockGuard;
499
500 static CLOCK_LOCK: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
501
502 impl ClockGuard {
503 fn acquire() -> Self {
504 while CLOCK_LOCK
505 .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
506 .is_err()
507 {
508 core::hint::spin_loop();
509 }
510 Self
511 }
512 }
513
514 impl Drop for ClockGuard {
515 fn drop(&mut self) {
516 // Leave the globals as a fresh process has them, so a test that
517 // does NOT take the guard still sees the documented defaults.
518 Clock::clear_ros_time_override();
519 Clock::set_steady_time(0);
520 #[cfg(feature = "platform-clock")]
521 fake_port::reset();
522 CLOCK_LOCK.store(false, Ordering::Release);
523 }
524 }
525
526 /// The stand-in platform port for the `platform-clock` flavour.
527 ///
528 /// One definition of each symbol, at module level, because a linker sees
529 /// one test binary: the wall symbol used to be defined INSIDE the single
530 /// test that needed it, which works exactly until a second test needs it
531 /// too. Both hands MOVE (they are atomics, not constants), which is what
532 /// lets a test assert that a clock ADVANCES rather than merely that it
533 /// reads a magic number.
534 #[cfg(feature = "platform-clock")]
535 mod fake_port {
536 use core::sync::atomic::{AtomicU64, Ordering};
537
538 /// 2001-09-09T01:46:40Z — a fixed instant no real clock will return.
539 pub(super) const WALL_BASE_NS: u64 = 1_000_000_000 * 1_000_000_000;
540 /// 42 s of uptime — no relation to `WALL_BASE_NS`, deliberately: the
541 /// two clocks are different quantities and a test must not be able to
542 /// pass by reading the wrong one.
543 pub(super) const STEADY_BASE_NS: u64 = 42_000_000_000;
544
545 pub(super) static WALL_NS: AtomicU64 = AtomicU64::new(WALL_BASE_NS);
546 pub(super) static STEADY_NS: AtomicU64 = AtomicU64::new(STEADY_BASE_NS);
547
548 pub(super) fn reset() {
549 WALL_NS.store(WALL_BASE_NS, Ordering::Relaxed);
550 STEADY_NS.store(STEADY_BASE_NS, Ordering::Relaxed);
551 }
552
553 #[unsafe(no_mangle)]
554 extern "C" fn nros_platform_time_now_ns() -> u64 {
555 WALL_NS.load(Ordering::Relaxed)
556 }
557
558 #[unsafe(no_mangle)]
559 extern "C" fn nros_platform_clock_ns() -> u64 {
560 STEADY_NS.load(Ordering::Relaxed)
561 }
562 }
563
564 #[test]
565 fn test_clock_type_default() {
566 let clock_type = ClockType::default();
567 assert_eq!(clock_type, ClockType::SystemTime);
568 }
569
570 #[test]
571 fn test_clock_constructors() {
572 let system = Clock::system();
573 assert_eq!(system.clock_type(), ClockType::SystemTime);
574
575 let steady = Clock::steady();
576 assert_eq!(steady.clock_type(), ClockType::SteadyTime);
577
578 let ros = Clock::ros_time();
579 assert_eq!(ros.clock_type(), ClockType::RosTime);
580
581 let custom = Clock::new(ClockType::SteadyTime);
582 assert_eq!(custom.clock_type(), ClockType::SteadyTime);
583 }
584
585 #[test]
586 fn test_clock_default() {
587 let clock = Clock::default();
588 assert_eq!(clock.clock_type(), ClockType::SystemTime);
589 }
590
591 #[test]
592 fn test_ros_time_override() {
593 let _guard = ClockGuard::acquire();
594 // Clear any existing override
595 Clock::clear_ros_time_override();
596 assert!(!Clock::is_ros_time_override_active());
597 assert!(Clock::get_ros_time_override().is_none());
598
599 // Set override
600 let override_time = Time::new(1234567890, 123456789);
601 Clock::set_ros_time_override_time(override_time);
602 assert!(Clock::is_ros_time_override_active());
603 assert_eq!(Clock::get_ros_time_override(), Some(override_time));
604
605 // ROS clock should return override time
606 let ros_clock = Clock::ros_time();
607 let now = ros_clock.now();
608 assert_eq!(now, override_time);
609
610 // Clear override
611 Clock::clear_ros_time_override();
612 assert!(!Clock::is_ros_time_override_active());
613 }
614
615 #[test]
616 fn test_steady_time_update() {
617 let _guard = ClockGuard::acquire();
618 // Reset steady time
619 Clock::set_steady_time(0);
620 assert_eq!(Clock::get_steady_time_nanos(), 0);
621
622 // Update by milliseconds
623 Clock::update_steady_time_ms(100);
624 assert_eq!(Clock::get_steady_time_nanos(), 100_000_000);
625
626 // Update by nanoseconds
627 Clock::update_steady_time(500_000_000);
628 assert_eq!(Clock::get_steady_time_nanos(), 600_000_000);
629
630 // Steady clock should return updated time -- in the build where the
631 // counter is the only clock there is. With a port linked the port
632 // outranks it (issue 1334), which is the test below.
633 #[cfg(not(feature = "platform-clock"))]
634 {
635 let steady_clock = Clock::steady();
636 let now = steady_clock.now();
637 assert_eq!(now.to_nanos(), 600_000_000);
638 }
639 }
640
641 /// With NO port linked there is no clock, and all three types say so with
642 /// the one counter they were given — issue 1334.
643 ///
644 /// This replaces `test_system_clock_returns_nonzero`, which asserted
645 /// `now.sec > 0` for `Clock::system()` in exactly this shape. That claim
646 /// was false from the day phase-359 W10 removed the `std::time` arm — a
647 /// portless build reads the counter, which is 0 — and nobody saw it,
648 /// because its `all(std, not(platform-clock))` gate put it in no lane at
649 /// all: the one nros-core feature lane is `std,platform-clock`, and the
650 /// default lane has no `std`. A test in no lane is the shape CLAUDE.md's
651 /// `required-features` rule names, one cfg over.
652 ///
653 /// What it asserts instead is the property that IS true here, and it is the
654 /// portless half of this issue's fix: `RosTime` with no override reads
655 /// whatever `SystemTime` reads, whatever that turns out to be.
656 #[test]
657 #[cfg(not(feature = "platform-clock"))]
658 fn without_a_port_every_clock_is_the_counter() {
659 let _guard = ClockGuard::acquire();
660 Clock::clear_ros_time_override();
661 Clock::set_steady_time(7_500_000_000);
662
663 assert_eq!(Clock::system().now().to_nanos(), 7_500_000_000);
664 assert_eq!(Clock::steady().now().to_nanos(), 7_500_000_000);
665 assert_eq!(
666 Clock::ros_time().now().to_nanos(),
667 7_500_000_000,
668 "with no override and no port, ROS time is what system time is"
669 );
670 }
671
672 /// phase-359 W10 follow-up — a linked port OUTRANKS `std` for the wall
673 /// clock, on a `std` build too.
674 ///
675 /// This is the one configuration where the two disagree observably, and it
676 /// is why the test image defines the port symbol itself (`fake_port`): the
677 /// value returned is nothing like a real `SystemTime`, so a
678 /// `Clock::system()` that answered with `SystemTime::now()` — which is what
679 /// this file did before, because `platform_wall_clock` was gated
680 /// `not(std)` — fails loudly rather than coincidentally passing. On POSIX
681 /// both sources are CLOCK_REALTIME, so nothing short of a port with an
682 /// opinion can tell them apart.
683 #[test]
684 #[cfg(all(feature = "std", feature = "platform-clock"))]
685 fn platform_port_outranks_std_for_the_wall_clock() {
686 let _guard = ClockGuard::acquire();
687 let now = Clock::system().now();
688 assert_eq!(
689 now.sec, 1_000_000_000,
690 "Clock::system() must read the linked port, not SystemTime"
691 );
692 assert_eq!(now.nanosec, 0);
693 }
694
695 /// ROS time with no `/clock` source is the WALL clock, and it RUNS — issue
696 /// 1334, and the negative control for the whole fix.
697 ///
698 /// Before the fix this arm read `STEADY_TIME_NANOS`, a counter whose own
699 /// doc says it is "advanced by its owner" and which nothing outside this
700 /// file's tests has ever advanced. So the assertion that fails on the
701 /// pre-fix tree is the first one (the reading was 0, not the port's wall
702 /// clock) and the one after it (a clock that does not move is not a
703 /// clock).
704 ///
705 /// `rclcpp`'s `ClockType::ROS_TIME` with `use_sim_time` false reads the
706 /// system clock; five doc sites in this tree across Rust, C and C++ said we
707 /// did too; and the C surface (`rclc_clock_get_now_time(RCL_ROS_TIME)`)
708 /// always did. This is the arm that disagreed with all of them.
709 #[test]
710 #[cfg(feature = "platform-clock")]
711 fn ros_time_with_no_override_reads_the_port_wall_clock() {
712 let _guard = ClockGuard::acquire();
713 Clock::clear_ros_time_override();
714 // A counter reading that is nothing like the wall clock, so reading the
715 // wrong source cannot pass by coincidence.
716 Clock::set_steady_time(1_234);
717
718 let ros = Clock::ros_time().now();
719 assert_eq!(
720 ros,
721 Clock::system().now(),
722 "with no override, ROS time IS system time"
723 );
724 assert_eq!(ros.sec, 1_000_000_000, "it must be the PORT's wall clock");
725
726 // ...and it advances with it. Two seconds on the port's hand.
727 fake_port::WALL_NS.fetch_add(2_000_000_000, core::sync::atomic::Ordering::Relaxed);
728 let later = Clock::ros_time().now();
729 assert_eq!(
730 later.to_nanos() - ros.to_nanos(),
731 2_000_000_000,
732 "ROS time with no source must ADVANCE with the wall clock"
733 );
734 }
735
736 /// A `/clock` override still wins over the wall clock — the sim-time path
737 /// this fix must not touch.
738 #[test]
739 #[cfg(feature = "platform-clock")]
740 fn an_override_still_outranks_the_wall_clock() {
741 let _guard = ClockGuard::acquire();
742 Clock::set_ros_time_override(5_000_000_000);
743 assert_eq!(Clock::ros_time().now().to_nanos(), 5_000_000_000);
744 assert_ne!(
745 Clock::ros_time().now(),
746 Clock::system().now(),
747 "a simulated clock must not be shadowed by the wall clock"
748 );
749 Clock::clear_ros_time_override();
750 assert_eq!(Clock::ros_time().now(), Clock::system().now());
751 }
752
753 /// The sibling site: a linked port outranks the counter for the MONOTONIC
754 /// clock too — issue 1334's sweep.
755 ///
756 /// Fails on the pre-fix tree, where `SteadyTime` read the counter on every
757 /// flavour and therefore returned 0 in every shipped image, while the C
758 /// surface answered `RCL_STEADY_TIME` with `nros_platform_clock_ns`.
759 #[test]
760 #[cfg(feature = "platform-clock")]
761 fn platform_port_outranks_the_counter_for_the_steady_clock() {
762 let _guard = ClockGuard::acquire();
763 Clock::set_steady_time(1_234);
764
765 let now = Clock::steady().now();
766 assert_eq!(
767 now.to_nanos(),
768 fake_port::STEADY_BASE_NS as i64,
769 "Clock::steady() must read the linked port, not the counter"
770 );
771 assert_ne!(
772 now.to_nanos(),
773 Clock::system().now().to_nanos(),
774 "the monotonic clock is a different quantity from the wall clock"
775 );
776
777 fake_port::STEADY_NS.fetch_add(250_000_000, core::sync::atomic::Ordering::Relaxed);
778 assert_eq!(
779 Clock::steady().now().to_nanos() - now.to_nanos(),
780 250_000_000
781 );
782 }
783}