Skip to main content

ph_curves/
tickless.rs

1//! Tickless scheduling helpers for monotonic curves.
2//!
3//! Instead of polling a curve at a fixed tick rate, the tickless scheduler
4//! computes the exact wall-clock deadline at which the *quantized* output
5//! value will next change.  This lets interrupt-driven firmware sleep between
6//! transitions, saving power and CPU cycles.
7
8use crate::MonotonicCurve;
9use crate::math::{Rounding, UnitValue, next_target_value, quantize};
10
11/// Repeat behaviour for a tickless schedule.
12#[derive(Copy, Clone, Debug, Eq, PartialEq)]
13pub enum RepeatMode {
14    /// Play once and stop.
15    Once,
16    /// Loop back to the start value after each cycle.
17    Repeat,
18    /// Reverse direction after each cycle (start→end, end→start, …).
19    PingPong,
20}
21
22/// A single output produced by the tickless scheduler.
23///
24/// Each deadline tells the caller what the current quantized output value is
25/// and when the *next* transition will occur, so the caller can set a timer
26/// and go to sleep.
27#[derive(Copy, Clone, Debug, Eq, PartialEq)]
28pub struct TicklessDeadline {
29    /// Wall-clock time (in milliseconds) at which the output will next change.
30    ///
31    /// Set a hardware timer or `sleep_until` to this value.
32    pub deadline_ms: u32,
33    /// The quantized output value that should be applied *now* (at the time
34    /// this deadline was computed).
35    pub current_val: u16,
36}
37
38/// A tickless schedule bound to a monotonic curve and segment parameters.
39///
40/// `C` is the curve type and `T` is the curve's normalised value type
41/// (e.g. `u8`). Use [`Tickless::tickless_schedule`] to construct one
42/// fluently, or build it directly with [`TicklessSchedule::new`].
43#[derive(Copy, Clone, Debug)]
44pub struct TicklessSchedule<C, T: UnitValue = u8> {
45    curve: C,
46    t0_ms: u32,
47    duration_ms: u32,
48    start_val: u16,
49    end_val: u16,
50    step: u16,
51    rounding: Rounding,
52    min_dt_ms: u32,
53    repeat: RepeatMode,
54    _marker: core::marker::PhantomData<T>,
55}
56
57impl<C, T> TicklessSchedule<C, T>
58where
59    C: MonotonicCurve<T, T>,
60    T: UnitValue,
61{
62    /// Create a new tickless schedule.
63    ///
64    /// # Parameters
65    ///
66    /// - `curve` — the monotonic curve that shapes the transition.
67    /// - `t0_ms` — wall-clock start time of the segment in milliseconds.
68    /// - `duration_ms` — total duration of the segment in milliseconds.
69    /// - `start_val` — raw output value at `t = 0` (before quantization).
70    /// - `end_val` — raw output value at `t = 1` (before quantization).
71    /// - `step` — quantization step size (clamped to a minimum of 1).
72    /// - `rounding` — how values are snapped to the quantization grid.
73    /// - `min_dt_ms` — minimum time between successive deadlines.  Useful for
74    ///   rate-limiting hardware updates.  Set to `0` for no limit.
75    pub fn new(
76        curve: C,
77        t0_ms: u32,
78        duration_ms: u32,
79        start_val: u16,
80        end_val: u16,
81        step: u16,
82        rounding: Rounding,
83        min_dt_ms: u32,
84    ) -> Self {
85        Self {
86            curve,
87            t0_ms,
88            duration_ms,
89            start_val,
90            end_val,
91            step: step.max(1),
92            rounding,
93            min_dt_ms,
94            repeat: RepeatMode::Once,
95            _marker: core::marker::PhantomData,
96        }
97    }
98
99    /// Set the repeat mode, consuming and returning `self` for chaining.
100    pub fn with_repeat(mut self, mode: RepeatMode) -> Self {
101        self.repeat = mode;
102        self
103    }
104
105    /// The end time of the current segment in milliseconds.
106    pub fn end_ms(&self) -> u32 {
107        self.t0_ms.saturating_add(self.duration_ms)
108    }
109
110    /// Compute the next deadline after `now_ms`.
111    ///
112    /// Returns the quantized output value that should be applied *now* and the
113    /// wall-clock time at which the next quantized transition will occur.
114    ///
115    /// When `now_ms` is at or past the end of the segment, the returned
116    /// deadline is clamped to the segment end and the final quantized value.
117    pub fn next_deadline(&self, now_ms: u32) -> TicklessDeadline {
118        let end_ms = self.end_ms();
119
120        let current_t = self.time_to_t(now_ms);
121        let w = self.curve.eval(current_t);
122        let raw_val = w.lerp_u16(self.start_val, self.end_val);
123        let current_val = quantize(raw_val, self.step, self.rounding);
124        let end_val_q = quantize(self.end_val, self.step, self.rounding);
125
126        if now_ms >= end_ms || current_val == end_val_q {
127            return TicklessDeadline {
128                deadline_ms: end_ms.max(now_ms),
129                current_val,
130            };
131        }
132
133        let increasing = self.end_val >= self.start_val;
134        let target_val = next_target_value(current_val, end_val_q, self.step, increasing);
135        let w_target = T::inv_lerp_u16(self.start_val, self.end_val, target_val);
136        let u_target = self.curve.inv(w_target);
137        let mut deadline_ms = self.t_to_time(u_target);
138
139        let min_deadline = now_ms.saturating_add(self.min_dt_ms);
140        if deadline_ms < min_deadline {
141            deadline_ms = min_deadline;
142        }
143        if deadline_ms > end_ms {
144            deadline_ms = end_ms;
145        }
146        if deadline_ms < now_ms {
147            deadline_ms = now_ms;
148        }
149
150        TicklessDeadline {
151            deadline_ms,
152            current_val,
153        }
154    }
155
156    /// Return an iterator that yields successive [`TicklessDeadline`] values
157    /// starting from `now_ms`, automatically advancing to each deadline.
158    ///
159    /// For [`RepeatMode::Once`] the iterator finishes when the segment ends.
160    /// For [`RepeatMode::Repeat`] and [`RepeatMode::PingPong`] it cycles
161    /// indefinitely.
162    pub fn iter(&self, now_ms: u32) -> TicklessIter<'_, C, T> {
163        TicklessIter {
164            schedule: self,
165            t0_ms: self.t0_ms,
166            start_val: self.start_val,
167            end_val: self.end_val,
168            now_ms,
169            done: false,
170        }
171    }
172
173    // -- private helpers --------------------------------------------------
174
175    fn time_to_t(&self, now_ms: u32) -> T {
176        if self.duration_ms == 0 || now_ms >= self.end_ms() {
177            return T::one();
178        }
179        if now_ms <= self.t0_ms {
180            return T::zero();
181        }
182        T::from_time_frac(now_ms - self.t0_ms, self.duration_ms)
183    }
184
185    fn t_to_time(&self, t: T) -> u32 {
186        self.t0_ms
187            .saturating_add(t.to_time_offset(self.duration_ms))
188    }
189}
190
191/// Iterator over successive [`TicklessDeadline`] values produced by a
192/// [`TicklessSchedule`].
193#[derive(Debug)]
194pub struct TicklessIter<'a, C, T: UnitValue = u8> {
195    schedule: &'a TicklessSchedule<C, T>,
196    t0_ms: u32,
197    start_val: u16,
198    end_val: u16,
199    now_ms: u32,
200    done: bool,
201}
202
203impl<C, T> TicklessIter<'_, C, T>
204where
205    C: MonotonicCurve<T, T> + Copy,
206    T: UnitValue,
207{
208    /// Build a single-cycle schedule from the iterator's current state.
209    fn cycle_schedule(&self) -> TicklessSchedule<C, T> {
210        TicklessSchedule {
211            curve: self.schedule.curve,
212            t0_ms: self.t0_ms,
213            duration_ms: self.schedule.duration_ms,
214            start_val: self.start_val,
215            end_val: self.end_val,
216            step: self.schedule.step,
217            rounding: self.schedule.rounding,
218            min_dt_ms: self.schedule.min_dt_ms,
219            repeat: RepeatMode::Once,
220            _marker: core::marker::PhantomData,
221        }
222    }
223
224    /// Advance to the next cycle, returning `true` if the iterator continues.
225    fn advance_cycle(&mut self) -> bool {
226        match self.schedule.repeat {
227            RepeatMode::Once => false,
228            RepeatMode::Repeat => {
229                self.t0_ms = self.t0_ms.saturating_add(self.schedule.duration_ms);
230                true
231            }
232            RepeatMode::PingPong => {
233                self.t0_ms = self.t0_ms.saturating_add(self.schedule.duration_ms);
234                core::mem::swap(&mut self.start_val, &mut self.end_val);
235                true
236            }
237        }
238    }
239}
240
241impl<C, T> Iterator for TicklessIter<'_, C, T>
242where
243    C: MonotonicCurve<T, T> + Copy,
244    T: UnitValue,
245{
246    type Item = TicklessDeadline;
247
248    fn next(&mut self) -> Option<TicklessDeadline> {
249        if self.done {
250            return None;
251        }
252
253        let cycle = self.cycle_schedule();
254        let dl = cycle.next_deadline(self.now_ms);
255        let end_ms = cycle.end_ms();
256        let end_val_q = quantize(self.end_val, self.schedule.step, self.schedule.rounding);
257
258        let cycle_finished = dl.deadline_ms >= end_ms || dl.current_val == end_val_q;
259
260        if cycle_finished {
261            if !self.advance_cycle() {
262                self.done = true;
263            } else {
264                self.now_ms = end_ms;
265            }
266        } else {
267            self.now_ms = dl.deadline_ms;
268        }
269
270        Some(dl)
271    }
272}
273
274/// Extension trait that adds tickless scheduling to any
275/// [`MonotonicCurve<T, T>`] where `T: UnitValue`.
276///
277/// This is the primary entry point for building a [`TicklessSchedule`].
278/// It is automatically implemented for every type that satisfies the bounds.
279pub trait Tickless<T: UnitValue>: MonotonicCurve<T, T> + Sized + Copy {
280    /// Build a [`TicklessSchedule`] for this curve.
281    ///
282    /// See [`TicklessSchedule::new`] for parameter descriptions.
283    ///
284    /// # Example
285    ///
286    /// ```ignore
287    /// use ph_curves::{Tickless, Rounding};
288    ///
289    /// let schedule = curve.tickless_schedule(
290    ///     0,     // t0_ms: start time
291    ///     1000,  // duration_ms
292    ///     0,     // start_val
293    ///     255,   // end_val
294    ///     10,    // step (quantization)
295    ///     Rounding::Nearest,
296    ///     0,     // min_dt_ms
297    /// );
298    ///
299    /// for deadline in schedule.iter(0) {
300    ///     set_timer(deadline.deadline_ms);
301    ///     set_output(deadline.current_val);
302    /// }
303    /// ```
304    fn tickless_schedule(
305        self,
306        t0_ms: u32,
307        duration_ms: u32,
308        start_val: u16,
309        end_val: u16,
310        step: u16,
311        rounding: Rounding,
312        min_dt_ms: u32,
313    ) -> TicklessSchedule<Self, T> {
314        TicklessSchedule::new(
315            self,
316            t0_ms,
317            duration_ms,
318            start_val,
319            end_val,
320            step,
321            rounding,
322            min_dt_ms,
323        )
324    }
325}
326
327impl<C, T> Tickless<T> for C
328where
329    C: MonotonicCurve<T, T> + Copy,
330    T: UnitValue,
331{
332}