Skip to main content

rgb_sequencer/
sequencer.rs

1//! RGB LED sequencer with state management.
2
3use crate::BLACK;
4use crate::command::SequencerAction;
5use crate::sequence::RgbSequence;
6use crate::time::{TimeDuration, TimeInstant, TimeSource};
7use palette::Srgb;
8
9/// Trait for abstracting RGB LED hardware.
10pub trait RgbLed {
11    /// Sets LED to specified color.
12    ///
13    /// Color components are in 0.0-1.0 range. Convert to your hardware's native format
14    /// (PWM duty cycles, 8-bit values, etc.) in your implementation.
15    fn set_color(&mut self, color: Srgb);
16}
17
18/// RGB sequencer state.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[cfg_attr(feature = "defmt", derive(defmt::Format))]
21pub enum SequencerState {
22    /// No sequence loaded.
23    Idle,
24    /// Sequence loaded.
25    Loaded,
26    /// Sequence running.
27    Running,
28    /// Sequence paused.
29    Paused,
30    /// Sequence complete.
31    Complete,
32}
33
34/// Timing information returned by service operations.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36#[cfg_attr(feature = "defmt", derive(defmt::Format))]
37pub enum ServiceTiming<D> {
38    /// Continuous animation - service again at your target frame rate (e.g., 16-33ms for 30-60 FPS).
39    Continuous,
40    /// Static hold - can delay this duration before next service call.
41    Delay(D),
42    /// Sequence complete - no further servicing needed.
43    Complete,
44}
45
46/// Current playback position within a sequence.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48#[cfg_attr(feature = "defmt", derive(defmt::Format))]
49pub struct Position {
50    /// Current step index (0-based).
51    pub step_index: usize,
52    /// Current loop number (0-based).
53    pub loop_number: u32,
54}
55
56/// Errors that can occur during sequencer operations.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58#[cfg_attr(feature = "defmt", derive(defmt::Format))]
59pub enum SequencerError {
60    /// Invalid state.
61    InvalidState {
62        /// Expected state description.
63        expected: &'static str,
64        /// Actual current state.
65        actual: SequencerState,
66    },
67    /// No sequence loaded.
68    NoSequenceLoaded,
69}
70
71impl core::fmt::Display for SequencerError {
72    /// Formats the error for display.
73    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
74        match self {
75            SequencerError::InvalidState { expected, actual } => {
76                write!(
77                    f,
78                    "invalid state: expected {}, but sequencer is in {:?}",
79                    expected, actual
80                )
81            }
82            SequencerError::NoSequenceLoaded => {
83                write!(f, "no sequence loaded")
84            }
85        }
86    }
87}
88
89/// Controls a single RGB LED through sequences.
90pub struct RgbSequencer<'t, I: TimeInstant, L: RgbLed, T: TimeSource<I>, const N: usize> {
91    led: L,
92    time_source: &'t T,
93    state: SequencerState,
94    sequence: Option<RgbSequence<I::Duration, N>>,
95    start_time: Option<I>,
96    pause_start_time: Option<I>,
97    current_color: Srgb,
98    color_epsilon: f32,
99    brightness: f32,
100}
101
102/// Default epsilon for floating-point color comparisons.
103pub const DEFAULT_COLOR_EPSILON: f32 = 0.001;
104
105/// Returns true if two colors are approximately equal within the given epsilon.
106#[inline]
107fn colors_approximately_equal(a: Srgb, b: Srgb, epsilon: f32) -> bool {
108    (a.red - b.red).abs() < epsilon
109        && (a.green - b.green).abs() < epsilon
110        && (a.blue - b.blue).abs() < epsilon
111}
112
113impl<'t, I: TimeInstant, L: RgbLed, T: TimeSource<I>, const N: usize> RgbSequencer<'t, I, L, T, N> {
114    /// Creates sequencer with LED off and default color epsilon.
115    pub fn new(mut led: L, time_source: &'t T) -> Self {
116        led.set_color(BLACK);
117
118        Self {
119            led,
120            time_source,
121            state: SequencerState::Idle,
122            sequence: None,
123            start_time: None,
124            pause_start_time: None,
125            current_color: BLACK,
126            color_epsilon: DEFAULT_COLOR_EPSILON,
127            brightness: 1.0,
128        }
129    }
130
131    /// Creates sequencer with custom color epsilon threshold.
132    pub fn with_epsilon(mut led: L, time_source: &'t T, epsilon: f32) -> Self {
133        led.set_color(BLACK);
134
135        Self {
136            led,
137            time_source,
138            state: SequencerState::Idle,
139            sequence: None,
140            start_time: None,
141            pause_start_time: None,
142            current_color: BLACK,
143            color_epsilon: epsilon,
144            brightness: 1.0,
145        }
146    }
147
148    /// Dispatches action to appropriate method.
149    pub fn handle_action(
150        &mut self,
151        action: SequencerAction<I::Duration, N>,
152    ) -> Result<(), SequencerError> {
153        match action {
154            SequencerAction::Load(sequence) => {
155                self.load(sequence);
156                Ok(())
157            }
158            SequencerAction::Start => self.start(),
159            SequencerAction::Stop => self.stop(),
160            SequencerAction::Pause => self.pause(),
161            SequencerAction::Resume => self.resume(),
162            SequencerAction::Restart => self.restart(),
163            SequencerAction::Clear => {
164                self.clear();
165                Ok(())
166            }
167            SequencerAction::SetBrightness(brightness) => {
168                self.set_brightness(brightness);
169                Ok(())
170            }
171        }
172    }
173
174    /// Loads a sequence.
175    pub fn load(&mut self, sequence: RgbSequence<I::Duration, N>) {
176        self.sequence = Some(sequence);
177        self.start_time = None;
178        self.pause_start_time = None;
179        self.state = SequencerState::Loaded;
180    }
181
182    /// Starts sequence playback.
183    ///
184    /// Transitions from `Loaded` to `Running` state.
185    pub fn start(&mut self) -> Result<(), SequencerError> {
186        if self.state != SequencerState::Loaded {
187            return Err(SequencerError::InvalidState {
188                expected: "Loaded",
189                actual: self.state,
190            });
191        }
192
193        if self.sequence.is_none() {
194            return Err(SequencerError::NoSequenceLoaded);
195        }
196
197        self.start_time = Some(self.time_source.now());
198        self.state = SequencerState::Running;
199        Ok(())
200    }
201
202    /// Loads and immediately starts a sequence.
203    ///
204    /// Convenience method that combines `load()` and `start()`.
205    pub fn load_and_start(
206        &mut self,
207        sequence: RgbSequence<I::Duration, N>,
208    ) -> Result<(), SequencerError> {
209        self.load(sequence);
210        self.start()
211    }
212
213    /// Restarts sequence from beginning.
214    ///
215    /// Resets the start time and transitions to `Running` state.
216    pub fn restart(&mut self) -> Result<(), SequencerError> {
217        match self.state {
218            SequencerState::Running | SequencerState::Paused | SequencerState::Complete => {
219                if self.sequence.is_none() {
220                    return Err(SequencerError::NoSequenceLoaded);
221                }
222
223                self.start_time = Some(self.time_source.now());
224                self.pause_start_time = None;
225                self.state = SequencerState::Running;
226                Ok(())
227            }
228            _ => Err(SequencerError::InvalidState {
229                expected: "Running, Paused, or Complete",
230                actual: self.state,
231            }),
232        }
233    }
234
235    /// Services sequencer, updating LED if color changed.
236    ///
237    /// Must be called from `Running` state. Returns timing hint for next service call.
238    #[inline]
239    pub fn service(&mut self) -> Result<ServiceTiming<I::Duration>, SequencerError> {
240        if self.state != SequencerState::Running {
241            return Err(SequencerError::InvalidState {
242                expected: "Running",
243                actual: self.state,
244            });
245        }
246
247        let sequence = self.sequence.as_ref().unwrap();
248        let start_time = self.start_time.unwrap();
249        let current_time = self.time_source.now();
250        let elapsed = current_time.duration_since(start_time);
251
252        // Evaluate color and timing
253        let (new_color, next_service) = sequence.evaluate(elapsed);
254
255        // Apply brightness to the evaluated color
256        let dimmed_color = Srgb::new(
257            new_color.red * self.brightness,
258            new_color.green * self.brightness,
259            new_color.blue * self.brightness,
260        );
261
262        // Update LED only if color changed (using epsilon for f32 comparison).
263        // This avoids unnecessary hardware writes during static holds and prevents
264        // spurious updates from floating-point rounding (<0.1% difference).
265        // Particularly valuable for slow I2C/SPI LED drivers.
266        if !colors_approximately_equal(dimmed_color, self.current_color, self.color_epsilon) {
267            self.led.set_color(dimmed_color);
268            self.current_color = dimmed_color;
269        }
270
271        // Convert timing hint to ServiceTiming
272        match next_service {
273            None => {
274                self.state = SequencerState::Complete;
275                Ok(ServiceTiming::Complete)
276            }
277            Some(duration) if duration == I::Duration::ZERO => Ok(ServiceTiming::Continuous),
278            Some(duration) => Ok(ServiceTiming::Delay(duration)),
279        }
280    }
281
282    /// Peeks at next timing hint without updating LED or advancing state.
283    ///
284    /// Returns `SequencerError::InvalidState` if not in `Running` state.
285    #[inline]
286    pub fn peek_next_timing(&self) -> Result<ServiceTiming<I::Duration>, SequencerError> {
287        if self.state != SequencerState::Running {
288            return Err(SequencerError::InvalidState {
289                expected: "Running",
290                actual: self.state,
291            });
292        }
293
294        let sequence = self.sequence.as_ref().unwrap();
295        let start_time = self.start_time.unwrap();
296        let current_time = self.time_source.now();
297        let elapsed = current_time.duration_since(start_time);
298
299        // Evaluate timing without updating state
300        let (_color, next_service) = sequence.evaluate(elapsed);
301
302        // Convert timing hint to ServiceTiming
303        match next_service {
304            None => Ok(ServiceTiming::Complete),
305            Some(duration) if duration == I::Duration::ZERO => Ok(ServiceTiming::Continuous),
306            Some(duration) => Ok(ServiceTiming::Delay(duration)),
307        }
308    }
309
310    /// Stops sequence and turns LED off.
311    pub fn stop(&mut self) -> Result<(), SequencerError> {
312        match self.state {
313            SequencerState::Running | SequencerState::Paused | SequencerState::Complete => {
314                self.start_time = None;
315                self.pause_start_time = None;
316                self.state = SequencerState::Loaded;
317
318                self.led.set_color(BLACK);
319                self.current_color = BLACK;
320
321                Ok(())
322            }
323            _ => Err(SequencerError::InvalidState {
324                expected: "Running, Paused, or Complete",
325                actual: self.state,
326            }),
327        }
328    }
329
330    /// Pauses sequence at current color.
331    ///
332    /// Timing is compensated on resume - sequence continues from same position.
333    pub fn pause(&mut self) -> Result<(), SequencerError> {
334        if self.state != SequencerState::Running {
335            return Err(SequencerError::InvalidState {
336                expected: "Running",
337                actual: self.state,
338            });
339        }
340
341        self.pause_start_time = Some(self.time_source.now());
342        self.state = SequencerState::Paused;
343        Ok(())
344    }
345
346    /// Resumes paused sequence.
347    ///
348    /// Automatically compensates for the paused duration to maintain timing continuity.
349    pub fn resume(&mut self) -> Result<(), SequencerError> {
350        if self.state != SequencerState::Paused {
351            return Err(SequencerError::InvalidState {
352                expected: "Paused",
353                actual: self.state,
354            });
355        }
356
357        let pause_start = self.pause_start_time.unwrap();
358        let current_time = self.time_source.now();
359        let pause_duration = current_time.duration_since(pause_start);
360
361        // Add the pause duration to start time to compensate for the time spent paused.
362        // This keeps the sequence at the same position it was at when paused.
363        // If checked_add returns None (overflow, e.g., due to very long pause on 32-bit timers),
364        // we fall back to the old start time. This causes the sequence to jump forward but
365        // prevents a crash. This is a graceful degradation on timer overflow.
366        let old_start = self.start_time.unwrap();
367        self.start_time = Some(old_start.checked_add(pause_duration).unwrap_or(old_start));
368
369        self.pause_start_time = None;
370        self.state = SequencerState::Running;
371        Ok(())
372    }
373
374    /// Clears sequence and turns LED off.
375    pub fn clear(&mut self) {
376        self.sequence = None;
377        self.start_time = None;
378        self.pause_start_time = None;
379        self.state = SequencerState::Idle;
380
381        self.led.set_color(BLACK);
382        self.current_color = BLACK;
383    }
384
385    /// Returns current state.
386    #[inline]
387    pub fn state(&self) -> SequencerState {
388        self.state
389    }
390
391    /// Returns current color.
392    #[inline]
393    pub fn current_color(&self) -> Srgb {
394        self.current_color
395    }
396
397    /// Returns true if paused.
398    #[inline]
399    pub fn is_paused(&self) -> bool {
400        self.state == SequencerState::Paused
401    }
402
403    /// Returns true if running.
404    #[inline]
405    pub fn is_running(&self) -> bool {
406        self.state == SequencerState::Running
407    }
408
409    /// Returns current sequence reference.
410    #[inline]
411    pub fn current_sequence(&self) -> Option<&RgbSequence<I::Duration, N>> {
412        self.sequence.as_ref()
413    }
414
415    /// Returns elapsed time since start.
416    pub fn elapsed_time(&self) -> Option<I::Duration> {
417        self.start_time.map(|start| {
418            let now = self.time_source.now();
419            now.duration_since(start)
420        })
421    }
422
423    /// Returns the current color epsilon threshold.
424    #[inline]
425    pub fn color_epsilon(&self) -> f32 {
426        self.color_epsilon
427    }
428
429    /// Sets the color epsilon threshold.
430    ///
431    /// Controls the sensitivity of color change detection.
432    #[inline]
433    pub fn set_color_epsilon(&mut self, epsilon: f32) {
434        self.color_epsilon = epsilon;
435    }
436
437    /// Returns current brightness multiplier (0.0-1.0).
438    #[inline]
439    pub fn brightness(&self) -> f32 {
440        self.brightness
441    }
442
443    /// Sets global brightness multiplier.
444    #[inline]
445    pub fn set_brightness(&mut self, brightness: f32) {
446        self.brightness = brightness.clamp(0.0, 1.0);
447    }
448
449    /// Returns current playback position.
450    ///
451    /// When running, returns the current position. When paused, returns the frozen position
452    /// where the sequence will resume from. Returns `None` if not running/paused or sequence
453    /// is function-based.
454    #[inline]
455    pub fn current_position(&self) -> Option<Position> {
456        match self.state {
457            SequencerState::Running | SequencerState::Paused => {
458                let sequence = self.sequence.as_ref()?;
459                let start_time = self.start_time?;
460
461                // Use pause_start_time for paused state to get frozen position,
462                // otherwise use current time for running state
463                let reference_time = if self.state == SequencerState::Paused {
464                    self.pause_start_time?
465                } else {
466                    self.time_source.now()
467                };
468
469                let elapsed = reference_time.duration_since(start_time);
470
471                let step_position = sequence.find_step_position(elapsed)?;
472                Some(Position {
473                    step_index: step_position.step_index,
474                    loop_number: step_position.current_loop,
475                })
476            }
477            _ => None,
478        }
479    }
480
481    /// Consumes the sequencer and returns the LED.
482    #[inline]
483    pub fn into_led(self) -> L {
484        self.led
485    }
486
487    /// Consumes the sequencer and returns the LED and current sequence.
488    #[inline]
489    pub fn into_parts(self) -> (L, Option<RgbSequence<I::Duration, N>>) {
490        (self.led, self.sequence)
491    }
492}