embassy-interval 0.2.1

A simple third party interval timer for the embassy-time crate
Documentation
#![no_std]

use embassy_time::{Duration, Instant, Timer};

/// A periodic timer
///
/// ```rust
/// use embassy_interval::{Interval, MissedTickBehavior};
/// use embassy_time::Duration;
///
/// async fn task() -> ! {
///     let mut interval = Interval::new(Duration::from_millis(1000), MissedTickBehavior::Burst);
///
///     loop {
///         interval.tick().await;
///
///         println!("tick every 1000ms");
///     }
/// }
/// ```
pub struct Interval {
    interval: Duration,
    wake_time: Instant,
    missed_tick_behavior: MissedTickBehavior,
}

/// Defines the behavior of an [`Interval`] when it misses a tick.
///
/// Generally, a tick is missed if too much time is spent without calling
/// [`Interval::tick()`].
///
/// `MissedTickBehavior` is used to specify the behavior for
/// [`Interval`] when execution time exceeds the set interval. Each variant represents a different
/// strategy.
///
/// Note that the precision of these behaviors depend on the precision of the timer.
///
/// This enum is mostly copied from tokio
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MissedTickBehavior {
    /// Ticks as fast as possible until caught up.
    ///
    /// When this strategy is used, [`Interval`] schedules ticks "normally" (the
    /// same as it would have if the ticks hadn't been delayed), which results
    /// in it firing ticks as fast as possible until it is caught up in time to
    /// where it should be. Unlike [`Delay`] and [`Skip`], the ticks yielded
    /// when `Burst` is used (the [`Instant`]s that [`tick`](Interval::tick)
    /// yields) aren't different than they would have been if a tick had not
    /// been missed. Like [`Skip`], and unlike [`Delay`], the ticks may be
    /// shortened.
    ///
    /// This looks something like this:
    /// ```text
    /// Expected ticks: |     1     |     2     |     3     |     4     |     5     |     6     |
    /// Actual ticks:   | work -----|          delay          | work | work | work -| work -----|
    /// ```
    ///
    /// This is the default behavior when [`Interval`] is created with
    /// [`interval`] and [`interval_at`].
    ///
    /// [`Delay`]: MissedTickBehavior::Delay
    /// [`Skip`]: MissedTickBehavior::Skip
    #[default]
    Burst,

    /// Tick at multiples of `period` from when [`tick`] was called, rather than
    /// from `start`.
    ///
    /// When this strategy is used and [`Interval`] has missed a tick, instead
    /// of scheduling ticks to fire at multiples of `period` from `start` (the
    /// time when the first tick was fired), it schedules all future ticks to
    /// happen at a regular `period` from the point when [`tick`] was called.
    /// Unlike [`Burst`] and [`Skip`], ticks are not shortened, and they aren't
    /// guaranteed to happen at a multiple of `period` from `start` any longer.
    ///
    /// This looks something like this:
    /// ```text
    /// Expected ticks: |     1     |     2     |     3     |     4     |     5     |     6     |
    /// Actual ticks:   | work -----|          delay          | work -----| work -----| work -----|
    /// ```
    ///
    /// [`Burst`]: MissedTickBehavior::Burst
    /// [`Skip`]: MissedTickBehavior::Skip
    /// [`tick`]: Interval::tick
    #[allow(unused)]
    Delay,

    /// Skips missed ticks and tick on the next multiple of `period` from
    /// `start`.
    ///
    /// When this strategy is used, [`Interval`] schedules the next tick to fire
    /// at the next-closest tick that is a multiple of `period` away from
    /// `start` (the point where [`Interval`] first ticked). Like [`Burst`], all
    /// ticks remain multiples of `period` away from `start`, but unlike
    /// [`Burst`], the ticks may not be *one* multiple of `period` away from the
    /// last tick. Like [`Delay`], the ticks are no longer the same as they
    /// would have been if ticks had not been missed, but unlike [`Delay`], and
    /// like [`Burst`], the ticks may be shortened to be less than one `period`
    /// away from each other.
    ///
    /// This looks something like this:
    /// ```text
    /// Expected ticks: |     1     |     2     |     3     |     4     |     5     |     6     |
    /// Actual ticks:   | work -----|          delay          | work ---| work -----| work -----|
    /// ```
    ///
    /// [`Burst`]: MissedTickBehavior::Burst
    /// [`Delay`]: MissedTickBehavior::Delay
    #[allow(unused)]
    Skip,
}

impl Interval {
    /// Create a new interval timer with given `interval` and `missed_tick_behavior`
    pub fn new(interval: Duration, missed_tick_behavior: MissedTickBehavior) -> Self {
        let wake_time = Instant::now();
        Self {
            interval,
            wake_time,
            missed_tick_behavior,
        }
    }

    /// Wait until the next tick
    ///
    /// ```rust
    /// use embassy_interval::{Interval, MissedTickBehavior};
    /// use embassy_time::Duration;
    ///
    /// async fn task() -> ! {
    ///     let mut interval = Interval::new(Duration::from_millis(1000), MissedTickBehavior::Burst);
    ///
    ///     loop {
    ///         interval.tick().await;
    ///
    ///         println!("tick every 1000ms");
    ///     }
    /// }
    /// ```
    pub fn tick(&mut self) -> Timer {
        let now = Instant::now();

        if now > self.wake_time {
            self.wake_time =
                self.missed_tick_behavior
                    .next_timeout(self.wake_time, now, self.interval);
        }

        let timer = Timer::at(self.wake_time);

        self.wake_time += self.interval;

        timer
    }
}

impl MissedTickBehavior {
    fn next_timeout(&self, timeout: Instant, now: Instant, period: Duration) -> Instant {
        match self {
            Self::Burst => timeout + period,
            Self::Delay => now + period,
            Self::Skip => {
                now + period - Duration::from_ticks((now - timeout).as_ticks() % period.as_ticks())
            }
        }
    }
}