Skip to main content

nros_node/
timer.rs

1//! Timer API for nros
2//!
3//! This module provides timer support matching rclrs patterns while maintaining
4//! `no_std` compatibility for embedded systems.
5//!
6//! # Overview
7//!
8//! Timers allow scheduling periodic or one-shot callbacks. In embedded environments
9//! without background threads, timers must be processed manually via `process_timers()`.
10//!
11//! # Timer Modes
12//!
13//! - **Repeating**: Fires at regular intervals until canceled
14//! - **OneShot**: Fires once after a delay, then becomes inert
15//! - **Inert**: Never fires, useful as a placeholder
16//!
17//! # Example (with std)
18//!
19//! ```ignore
20//! use nros::prelude::*;
21//! use nros::timer::Duration;
22//!
23//! let mut node = ConnectedNode::connect(config, locator)?;
24//!
25//! // Create a repeating timer
26//! let timer = node.create_timer_repeating(
27//!     Duration::from_millis(100),
28//!     || println!("Timer fired!"),
29//! )?;
30//!
31//! // Process timers periodically
32//! loop {
33//!     node.process_timers(10); // 10ms elapsed
34//!     std::thread::sleep(core::time::Duration::from_millis(10));
35//! }
36//! ```
37//!
38//! # Example (RTIC)
39//!
40//! ```ignore
41//! // In RTIC, use a periodic task to process timers
42//! #[task(priority = 2, shared = [node])]
43//! async fn timer_process(mut cx: timer_process::Context) {
44//!     loop {
45//!         cx.shared.node.lock(|node| {
46//!             node.process_timers(TIMER_PROCESS_INTERVAL_MS as u64);
47//!         });
48//!         Systick::delay(TIMER_PROCESS_INTERVAL_MS.millis()).await;
49//!     }
50//! }
51//! ```
52
53use core::marker::PhantomData;
54
55/// Duration type for timer periods
56///
57/// This is a simple millisecond-based duration for `no_std` compatibility.
58/// It can be converted to/from the ROS Duration type.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
60pub struct TimerDuration {
61    /// Duration in microseconds.
62    ///
63    /// Issue #505: this was milliseconds, so `from_micros` silently
64    /// truncated — `from_micros(500)` produced a ZERO period (which the
65    /// dispatcher treats as "every spin") and `from_micros(1_500)` a
66    /// 1 ms one, a 50% rate error with no diagnostic. The executor's
67    /// timer accounting is microsecond-based, so store microseconds and
68    /// keep the millisecond constructors/accessors as exact wrappers.
69    micros: u64,
70}
71
72impl TimerDuration {
73    /// Create a new duration from milliseconds
74    pub const fn from_millis(millis: u64) -> Self {
75        Self {
76            micros: millis.saturating_mul(1000),
77        }
78    }
79
80    /// Create a new duration from seconds
81    pub const fn from_secs(secs: u64) -> Self {
82        Self {
83            micros: secs.saturating_mul(1_000_000),
84        }
85    }
86
87    /// Create a new duration from microseconds
88    pub const fn from_micros(micros: u64) -> Self {
89        Self { micros }
90    }
91
92    /// Create a zero duration
93    pub const fn zero() -> Self {
94        Self { micros: 0 }
95    }
96
97    /// Get duration as milliseconds (truncated; prefer
98    /// [`Self::as_micros`] for sub-millisecond periods)
99    pub const fn as_millis(&self) -> u64 {
100        self.micros / 1000
101    }
102
103    /// Get duration as microseconds
104    pub const fn as_micros(&self) -> u64 {
105        self.micros
106    }
107
108    /// Get duration as seconds (truncated)
109    pub const fn as_secs(&self) -> u64 {
110        self.micros / 1_000_000
111    }
112
113    /// Check if duration is zero
114    pub const fn is_zero(&self) -> bool {
115        self.micros == 0
116    }
117
118    /// Saturating subtraction
119    pub const fn saturating_sub(self, rhs: Self) -> Self {
120        Self {
121            micros: self.micros.saturating_sub(rhs.micros),
122        }
123    }
124}
125
126impl From<nros_core::Duration> for TimerDuration {
127    fn from(d: nros_core::Duration) -> Self {
128        let micros = (d.sec as i64 * 1_000_000 + d.nanosec as i64 / 1_000) as u64;
129        Self { micros }
130    }
131}
132
133impl From<TimerDuration> for nros_core::Duration {
134    fn from(d: TimerDuration) -> Self {
135        let sec = (d.micros / 1_000_000) as i32;
136        let nanosec = ((d.micros % 1_000_000) * 1_000) as u32;
137        nros_core::Duration { sec, nanosec }
138    }
139}
140
141/// Timer mode (repeating, one-shot, or inert)
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum TimerMode {
144    /// Timer fires repeatedly at the specified period
145    Repeating,
146    /// Timer fires once then becomes inert
147    OneShot,
148    /// Timer never fires (placeholder)
149    Inert,
150}
151
152/// Timer callback as a bare function pointer (`no_std`, no heap required).
153pub type TimerCallbackFn = fn();
154
155/// Internal timer state
156///
157/// Stored in the node's timer collection.
158pub struct TimerState {
159    /// Timer period in milliseconds
160    period_ms: u64,
161    /// Time elapsed since last fire in milliseconds
162    elapsed_ms: u64,
163    /// Timer mode
164    mode: TimerMode,
165    /// Whether the timer is canceled
166    canceled: bool,
167    /// Callback function pointer (no heap)
168    callback_fn: Option<TimerCallbackFn>,
169}
170
171impl core::fmt::Debug for TimerState {
172    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
173        f.debug_struct("TimerState")
174            .field("period_ms", &self.period_ms)
175            .field("elapsed_ms", &self.elapsed_ms)
176            .field("mode", &self.mode)
177            .field("canceled", &self.canceled)
178            .field("has_callback_fn", &self.callback_fn.is_some())
179            .finish()
180    }
181}
182
183impl TimerState {
184    /// Create a new timer state with function pointer callback
185    pub fn new_with_fn(period: TimerDuration, mode: TimerMode, callback: TimerCallbackFn) -> Self {
186        Self {
187            period_ms: period.as_millis(),
188            elapsed_ms: 0,
189            mode,
190            canceled: false,
191            callback_fn: Some(callback),
192        }
193    }
194
195    /// Create an inert timer state
196    pub fn new_inert(period: TimerDuration) -> Self {
197        Self {
198            period_ms: period.as_millis(),
199            elapsed_ms: 0,
200            mode: TimerMode::Inert,
201            canceled: false,
202            callback_fn: None,
203        }
204    }
205
206    /// Get the timer period
207    pub fn period(&self) -> TimerDuration {
208        TimerDuration::from_millis(self.period_ms)
209    }
210
211    /// Get the timer mode
212    pub fn mode(&self) -> TimerMode {
213        self.mode
214    }
215
216    /// Check if the timer is canceled
217    pub fn is_canceled(&self) -> bool {
218        self.canceled
219    }
220
221    /// Cancel the timer
222    pub fn cancel(&mut self) {
223        self.canceled = true;
224    }
225
226    /// Reset the timer (uncancels and resets elapsed time)
227    pub fn reset(&mut self) {
228        self.canceled = false;
229        self.elapsed_ms = 0;
230    }
231
232    /// Check if the timer is ready to fire
233    pub fn is_ready(&self) -> bool {
234        !self.canceled && self.mode != TimerMode::Inert && self.elapsed_ms >= self.period_ms
235    }
236
237    /// Get time until next call (0 if ready)
238    pub fn time_until_next_call(&self) -> TimerDuration {
239        if self.canceled || self.mode == TimerMode::Inert {
240            return TimerDuration::from_millis(u64::MAX);
241        }
242        if self.elapsed_ms >= self.period_ms {
243            TimerDuration::zero()
244        } else {
245            TimerDuration::from_millis(self.period_ms - self.elapsed_ms)
246        }
247    }
248
249    /// Get time since last call
250    pub fn time_since_last_call(&self) -> TimerDuration {
251        TimerDuration::from_millis(self.elapsed_ms)
252    }
253
254    /// Set callback to function pointer
255    pub fn set_callback_fn(&mut self, callback: TimerCallbackFn) {
256        self.callback_fn = Some(callback);
257    }
258
259    /// Set timer to repeating mode
260    pub fn set_repeating(&mut self) {
261        self.mode = TimerMode::Repeating;
262    }
263
264    /// Set timer to one-shot mode
265    pub fn set_oneshot(&mut self) {
266        self.mode = TimerMode::OneShot;
267    }
268
269    /// Set timer to inert mode
270    pub fn set_inert(&mut self) {
271        self.mode = TimerMode::Inert;
272    }
273
274    /// Update elapsed time and return true if timer should fire
275    #[allow(dead_code)] // Used by ConnectedNode when zenoh feature is enabled
276    pub(crate) fn update(&mut self, delta_ms: u64) -> bool {
277        if self.canceled || self.mode == TimerMode::Inert {
278            return false;
279        }
280
281        self.elapsed_ms = self.elapsed_ms.saturating_add(delta_ms);
282
283        self.elapsed_ms >= self.period_ms
284    }
285
286    /// Fire the timer callback and handle mode-specific behavior
287    #[allow(dead_code)] // Used by ConnectedNode when zenoh feature is enabled
288    pub(crate) fn fire(&mut self) {
289        // Execute callback
290        if let Some(ref callback) = self.callback_fn {
291            callback();
292        }
293
294        // Handle mode-specific behavior
295        match self.mode {
296            TimerMode::Repeating => {
297                // Reset elapsed time for next period
298                self.elapsed_ms = self.elapsed_ms.saturating_sub(self.period_ms);
299            }
300            TimerMode::OneShot => {
301                // Become inert after firing
302                self.mode = TimerMode::Inert;
303                self.elapsed_ms = 0;
304            }
305            TimerMode::Inert => {
306                // Should not reach here
307            }
308        }
309    }
310}
311
312/// A handle to a timer stored in a node
313///
314/// This is a lightweight handle that references a timer by index.
315/// The actual timer state is stored in the `ConnectedNode`.
316///
317/// # Type Parameters
318///
319/// - `C`: Callback type marker (function pointer or boxed)
320#[derive(Debug, Clone, Copy)]
321pub struct TimerHandle<C = TimerCallbackFn> {
322    /// Timer index in the node's timer collection
323    index: usize,
324    /// Phantom data for callback type
325    _marker: PhantomData<C>,
326}
327
328impl<C> TimerHandle<C> {
329    /// Create a new timer handle
330    #[allow(dead_code)] // Used by ConnectedNode when zenoh feature is enabled
331    pub(crate) fn new(index: usize) -> Self {
332        Self {
333            index,
334            _marker: PhantomData,
335        }
336    }
337
338    /// Get the timer index
339    pub fn index(&self) -> usize {
340        self.index
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn test_timer_duration() {
350        let d = TimerDuration::from_millis(1500);
351        assert_eq!(d.as_millis(), 1500);
352        assert_eq!(d.as_secs(), 1);
353
354        let d2 = TimerDuration::from_secs(2);
355        assert_eq!(d2.as_millis(), 2000);
356
357        let d3 = TimerDuration::from_micros(5500);
358        assert_eq!(d3.as_millis(), 5);
359    }
360
361    #[test]
362    fn test_timer_duration_conversion() {
363        let ros_dur = nros_core::Duration::from_millis(1500);
364        let timer_dur: TimerDuration = ros_dur.into();
365        assert_eq!(timer_dur.as_millis(), 1500);
366
367        let back: nros_core::Duration = timer_dur.into();
368        assert_eq!(back.sec, 1);
369        assert_eq!(back.nanosec, 500_000_000);
370    }
371
372    fn test_callback() {
373        // Empty callback for testing
374    }
375
376    #[test]
377    fn test_timer_state_repeating() {
378        let mut state = TimerState::new_with_fn(
379            TimerDuration::from_millis(100),
380            TimerMode::Repeating,
381            test_callback,
382        );
383
384        assert_eq!(state.period().as_millis(), 100);
385        assert_eq!(state.mode(), TimerMode::Repeating);
386        assert!(!state.is_canceled());
387        assert!(!state.is_ready());
388
389        // Advance time
390        assert!(!state.update(50));
391        assert!(!state.is_ready());
392        assert_eq!(state.time_until_next_call().as_millis(), 50);
393        assert_eq!(state.time_since_last_call().as_millis(), 50);
394
395        // Advance to ready
396        assert!(state.update(50));
397        assert!(state.is_ready());
398        assert_eq!(state.time_until_next_call().as_millis(), 0);
399
400        // Fire and check it repeats
401        state.fire();
402        assert_eq!(state.mode(), TimerMode::Repeating);
403        assert!(!state.is_ready());
404    }
405
406    #[test]
407    fn test_timer_state_oneshot() {
408        let mut state = TimerState::new_with_fn(
409            TimerDuration::from_millis(100),
410            TimerMode::OneShot,
411            test_callback,
412        );
413
414        assert_eq!(state.mode(), TimerMode::OneShot);
415
416        // Advance to ready
417        state.update(100);
418        assert!(state.is_ready());
419
420        // Fire and check it becomes inert
421        state.fire();
422        assert_eq!(state.mode(), TimerMode::Inert);
423        assert!(!state.is_ready());
424    }
425
426    #[test]
427    fn test_timer_state_inert() {
428        let state = TimerState::new_inert(TimerDuration::from_millis(100));
429
430        assert_eq!(state.mode(), TimerMode::Inert);
431        assert!(!state.is_ready());
432    }
433
434    #[test]
435    fn test_timer_cancel_reset() {
436        let mut state = TimerState::new_with_fn(
437            TimerDuration::from_millis(100),
438            TimerMode::Repeating,
439            test_callback,
440        );
441
442        state.update(50);
443        state.cancel();
444        assert!(state.is_canceled());
445        assert!(!state.is_ready());
446
447        state.update(100);
448        assert!(!state.is_ready()); // Still canceled
449
450        state.reset();
451        assert!(!state.is_canceled());
452        assert_eq!(state.time_since_last_call().as_millis(), 0);
453    }
454
455    #[test]
456    fn test_timer_mode_changes() {
457        let mut state = TimerState::new_with_fn(
458            TimerDuration::from_millis(100),
459            TimerMode::Repeating,
460            test_callback,
461        );
462
463        state.set_oneshot();
464        assert_eq!(state.mode(), TimerMode::OneShot);
465
466        state.set_inert();
467        assert_eq!(state.mode(), TimerMode::Inert);
468
469        state.set_repeating();
470        assert_eq!(state.mode(), TimerMode::Repeating);
471    }
472
473    #[test]
474    fn test_timer_handle() {
475        let handle: TimerHandle = TimerHandle::new(5);
476        assert_eq!(handle.index(), 5);
477    }
478}
479
480// =============================================================================
481// Ghost model validation
482// =============================================================================
483
484#[cfg(test)]
485mod ghost_checks {
486    use super::*;
487    use nros_ghost_types::{TimerGhost, TimerModeGhost};
488
489    /// Structural check: map TimerMode to TimerModeGhost.
490    /// If a variant is added or removed, this fails to compile.
491    fn ghost_mode(m: &TimerMode) -> TimerModeGhost {
492        match m {
493            TimerMode::Repeating => TimerModeGhost::Repeating,
494            TimerMode::OneShot => TimerModeGhost::OneShot,
495            TimerMode::Inert => TimerModeGhost::Inert,
496        }
497    }
498
499    /// Structural check: construct TimerGhost from TimerState private fields.
500    /// If a field is renamed or retyped, this fails to compile.
501    fn ghost_from_timer(t: &TimerState) -> TimerGhost {
502        TimerGhost {
503            period_ms: t.period_ms,
504            elapsed_ms: t.elapsed_ms,
505            mode: ghost_mode(&t.mode),
506            canceled: t.canceled,
507        }
508    }
509
510    fn test_callback() {}
511
512    #[test]
513    fn ghost_new_state() {
514        let state = TimerState::new_with_fn(
515            TimerDuration::from_millis(100),
516            TimerMode::Repeating,
517            test_callback,
518        );
519        let ghost = ghost_from_timer(&state);
520        assert_eq!(ghost.period_ms, 100);
521        assert_eq!(ghost.elapsed_ms, 0);
522        assert_eq!(ghost.mode, TimerModeGhost::Repeating);
523        assert!(!ghost.canceled);
524    }
525
526    #[test]
527    fn ghost_update_accumulates() {
528        let mut state = TimerState::new_with_fn(
529            TimerDuration::from_millis(100),
530            TimerMode::Repeating,
531            test_callback,
532        );
533        state.update(30);
534        let ghost = ghost_from_timer(&state);
535        assert_eq!(ghost.elapsed_ms, 30);
536
537        state.update(25);
538        let ghost2 = ghost_from_timer(&state);
539        assert_eq!(ghost2.elapsed_ms, 55);
540    }
541
542    #[test]
543    fn ghost_canceled_no_fire() {
544        let mut state = TimerState::new_with_fn(
545            TimerDuration::from_millis(100),
546            TimerMode::Repeating,
547            test_callback,
548        );
549        state.cancel();
550        let fired = state.update(200);
551        assert!(!fired);
552        let ghost = ghost_from_timer(&state);
553        assert!(ghost.canceled);
554    }
555
556    #[test]
557    fn ghost_inert_no_fire() {
558        let mut state = TimerState::new_inert(TimerDuration::from_millis(100));
559        let fired = state.update(200);
560        assert!(!fired);
561        let ghost = ghost_from_timer(&state);
562        assert_eq!(ghost.mode, TimerModeGhost::Inert);
563    }
564
565    #[test]
566    fn ghost_oneshot_becomes_inert() {
567        let mut state = TimerState::new_with_fn(
568            TimerDuration::from_millis(100),
569            TimerMode::OneShot,
570            test_callback,
571        );
572        state.update(100);
573        state.fire();
574        let ghost = ghost_from_timer(&state);
575        assert_eq!(ghost.mode, TimerModeGhost::Inert);
576    }
577}