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