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 // phase-518 (issue 2071) -- a port with no RTC (FreeRTOS, ThreadX)
333 // answers 0, and the counter below is the constant 0 on every image
334 // with a port linked, because nothing advances it. So a `RosTime`
335 // timer with no `/clock` source never fired on those ports: the
336 // stock `rclcpp::create_timer(this, get_clock(), ...)` of every
337 // Autoware node was dead. Boot-relative time is what issue 0758
338 // already says those images stamp with, and it ADVANCES, which is the
339 // property a timer needs. A port that can state an epoch still wins.
340 if let Some(t) = platform_steady_clock() {
341 return t;
342 }
343 Time::from_nanos(atomic_time::get_steady())
344 }
345
346 /// The monotonic clock, in ONE place — issue 1334's sibling site.
347 ///
348 /// Same rule as `Self::wall_now`, one clock over: a linked port IS the
349 /// monotonic clock, and the counter is what a build with no port was
350 /// given.
351 ///
352 /// This overturns half of phase-359 W10, which moved `SystemTime` onto the
353 /// port and left this line reading "`SteadyTime` is the counter on every
354 /// flavour, deliberately: it is advanced by its owner … and the port's
355 /// monotonic export is the executor's business, not this type's". The
356 /// owner never arrived — nothing outside this file's own tests has ever
357 /// called `update_steady_time` — so on every shipped image
358 /// `Clock::steady().now()` was the constant 0, while the C surface
359 /// answered the SAME question (`RCL_STEADY_TIME`) with
360 /// `nros_platform_clock_ns`. That is issue 1334's complaint exactly, one
361 /// clock over: two surfaces, one image, different answers, and the Rust
362 /// one is not a clock. W10's own rule settles it — when a port is linked
363 /// it IS the clock — and the counter keeps its documented RTIC meaning in
364 /// the one build where it is the only source.
365 fn steady_now() -> Time {
366 if let Some(t) = platform_steady_clock() {
367 return t;
368 }
369 Time::from_nanos(atomic_time::get_steady())
370 }
371
372 /// Get the current time from this clock
373 ///
374 /// # Platform behavior
375 ///
376 /// - **A platform port linked** (`platform-clock`): the port's clocks —
377 /// `nros_platform_time_now_ns` for the wall, `nros_platform_clock_ns`
378 /// for the monotonic. The port is the authority, and an image must not
379 /// hold two answers to "what time is it".
380 /// - **No port**: the internal counter, which the caller advances with
381 /// `update_steady_time()`.
382 ///
383 /// phase-359 W10 — there is no `std::time` arm. The platform API IS the
384 /// clock: a build with a port reads it, a build without one has no clock to
385 /// read and says so with the counter it was given. `SystemTime` used to sit
386 /// here as a third answer for hosted builds, which made "what time is it"
387 /// depend on whether some crate in the graph happened to name `std`.
388 ///
389 /// `RosTime` with no override is `SystemTime`, in EVERY build shape and by
390 /// construction — both arms call `Self::wall_now`. That is rclcpp's
391 /// contract (`ClockType::ROS_TIME` with `use_sim_time` false reads the
392 /// system clock), what five doc sites across three languages already
393 /// promised, and what the C surface already did; issue 1334 is the four
394 /// phases in which the Rust arm said something else.
395 pub fn now(&self) -> Time {
396 match self.clock_type {
397 ClockType::SystemTime => Self::wall_now(),
398 ClockType::SteadyTime => Self::steady_now(),
399 ClockType::RosTime => {
400 let override_nanos = atomic_time::get_ros_override();
401 if override_nanos >= 0 {
402 // A `/clock` source is driving simulated time.
403 Time::from_nanos(override_nanos)
404 } else {
405 // None is. rclcpp reads the system clock here and so do we
406 // — the same expression `Clock::system()` evaluates, not a
407 // copy of it (issue 1334).
408 Self::wall_now()
409 }
410 }
411 }
412 }
413
414 /// Set a ROS time override
415 ///
416 /// When set, all `RosTime` clocks will return this time instead of
417 /// system time. This is useful for simulation.
418 ///
419 /// # Arguments
420 /// * `nanos` - Nanoseconds since epoch
421 pub fn set_ros_time_override(nanos: i64) {
422 atomic_time::set_ros_override(nanos);
423 }
424
425 /// Set a ROS time override from a Time value
426 pub fn set_ros_time_override_time(time: Time) {
427 Self::set_ros_time_override(time.to_nanos());
428 }
429
430 /// Clear the ROS time override
431 ///
432 /// After clearing, `RosTime` clocks will return system time again.
433 pub fn clear_ros_time_override() {
434 atomic_time::set_ros_override(-1);
435 }
436
437 /// Check if a ROS time override is active
438 pub fn is_ros_time_override_active() -> bool {
439 atomic_time::get_ros_override() >= 0
440 }
441
442 /// Get the current ROS time override value (if active)
443 pub fn get_ros_time_override() -> Option<Time> {
444 let nanos = atomic_time::get_ros_override();
445 if nanos >= 0 {
446 Some(Time::from_nanos(nanos))
447 } else {
448 None
449 }
450 }
451
452 /// Update the steady time counter
453 ///
454 /// For environments with NO platform port, call this periodically from
455 /// your main loop or RTIC task to advance the clocks. With a port linked
456 /// this counter is not what any clock reads — the port is (issue 1334) —
457 /// and advancing it has no observable effect.
458 ///
459 /// # Arguments
460 /// * `delta_nanos` - Nanoseconds elapsed since last call
461 pub fn update_steady_time(delta_nanos: i64) {
462 atomic_time::add_steady(delta_nanos);
463 }
464
465 /// Update the steady time counter (milliseconds version)
466 ///
467 /// Convenience method for RTIC tasks using millisecond intervals.
468 ///
469 /// # Arguments
470 /// * `delta_ms` - Milliseconds elapsed since last call
471 pub fn update_steady_time_ms(delta_ms: u64) {
472 let delta_nanos = delta_ms as i64 * 1_000_000;
473 Self::update_steady_time(delta_nanos);
474 }
475
476 /// Set the steady time counter to a specific value
477 ///
478 /// Use this to initialize the clock or synchronize with an external
479 /// time source.
480 pub fn set_steady_time(nanos: i64) {
481 atomic_time::set_steady(nanos);
482 }
483
484 /// Get the current steady time counter value
485 pub fn get_steady_time_nanos() -> i64 {
486 atomic_time::get_steady()
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493
494 /// Serialises every test that touches the process-global clock state —
495 /// issue 1334.
496 ///
497 /// The override, the counter and (below) the fake port are all statics, and
498 /// `cargo test` runs these in parallel threads: a test that SETS one and
499 /// reads it back is racing every sibling that writes the same static. That
500 /// was already true before this guard existed (`test_ros_time_override` and
501 /// `test_steady_time_update` write two different globals that `Clock::now`
502 /// reads together) and it becomes load-bearing here, because the new tests
503 /// move the fake port's hands while another test asserts where they point.
504 ///
505 /// A spin lock rather than `std::sync::Mutex`: this crate is `#![no_std]`
506 /// and `std` is a FEATURE, so a `Mutex` here would confine the guard — and
507 /// therefore every test using it — to the `std` flavour, which is not where
508 /// the `platform-clock` tests need to live.
509 struct ClockGuard;
510
511 static CLOCK_LOCK: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
512
513 impl ClockGuard {
514 fn acquire() -> Self {
515 while CLOCK_LOCK
516 .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
517 .is_err()
518 {
519 core::hint::spin_loop();
520 }
521 Self
522 }
523 }
524
525 impl Drop for ClockGuard {
526 fn drop(&mut self) {
527 // Leave the globals as a fresh process has them, so a test that
528 // does NOT take the guard still sees the documented defaults.
529 Clock::clear_ros_time_override();
530 Clock::set_steady_time(0);
531 #[cfg(feature = "platform-clock")]
532 fake_port::reset();
533 CLOCK_LOCK.store(false, Ordering::Release);
534 }
535 }
536
537 /// The stand-in platform port for the `platform-clock` flavour.
538 ///
539 /// One definition of each symbol, at module level, because a linker sees
540 /// one test binary: the wall symbol used to be defined INSIDE the single
541 /// test that needed it, which works exactly until a second test needs it
542 /// too. Both hands MOVE (they are atomics, not constants), which is what
543 /// lets a test assert that a clock ADVANCES rather than merely that it
544 /// reads a magic number.
545 #[cfg(feature = "platform-clock")]
546 mod fake_port {
547 use core::sync::atomic::{AtomicU64, Ordering};
548
549 /// 2001-09-09T01:46:40Z — a fixed instant no real clock will return.
550 pub(super) const WALL_BASE_NS: u64 = 1_000_000_000 * 1_000_000_000;
551 /// 42 s of uptime — no relation to `WALL_BASE_NS`, deliberately: the
552 /// two clocks are different quantities and a test must not be able to
553 /// pass by reading the wrong one.
554 pub(super) const STEADY_BASE_NS: u64 = 42_000_000_000;
555
556 pub(super) static WALL_NS: AtomicU64 = AtomicU64::new(WALL_BASE_NS);
557 pub(super) static STEADY_NS: AtomicU64 = AtomicU64::new(STEADY_BASE_NS);
558
559 pub(super) fn reset() {
560 WALL_NS.store(WALL_BASE_NS, Ordering::Relaxed);
561 STEADY_NS.store(STEADY_BASE_NS, Ordering::Relaxed);
562 }
563
564 #[unsafe(no_mangle)]
565 extern "C" fn nros_platform_time_now_ns() -> u64 {
566 WALL_NS.load(Ordering::Relaxed)
567 }
568
569 #[unsafe(no_mangle)]
570 extern "C" fn nros_platform_clock_ns() -> u64 {
571 STEADY_NS.load(Ordering::Relaxed)
572 }
573 }
574
575 #[test]
576 fn test_clock_type_default() {
577 let clock_type = ClockType::default();
578 assert_eq!(clock_type, ClockType::SystemTime);
579 }
580
581 #[test]
582 fn test_clock_constructors() {
583 let system = Clock::system();
584 assert_eq!(system.clock_type(), ClockType::SystemTime);
585
586 let steady = Clock::steady();
587 assert_eq!(steady.clock_type(), ClockType::SteadyTime);
588
589 let ros = Clock::ros_time();
590 assert_eq!(ros.clock_type(), ClockType::RosTime);
591
592 let custom = Clock::new(ClockType::SteadyTime);
593 assert_eq!(custom.clock_type(), ClockType::SteadyTime);
594 }
595
596 #[test]
597 fn test_clock_default() {
598 let clock = Clock::default();
599 assert_eq!(clock.clock_type(), ClockType::SystemTime);
600 }
601
602 #[test]
603 fn test_ros_time_override() {
604 let _guard = ClockGuard::acquire();
605 // Clear any existing override
606 Clock::clear_ros_time_override();
607 assert!(!Clock::is_ros_time_override_active());
608 assert!(Clock::get_ros_time_override().is_none());
609
610 // Set override
611 let override_time = Time::new(1234567890, 123456789);
612 Clock::set_ros_time_override_time(override_time);
613 assert!(Clock::is_ros_time_override_active());
614 assert_eq!(Clock::get_ros_time_override(), Some(override_time));
615
616 // ROS clock should return override time
617 let ros_clock = Clock::ros_time();
618 let now = ros_clock.now();
619 assert_eq!(now, override_time);
620
621 // Clear override
622 Clock::clear_ros_time_override();
623 assert!(!Clock::is_ros_time_override_active());
624 }
625
626 #[test]
627 fn test_steady_time_update() {
628 let _guard = ClockGuard::acquire();
629 // Reset steady time
630 Clock::set_steady_time(0);
631 assert_eq!(Clock::get_steady_time_nanos(), 0);
632
633 // Update by milliseconds
634 Clock::update_steady_time_ms(100);
635 assert_eq!(Clock::get_steady_time_nanos(), 100_000_000);
636
637 // Update by nanoseconds
638 Clock::update_steady_time(500_000_000);
639 assert_eq!(Clock::get_steady_time_nanos(), 600_000_000);
640
641 // Steady clock should return updated time -- in the build where the
642 // counter is the only clock there is. With a port linked the port
643 // outranks it (issue 1334), which is the test below.
644 #[cfg(not(feature = "platform-clock"))]
645 {
646 let steady_clock = Clock::steady();
647 let now = steady_clock.now();
648 assert_eq!(now.to_nanos(), 600_000_000);
649 }
650 }
651
652 /// With NO port linked there is no clock, and all three types say so with
653 /// the one counter they were given — issue 1334.
654 ///
655 /// This replaces ~~`test_system_clock_returns_nonzero`~~, which asserted
656 /// `now.sec > 0` for `Clock::system()` in exactly this shape. That claim
657 /// was false from the day phase-359 W10 removed the `std::time` arm — a
658 /// portless build reads the counter, which is 0 — and nobody saw it,
659 /// because its `all(std, not(platform-clock))` gate put it in no lane at
660 /// all: the one nros-core feature lane is `std,platform-clock`, and the
661 /// default lane has no `std`. A test in no lane is the shape CLAUDE.md's
662 /// `required-features` rule names, one cfg over.
663 ///
664 /// What it asserts instead is the property that IS true here, and it is the
665 /// portless half of this issue's fix: `RosTime` with no override reads
666 /// whatever `SystemTime` reads, whatever that turns out to be.
667 #[test]
668 #[cfg(not(feature = "platform-clock"))]
669 fn without_a_port_every_clock_is_the_counter() {
670 let _guard = ClockGuard::acquire();
671 Clock::clear_ros_time_override();
672 Clock::set_steady_time(7_500_000_000);
673
674 assert_eq!(Clock::system().now().to_nanos(), 7_500_000_000);
675 assert_eq!(Clock::steady().now().to_nanos(), 7_500_000_000);
676 assert_eq!(
677 Clock::ros_time().now().to_nanos(),
678 7_500_000_000,
679 "with no override and no port, ROS time is what system time is"
680 );
681 }
682
683 /// phase-359 W10 follow-up — a linked port OUTRANKS `std` for the wall
684 /// clock, on a `std` build too.
685 ///
686 /// This is the one configuration where the two disagree observably, and it
687 /// is why the test image defines the port symbol itself (`fake_port`): the
688 /// value returned is nothing like a real `SystemTime`, so a
689 /// `Clock::system()` that answered with `SystemTime::now()` — which is what
690 /// this file did before, because `platform_wall_clock` was gated
691 /// `not(std)` — fails loudly rather than coincidentally passing. On POSIX
692 /// both sources are CLOCK_REALTIME, so nothing short of a port with an
693 /// opinion can tell them apart.
694 #[test]
695 #[cfg(feature = "platform-clock")]
696 fn platform_port_outranks_std_for_the_wall_clock() {
697 let _guard = ClockGuard::acquire();
698 let now = Clock::system().now();
699 assert_eq!(
700 now.sec, 1_000_000_000,
701 "Clock::system() must read the linked port, not SystemTime"
702 );
703 assert_eq!(now.nanosec, 0);
704 }
705
706 /// ROS time with no `/clock` source is the WALL clock, and it RUNS — issue
707 /// 1334, and the negative control for the whole fix.
708 ///
709 /// Before the fix this arm read `STEADY_TIME_NANOS`, a counter whose own
710 /// doc says it is "advanced by its owner" and which nothing outside this
711 /// file's tests has ever advanced. So the assertion that fails on the
712 /// pre-fix tree is the first one (the reading was 0, not the port's wall
713 /// clock) and the one after it (a clock that does not move is not a
714 /// clock).
715 ///
716 /// `rclcpp`'s `ClockType::ROS_TIME` with `use_sim_time` false reads the
717 /// system clock; five doc sites in this tree across Rust, C and C++ said we
718 /// did too; and the C surface (`rclc_clock_get_now_time(RCL_ROS_TIME)`)
719 /// always did. This is the arm that disagreed with all of them.
720 #[test]
721 #[cfg(feature = "platform-clock")]
722 fn ros_time_with_no_override_reads_the_port_wall_clock() {
723 let _guard = ClockGuard::acquire();
724 Clock::clear_ros_time_override();
725 // A counter reading that is nothing like the wall clock, so reading the
726 // wrong source cannot pass by coincidence.
727 Clock::set_steady_time(1_234);
728
729 let ros = Clock::ros_time().now();
730 assert_eq!(
731 ros,
732 Clock::system().now(),
733 "with no override, ROS time IS system time"
734 );
735 assert_eq!(ros.sec, 1_000_000_000, "it must be the PORT's wall clock");
736
737 // ...and it advances with it. Two seconds on the port's hand.
738 fake_port::WALL_NS.fetch_add(2_000_000_000, core::sync::atomic::Ordering::Relaxed);
739 let later = Clock::ros_time().now();
740 assert_eq!(
741 later.to_nanos() - ros.to_nanos(),
742 2_000_000_000,
743 "ROS time with no source must ADVANCE with the wall clock"
744 );
745 }
746
747 /// A `/clock` override still wins over the wall clock — the sim-time path
748 /// this fix must not touch.
749 #[test]
750 #[cfg(feature = "platform-clock")]
751 fn an_override_still_outranks_the_wall_clock() {
752 let _guard = ClockGuard::acquire();
753 Clock::set_ros_time_override(5_000_000_000);
754 assert_eq!(Clock::ros_time().now().to_nanos(), 5_000_000_000);
755 assert_ne!(
756 Clock::ros_time().now(),
757 Clock::system().now(),
758 "a simulated clock must not be shadowed by the wall clock"
759 );
760 Clock::clear_ros_time_override();
761 assert_eq!(Clock::ros_time().now(), Clock::system().now());
762 }
763
764 /// The sibling site: a linked port outranks the counter for the MONOTONIC
765 /// clock too — issue 1334's sweep.
766 ///
767 /// Fails on the pre-fix tree, where `SteadyTime` read the counter on every
768 /// flavour and therefore returned 0 in every shipped image, while the C
769 /// surface answered `RCL_STEADY_TIME` with `nros_platform_clock_ns`.
770 #[test]
771 #[cfg(feature = "platform-clock")]
772 fn platform_port_outranks_the_counter_for_the_steady_clock() {
773 let _guard = ClockGuard::acquire();
774 Clock::set_steady_time(1_234);
775
776 let now = Clock::steady().now();
777 assert_eq!(
778 now.to_nanos(),
779 fake_port::STEADY_BASE_NS as i64,
780 "Clock::steady() must read the linked port, not the counter"
781 );
782 assert_ne!(
783 now.to_nanos(),
784 Clock::system().now().to_nanos(),
785 "the monotonic clock is a different quantity from the wall clock"
786 );
787
788 fake_port::STEADY_NS.fetch_add(250_000_000, core::sync::atomic::Ordering::Relaxed);
789 assert_eq!(
790 Clock::steady().now().to_nanos() - now.to_nanos(),
791 250_000_000
792 );
793 }
794}