Skip to main content

embassy_interval/
lib.rs

1#![no_std]
2
3use embassy_time::{Duration, Instant, Timer};
4
5/// A periodic timer
6///
7/// ```rust
8/// use embassy_interval::{Interval, MissedTickBehavior};
9/// use embassy_time::Duration;
10///
11/// async fn task() -> ! {
12///     let mut interval = Interval::new(Duration::from_millis(1000), MissedTickBehavior::Burst);
13///
14///     loop {
15///         interval.tick().await;
16///
17///         println!("tick every 1000ms");
18///     }
19/// }
20/// ```
21pub struct Interval {
22    interval: Duration,
23    wake_time: Instant,
24    missed_tick_behavior: MissedTickBehavior,
25}
26
27/// Defines the behavior of an [`Interval`] when it misses a tick.
28///
29/// Generally, a tick is missed if too much time is spent without calling
30/// [`Interval::tick()`].
31///
32/// `MissedTickBehavior` is used to specify the behavior for
33/// [`Interval`] when execution time exceeds the set interval. Each variant represents a different
34/// strategy.
35///
36/// Note that the precision of these behaviors depend on the precision of the timer.
37///
38/// This enum is mostly copied from tokio
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40pub enum MissedTickBehavior {
41    /// Ticks as fast as possible until caught up.
42    ///
43    /// When this strategy is used, [`Interval`] schedules ticks "normally" (the
44    /// same as it would have if the ticks hadn't been delayed), which results
45    /// in it firing ticks as fast as possible until it is caught up in time to
46    /// where it should be. Unlike [`Delay`] and [`Skip`], the ticks yielded
47    /// when `Burst` is used (the [`Instant`]s that [`tick`](Interval::tick)
48    /// yields) aren't different than they would have been if a tick had not
49    /// been missed. Like [`Skip`], and unlike [`Delay`], the ticks may be
50    /// shortened.
51    ///
52    /// This looks something like this:
53    /// ```text
54    /// Expected ticks: |     1     |     2     |     3     |     4     |     5     |     6     |
55    /// Actual ticks:   | work -----|          delay          | work | work | work -| work -----|
56    /// ```
57    ///
58    /// This is the default behavior when [`Interval`] is created with
59    /// [`interval`] and [`interval_at`].
60    ///
61    /// [`Delay`]: MissedTickBehavior::Delay
62    /// [`Skip`]: MissedTickBehavior::Skip
63    #[default]
64    Burst,
65
66    /// Tick at multiples of `period` from when [`tick`] was called, rather than
67    /// from `start`.
68    ///
69    /// When this strategy is used and [`Interval`] has missed a tick, instead
70    /// of scheduling ticks to fire at multiples of `period` from `start` (the
71    /// time when the first tick was fired), it schedules all future ticks to
72    /// happen at a regular `period` from the point when [`tick`] was called.
73    /// Unlike [`Burst`] and [`Skip`], ticks are not shortened, and they aren't
74    /// guaranteed to happen at a multiple of `period` from `start` any longer.
75    ///
76    /// This looks something like this:
77    /// ```text
78    /// Expected ticks: |     1     |     2     |     3     |     4     |     5     |     6     |
79    /// Actual ticks:   | work -----|          delay          | work -----| work -----| work -----|
80    /// ```
81    ///
82    /// [`Burst`]: MissedTickBehavior::Burst
83    /// [`Skip`]: MissedTickBehavior::Skip
84    /// [`tick`]: Interval::tick
85    #[allow(unused)]
86    Delay,
87
88    /// Skips missed ticks and tick on the next multiple of `period` from
89    /// `start`.
90    ///
91    /// When this strategy is used, [`Interval`] schedules the next tick to fire
92    /// at the next-closest tick that is a multiple of `period` away from
93    /// `start` (the point where [`Interval`] first ticked). Like [`Burst`], all
94    /// ticks remain multiples of `period` away from `start`, but unlike
95    /// [`Burst`], the ticks may not be *one* multiple of `period` away from the
96    /// last tick. Like [`Delay`], the ticks are no longer the same as they
97    /// would have been if ticks had not been missed, but unlike [`Delay`], and
98    /// like [`Burst`], the ticks may be shortened to be less than one `period`
99    /// away from each other.
100    ///
101    /// This looks something like this:
102    /// ```text
103    /// Expected ticks: |     1     |     2     |     3     |     4     |     5     |     6     |
104    /// Actual ticks:   | work -----|          delay          | work ---| work -----| work -----|
105    /// ```
106    ///
107    /// [`Burst`]: MissedTickBehavior::Burst
108    /// [`Delay`]: MissedTickBehavior::Delay
109    #[allow(unused)]
110    Skip,
111}
112
113impl Interval {
114    /// Create a new interval timer with given `interval` and `missed_tick_behavior`
115    pub fn new(interval: Duration, missed_tick_behavior: MissedTickBehavior) -> Self {
116        let wake_time = Instant::now();
117        Self {
118            interval,
119            wake_time,
120            missed_tick_behavior,
121        }
122    }
123
124    /// Wait until the next tick
125    ///
126    /// ```rust
127    /// use embassy_interval::{Interval, MissedTickBehavior};
128    /// use embassy_time::Duration;
129    ///
130    /// async fn task() -> ! {
131    ///     let mut interval = Interval::new(Duration::from_millis(1000), MissedTickBehavior::Burst);
132    ///
133    ///     loop {
134    ///         interval.tick().await;
135    ///
136    ///         println!("tick every 1000ms");
137    ///     }
138    /// }
139    /// ```
140    pub fn tick(&mut self) -> Timer {
141        let now = Instant::now();
142
143        if now > self.wake_time {
144            self.wake_time =
145                self.missed_tick_behavior
146                    .next_timeout(self.wake_time, now, self.interval);
147        }
148
149        let timer = Timer::at(self.wake_time);
150
151        self.wake_time += self.interval;
152
153        timer
154    }
155}
156
157impl MissedTickBehavior {
158    fn next_timeout(&self, timeout: Instant, now: Instant, period: Duration) -> Instant {
159        match self {
160            Self::Burst => timeout + period,
161            Self::Delay => now + period,
162            Self::Skip => {
163                now + period - Duration::from_ticks((now - timeout).as_ticks() % period.as_ticks())
164            }
165        }
166    }
167}