Skip to main content

nros_core/
time.rs

1//! ROS time types.
2//!
3//! Provides [`Time`] (seconds + nanoseconds) and [`Duration`] with
4//! arithmetic, conversions, and CDR serialization. All types are `no_std`
5//! compatible and use integer arithmetic only (no `f64::floor`/`f64::abs`).
6
7use core::ops::{Add, Sub};
8use nros_serdes::{CdrReader, CdrWriter, DeserError, Deserialize, SerError, Serialize};
9
10/// Nanoseconds per second
11const NANOS_PER_SEC: i64 = 1_000_000_000;
12
13/// Split a float seconds value into (sec, nanosec) without using `f64::floor()`/`f64::abs()`
14/// which require `std`. Uses integer truncation with correction for negative values.
15fn split_secs_f64(secs: f64) -> (i32, u32) {
16    let truncated = secs as i32;
17    // `as i32` truncates toward zero; floor rounds toward negative infinity.
18    // Correct for negative values with a fractional part.
19    let sec = if (truncated as f64) > secs {
20        truncated - 1
21    } else {
22        truncated
23    };
24    let frac = secs - sec as f64;
25    // frac should be in [0, 1), but guard against float imprecision
26    let frac = if frac < 0.0 { -frac } else { frac };
27    let nanosec = (frac * NANOS_PER_SEC as f64) as u32;
28    (sec, nanosec)
29}
30
31/// A generated message whose field set is `{ sec: i32, nanosec: u32 }` —
32/// `builtin_interfaces/msg/Time` and its `Duration` twin.
33///
34/// **This is the Rust substitute for a C++ template parameter.** `nros-cpp`
35/// spells the same conversion as
36/// `template <typename TimeMsgT> void Time::to_msg(TimeMsgT&)`
37/// (`packages/api/nros-cpp/include/rclcpp/time.hpp`), which binds structurally to
38/// anything carrying `sec` / `nanosec`. Rust has no structural bound, so the same
39/// trick needs an AUTHORED trait that `rosidl-codegen` implements for every
40/// generated message with that exact field set — the same binding set the C++
41/// template has, decided by SHAPE rather than by a hard-coded package name.
42///
43/// Why the trait lives here and the impl does not: `nros-core` can never name a
44/// message type. Every generated message crate depends on `nros-core`, so a
45/// dependency the other way is a cycle, and message types are generated per USER
46/// package (RFC-0023 / RFC-0067) — there is no one crate for the client library
47/// to name. The trait is the seam that lets the conversion cross that edge
48/// without an edge.
49///
50/// Implemented by codegen; nothing in this crate implements it.
51pub trait SecNanosecMsg: Sized {
52    /// Build the message from an already-decomposed timestamp.
53    ///
54    /// Infallible by construction — both components are already the message's
55    /// own field types. See [`Time::to_ros_msg`] for why no `Result` appears
56    /// here.
57    fn from_sec_nanosec(sec: i32, nanosec: u32) -> Self;
58}
59
60/// ROS Time representation
61///
62/// Matches `builtin_interfaces/msg/Time`:
63/// - `sec`: Seconds since epoch (signed for pre-epoch times)
64/// - `nanosec`: Nanoseconds within the second (0-999999999)
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, PartialOrd, Ord)]
66pub struct Time {
67    /// Seconds component
68    pub sec: i32,
69    /// Nanoseconds component (0-999999999)
70    pub nanosec: u32,
71}
72
73impl Time {
74    /// Create a new Time
75    pub const fn new(sec: i32, nanosec: u32) -> Self {
76        Self { sec, nanosec }
77    }
78
79    /// Create a Time representing zero
80    pub const fn zero() -> Self {
81        Self { sec: 0, nanosec: 0 }
82    }
83
84    /// Check if this time is zero
85    pub const fn is_zero(&self) -> bool {
86        self.sec == 0 && self.nanosec == 0
87    }
88
89    /// Create a Time from nanoseconds since epoch
90    pub const fn from_nanos(nanos: i64) -> Self {
91        let sec = (nanos / NANOS_PER_SEC) as i32;
92        let nanosec = (nanos % NANOS_PER_SEC) as u32;
93        Self { sec, nanosec }
94    }
95
96    /// Convert to total nanoseconds since epoch
97    pub const fn to_nanos(&self) -> i64 {
98        (self.sec as i64) * NANOS_PER_SEC + (self.nanosec as i64)
99    }
100
101    /// Create a Time from a Duration (treating duration as time since epoch)
102    pub const fn from_duration(d: Duration) -> Self {
103        Self {
104            sec: d.sec,
105            nanosec: d.nanosec,
106        }
107    }
108
109    /// Convert to Duration (treating time as duration since epoch)
110    pub const fn as_duration(&self) -> Duration {
111        Duration {
112            sec: self.sec,
113            nanosec: self.nanosec,
114        }
115    }
116
117    /// Create a Time from seconds (float)
118    pub fn from_secs_f64(secs: f64) -> Self {
119        let (sec, nanosec) = split_secs_f64(secs);
120        Self { sec, nanosec }
121    }
122
123    /// Convert to seconds (float)
124    pub fn to_secs_f64(&self) -> f64 {
125        self.sec as f64 + (self.nanosec as f64 / NANOS_PER_SEC as f64)
126    }
127
128    /// Stamp a generated `builtin_interfaces/msg/Time` — the single most common
129    /// thing a publisher does.
130    ///
131    /// ```ignore
132    /// msg.header.stamp = node.now().to_ros_msg();
133    /// ```
134    ///
135    /// The return type is inferred from the call site; any generated message
136    /// with the `{ sec, nanosec }` field set satisfies [`SecNanosecMsg`], which
137    /// is the same binding set the C++ `Time::to_msg` template has.
138    ///
139    /// **INFALLIBLE, deliberately.** `rclrs::Time::to_ros_msg` returns
140    /// `Result<builtin_interfaces::msg::Time, TryFromIntError>` because its
141    /// `Time` holds `i64` nanoseconds and narrowing to `sec: i32` can overflow.
142    /// Ours ALREADY IS `{ sec: i32, nanosec: u32 }`, so this is two field copies
143    /// and cannot fail. Wrapping it in a `Result` so a ported `t.to_ros_msg()?`
144    /// keeps compiling would hide a difference instead of making the compiler
145    /// point at it — the inverse of RFC-0089's rule. A ported `?` fails to
146    /// compile, locally, with one character to fix.
147    pub fn to_ros_msg<M: SecNanosecMsg>(&self) -> M {
148        M::from_sec_nanosec(self.sec, self.nanosec)
149    }
150}
151
152impl Add<Duration> for Time {
153    type Output = Time;
154
155    fn add(self, rhs: Duration) -> Self::Output {
156        let total_nanos = self.to_nanos() + rhs.to_nanos();
157        Time::from_nanos(total_nanos)
158    }
159}
160
161impl Sub<Duration> for Time {
162    type Output = Time;
163
164    fn sub(self, rhs: Duration) -> Self::Output {
165        let total_nanos = self.to_nanos() - rhs.to_nanos();
166        Time::from_nanos(total_nanos)
167    }
168}
169
170impl Sub<Time> for Time {
171    type Output = Duration;
172
173    fn sub(self, rhs: Time) -> Self::Output {
174        let diff_nanos = self.to_nanos() - rhs.to_nanos();
175        Duration::from_nanos(diff_nanos)
176    }
177}
178
179impl Serialize for Time {
180    fn serialize(&self, writer: &mut CdrWriter) -> Result<(), SerError> {
181        writer.write_i32(self.sec)?;
182        writer.write_u32(self.nanosec)?;
183        Ok(())
184    }
185}
186
187impl Deserialize for Time {
188    fn deserialize(reader: &mut CdrReader) -> Result<Self, DeserError> {
189        Ok(Self {
190            sec: reader.read_i32()?,
191            nanosec: reader.read_u32()?,
192        })
193    }
194}
195
196/// ROS Duration representation
197///
198/// Matches `builtin_interfaces/msg/Duration`:
199/// - `sec`: Seconds (signed for negative durations)
200/// - `nanosec`: Nanoseconds (0-999999999)
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, PartialOrd, Ord)]
202pub struct Duration {
203    /// Seconds component
204    pub sec: i32,
205    /// Nanoseconds component (0-999999999)
206    pub nanosec: u32,
207}
208
209impl Duration {
210    /// Create a new Duration
211    pub const fn new(sec: i32, nanosec: u32) -> Self {
212        Self { sec, nanosec }
213    }
214
215    /// Create a Duration representing zero
216    pub const fn zero() -> Self {
217        Self { sec: 0, nanosec: 0 }
218    }
219
220    /// Check if this duration is zero
221    pub const fn is_zero(&self) -> bool {
222        self.sec == 0 && self.nanosec == 0
223    }
224
225    /// Create a Duration from seconds
226    pub const fn from_secs(secs: i32) -> Self {
227        Self {
228            sec: secs,
229            nanosec: 0,
230        }
231    }
232
233    /// Create a Duration from milliseconds
234    pub const fn from_millis(millis: i64) -> Self {
235        let sec = (millis / 1000) as i32;
236        let nanosec = ((millis % 1000) * 1_000_000) as u32;
237        Self { sec, nanosec }
238    }
239
240    /// Create a Duration from nanoseconds
241    pub const fn from_nanos(nanos: i64) -> Self {
242        let sec = (nanos / NANOS_PER_SEC) as i32;
243        let nanosec = (nanos % NANOS_PER_SEC).unsigned_abs() as u32;
244        Self { sec, nanosec }
245    }
246
247    /// Convert to total nanoseconds
248    pub const fn to_nanos(&self) -> i64 {
249        (self.sec as i64) * NANOS_PER_SEC + (self.nanosec as i64)
250    }
251
252    /// Create a Duration from seconds (float)
253    pub fn from_secs_f64(secs: f64) -> Self {
254        let (sec, nanosec) = split_secs_f64(secs);
255        Self { sec, nanosec }
256    }
257
258    /// Convert to seconds (float)
259    pub fn to_secs_f64(&self) -> f64 {
260        self.sec as f64 + (self.nanosec as f64 / NANOS_PER_SEC as f64)
261    }
262}
263
264impl Add for Duration {
265    type Output = Duration;
266
267    fn add(self, rhs: Duration) -> Self::Output {
268        let total_nanos = self.to_nanos() + rhs.to_nanos();
269        Duration::from_nanos(total_nanos)
270    }
271}
272
273impl Sub for Duration {
274    type Output = Duration;
275
276    fn sub(self, rhs: Duration) -> Self::Output {
277        let diff_nanos = self.to_nanos() - rhs.to_nanos();
278        Duration::from_nanos(diff_nanos)
279    }
280}
281
282impl Serialize for Duration {
283    fn serialize(&self, writer: &mut CdrWriter) -> Result<(), SerError> {
284        writer.write_i32(self.sec)?;
285        writer.write_u32(self.nanosec)?;
286        Ok(())
287    }
288}
289
290impl Deserialize for Duration {
291    fn deserialize(reader: &mut CdrReader) -> Result<Self, DeserError> {
292        Ok(Self {
293            sec: reader.read_i32()?,
294            nanosec: reader.read_u32()?,
295        })
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    /// Stands in for a generated `builtin_interfaces/msg/Time`. This crate
304    /// cannot name the real one — that edge is the whole reason
305    /// [`SecNanosecMsg`] exists — so the trait's contract is checked here and
306    /// the CODEGEN side of it is checked where a generated crate is available
307    /// (`packages/testing/nros-tests/tests/time_to_ros_msg.rs`).
308    #[derive(Debug, PartialEq)]
309    struct FakeTimeMsg {
310        sec: i32,
311        nanosec: u32,
312    }
313
314    impl SecNanosecMsg for FakeTimeMsg {
315        fn from_sec_nanosec(sec: i32, nanosec: u32) -> Self {
316            Self { sec, nanosec }
317        }
318    }
319
320    #[test]
321    fn to_ros_msg_copies_both_fields_and_cannot_fail() {
322        let t = Time::new(1_234_567_890, 123_456_789);
323        // No `?`, no `.unwrap()`: the return type IS the message. Upstream's
324        // `Result` is upstream's `i64` narrowing, which ours does not do.
325        let msg: FakeTimeMsg = t.to_ros_msg();
326        assert_eq!(
327            msg,
328            FakeTimeMsg {
329                sec: 1_234_567_890,
330                nanosec: 123_456_789
331            }
332        );
333    }
334
335    #[test]
336    fn to_ros_msg_carries_a_pre_epoch_stamp_unchanged() {
337        // `sec` is signed and `nanosec` is not; a pre-epoch stamp is two field
338        // copies like any other, with no renormalisation on the way out.
339        let t = Time::new(-5, 999_999_999);
340        let msg: FakeTimeMsg = t.to_ros_msg();
341        assert_eq!(msg.sec, -5);
342        assert_eq!(msg.nanosec, 999_999_999);
343    }
344
345    #[test]
346    fn test_time_roundtrip() {
347        let mut buf = [0u8; 16];
348        let time = Time::new(1234567890, 123456789);
349
350        let mut writer = CdrWriter::new(&mut buf);
351        time.serialize(&mut writer).unwrap();
352
353        let mut reader = CdrReader::new(&buf);
354        let result = Time::deserialize(&mut reader).unwrap();
355        assert_eq!(result, time);
356    }
357
358    #[test]
359    fn test_duration_roundtrip() {
360        let mut buf = [0u8; 16];
361        let duration = Duration::new(-10, 500000000);
362
363        let mut writer = CdrWriter::new(&mut buf);
364        duration.serialize(&mut writer).unwrap();
365
366        let mut reader = CdrReader::new(&buf);
367        let result = Duration::deserialize(&mut reader).unwrap();
368        assert_eq!(result, duration);
369    }
370
371    #[test]
372    fn test_time_from_nanos() {
373        let time = Time::from_nanos(1_500_000_000);
374        assert_eq!(time.sec, 1);
375        assert_eq!(time.nanosec, 500_000_000);
376    }
377
378    #[test]
379    fn test_time_to_nanos() {
380        let time = Time::new(2, 500_000_000);
381        assert_eq!(time.to_nanos(), 2_500_000_000);
382    }
383
384    #[test]
385    fn test_time_add_duration() {
386        let time = Time::new(10, 500_000_000);
387        let duration = Duration::new(5, 700_000_000);
388        let result = time + duration;
389        assert_eq!(result.sec, 16);
390        assert_eq!(result.nanosec, 200_000_000);
391    }
392
393    #[test]
394    fn test_time_sub_duration() {
395        let time = Time::new(10, 500_000_000);
396        let duration = Duration::new(3, 200_000_000);
397        let result = time - duration;
398        assert_eq!(result.sec, 7);
399        assert_eq!(result.nanosec, 300_000_000);
400    }
401
402    #[test]
403    fn test_time_sub_time() {
404        let t1 = Time::new(10, 500_000_000);
405        let t2 = Time::new(7, 200_000_000);
406        let result = t1 - t2;
407        assert_eq!(result.sec, 3);
408        assert_eq!(result.nanosec, 300_000_000);
409    }
410
411    #[test]
412    fn test_time_from_duration() {
413        let duration = Duration::new(5, 123456789);
414        let time = Time::from_duration(duration);
415        assert_eq!(time.sec, 5);
416        assert_eq!(time.nanosec, 123456789);
417    }
418
419    #[test]
420    fn test_time_as_duration() {
421        let time = Time::new(5, 123456789);
422        let duration = time.as_duration();
423        assert_eq!(duration.sec, 5);
424        assert_eq!(duration.nanosec, 123456789);
425    }
426
427    #[test]
428    fn test_time_ordering() {
429        let t1 = Time::new(10, 0);
430        let t2 = Time::new(10, 500_000_000);
431        let t3 = Time::new(11, 0);
432        assert!(t1 < t2);
433        assert!(t2 < t3);
434    }
435
436    #[test]
437    fn test_duration_from_nanos() {
438        let duration = Duration::from_nanos(2_500_000_000);
439        assert_eq!(duration.sec, 2);
440        assert_eq!(duration.nanosec, 500_000_000);
441    }
442
443    #[test]
444    fn test_duration_to_nanos() {
445        let duration = Duration::new(3, 250_000_000);
446        assert_eq!(duration.to_nanos(), 3_250_000_000);
447    }
448
449    #[test]
450    fn test_duration_add() {
451        let d1 = Duration::new(2, 500_000_000);
452        let d2 = Duration::new(1, 700_000_000);
453        let result = d1 + d2;
454        assert_eq!(result.sec, 4);
455        assert_eq!(result.nanosec, 200_000_000);
456    }
457
458    #[test]
459    fn test_duration_sub() {
460        let d1 = Duration::new(5, 500_000_000);
461        let d2 = Duration::new(2, 200_000_000);
462        let result = d1 - d2;
463        assert_eq!(result.sec, 3);
464        assert_eq!(result.nanosec, 300_000_000);
465    }
466
467    #[test]
468    fn test_duration_ordering() {
469        let d1 = Duration::new(1, 0);
470        let d2 = Duration::new(1, 500_000_000);
471        let d3 = Duration::new(2, 0);
472        assert!(d1 < d2);
473        assert!(d2 < d3);
474    }
475
476    #[test]
477    fn test_time_secs_f64() {
478        let time = Time::from_secs_f64(1.5);
479        assert_eq!(time.sec, 1);
480        assert_eq!(time.nanosec, 500_000_000);
481
482        let secs = time.to_secs_f64();
483        assert!((secs - 1.5).abs() < 0.000001);
484    }
485
486    #[test]
487    fn test_duration_secs_f64() {
488        let duration = Duration::from_secs_f64(2.25);
489        assert_eq!(duration.sec, 2);
490        assert_eq!(duration.nanosec, 250_000_000);
491
492        let secs = duration.to_secs_f64();
493        assert!((secs - 2.25).abs() < 0.000001);
494    }
495}
496
497// =============================================================================
498// Kani bounded model checking proofs
499// =============================================================================
500
501#[cfg(kani)]
502mod verification {
503    use super::*;
504
505    // ---- Duration ----
506
507    #[kani::proof]
508    fn duration_from_nanos_no_panic() {
509        let nanos: i64 = kani::any();
510        // Constrain to tractable range for CBMC (i64 div/mod is expensive)
511        kani::assume(nanos >= -10_000_000_000 && nanos <= 10_000_000_000);
512        let dur = Duration::from_nanos(nanos);
513        // nanosec must always be in valid range
514        assert!(dur.nanosec < NANOS_PER_SEC as u32);
515    }
516
517    #[kani::proof]
518    fn duration_roundtrip_nanos() {
519        let nanos: i64 = kani::any();
520        // Constrain to tractable range for CBMC (i64 div/mod is expensive)
521        // Still covers multi-second values to exercise the div/mod logic
522        kani::assume(nanos >= 0 && nanos <= 10_000_000_000);
523        let dur = Duration::from_nanos(nanos);
524        assert_eq!(dur.to_nanos(), nanos);
525    }
526
527    #[kani::proof]
528    fn duration_zero_is_zero() {
529        let dur = Duration::zero();
530        assert!(dur.is_zero());
531        assert_eq!(dur.to_nanos(), 0);
532    }
533
534    #[kani::proof]
535    fn duration_from_secs() {
536        let secs: i32 = kani::any();
537        let dur = Duration::from_secs(secs);
538        assert_eq!(dur.sec, secs);
539        assert_eq!(dur.nanosec, 0);
540    }
541
542    #[kani::proof]
543    #[kani::unwind(5)]
544    fn duration_serialize_roundtrip() {
545        let sec: i32 = kani::any();
546        let nanosec: u32 = kani::any();
547        kani::assume(nanosec < NANOS_PER_SEC as u32);
548        let dur = Duration::new(sec, nanosec);
549
550        let mut buf = [0u8; 16];
551        let len = {
552            let mut writer = CdrWriter::new(&mut buf);
553            dur.serialize(&mut writer).unwrap();
554            writer.position()
555        };
556
557        let mut reader = CdrReader::new(&buf[..len]);
558        let deserialized = Duration::deserialize(&mut reader).unwrap();
559        assert_eq!(dur, deserialized);
560    }
561
562    // ---- Time ----
563
564    #[kani::proof]
565    fn time_from_nanos_no_panic() {
566        let nanos: i64 = kani::any();
567        // Constrain to tractable range for CBMC (i64 div/mod is expensive)
568        // NOTE: Negative nanos cause nanosec wrapping (known bug — Time::from_nanos
569        // lacks .unsigned_abs() unlike Duration::from_nanos). Constrain to >= 0.
570        kani::assume(nanos >= 0 && nanos <= 10_000_000_000);
571        let time = Time::from_nanos(nanos);
572        // nanosec must always be in valid range
573        assert!(time.nanosec < NANOS_PER_SEC as u32);
574    }
575
576    #[kani::proof]
577    fn time_roundtrip_nanos() {
578        let nanos: i64 = kani::any();
579        // Constrain to tractable range for CBMC (i64 div/mod is expensive)
580        kani::assume(nanos >= 0 && nanos <= 10_000_000_000);
581        let time = Time::from_nanos(nanos);
582        assert_eq!(time.to_nanos(), nanos);
583    }
584
585    #[kani::proof]
586    fn time_zero_is_zero() {
587        let time = Time::zero();
588        assert!(time.is_zero());
589        assert_eq!(time.to_nanos(), 0);
590    }
591
592    #[kani::proof]
593    fn time_duration_conversion() {
594        let sec: i32 = kani::any();
595        let nanosec: u32 = kani::any();
596        kani::assume(nanosec < NANOS_PER_SEC as u32);
597        let time = Time::new(sec, nanosec);
598        let dur = time.as_duration();
599        assert_eq!(dur.sec, time.sec);
600        assert_eq!(dur.nanosec, time.nanosec);
601        let back = Time::from_duration(dur);
602        assert_eq!(back, time);
603    }
604
605    #[kani::proof]
606    #[kani::unwind(5)]
607    fn time_serialize_roundtrip() {
608        let sec: i32 = kani::any();
609        let nanosec: u32 = kani::any();
610        kani::assume(nanosec < NANOS_PER_SEC as u32);
611        let time = Time::new(sec, nanosec);
612
613        let mut buf = [0u8; 16];
614        let len = {
615            let mut writer = CdrWriter::new(&mut buf);
616            time.serialize(&mut writer).unwrap();
617            writer.position()
618        };
619
620        let mut reader = CdrReader::new(&buf[..len]);
621        let deserialized = Time::deserialize(&mut reader).unwrap();
622        assert_eq!(time, deserialized);
623    }
624}