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}