rgb-sequencer
A no_std-compatible Rust library for controlling RGB LEDs through timed color sequences on embedded systems.
Overview
rgb-sequencer provides a lightweight, flexible framework for creating and executing RGB LED animations on resource-constrained embedded devices. Instead of manually managing timers, color interpolation, and LED updates in your application code, you define high-level sequences and let the library handle the timing complexity.
The library supports two animation approaches:
-
Step-based sequences: Define explicit color waypoints with durations and transition styles (instant or smooth linear interpolation). Perfect for discrete animations like police lights, status indicators, or scripted color shows. Support finite or infinite looping with configurable landing colors, and smooth entry animations via start colors.
-
Function-based sequences: Use custom functions to compute colors algorithmically based on elapsed time. Ideal for mathematical animations like sine wave breathing effects, HSV color wheels, or any procedurally generated pattern.
Each RgbSequencer instance controls one LED independently through trait abstractions, allowing you to:
- Run different animations on multiple LEDs simultaneously
- Pause and resume individual sequences
- Query the current color of individual LEDs
The library is built for embedded systems with:
- Zero heap allocation: All storage uses fixed-capacity collections with compile-time sizing
- Platform independence: Abstracts LED control and timing system through traits
- Efficient timing: Service timing hints enable power-efficient operation without busy-waiting
- Type-safe colors: Uses
palette::Srgb<f32>for accurate color math and smooth transitions
Whether you're building a status LED that breathes gently, a multi-LED notification system with synchronized animations, or an interactive light show that responds to user input, rgb-sequencer provides the building blocks and lets you focus on your application logic.
Quick Start
Add Dependency
[]
= "0.1"
= { = "0.7.6", = false, = ["libm"] }
Minimal Example
use ;
use Srgb;
// 1. Implement the RgbLed trait for your hardware
// 2. Implement the TimeSource trait for your timing system
;
// 3. Create a blinking sequence
let sequence = builder
.step // White
.step // Off
.loop_count // Loop indefinitely
.build?;
// 4. Create sequencer and start
let led = new;
let timer = new;
let mut sequencer = new;
sequencer.load;
sequencer.start.unwrap;
// 5. Service in your main loop
loop
Features
Step-Based Sequences
Step-based sequences define animations as a series of color waypoints with explicit durations and transition styles.
Basic Step Construction
let sequence = builder
.step
.step
.build?;
Transition Styles
-
TransitionStyle::Step: Instantly jumps to the target color and holds it for the duration. Perfect for discrete animations like blinking or status indicators. -
TransitionStyle::Linear: Smoothly interpolates from the previous color to the target color over the step's duration using linear RGB interpolation. Ideal for smooth fades and color transitions.
Zero-Duration Steps
For steps with TransitionStyle::Step, setting zero-duration is allowed and serves as a color waypoint:
let sequence = builder
.step // Yellow waypoint
.step // Fade to black
.loop_count
.build?;
This creates a sequence that on each loop iteration, will jump to yellow and then smoothly transition to black (off).
Important: Zero-duration steps with TransitionStyle::Linear are invalid and will be rejected during sequence building.
Start Color for Smooth Entry
The start_color() method allows you to define a color to interpolate from at the very beginning of the sequence.
let sequence = builder
.start_color // Start from black
.step // Fade to red
.step // Fade to blue
.loop_count
.build?;
Behavior:
- First loop: Uses
start_colorfor interpolation to first step (black → red) - Subsequent loops: Uses last step's color for interpolation to first step (blue → red)
This is particularly useful for creating smooth entry animations into looping sequences without affecting the loop-to-loop transitions.
Note: start_color only affects the first step if it uses TransitionStyle::Linear. For TransitionStyle::Step, the start color is ignored.
Landing Color for Completion
For finite sequences, you can specify a landing_color to display after all loops complete:
let sequence = builder
.step // Red
.step // Green
.loop_count
.landing_color // Turn blue when done
.build?;
Behavior:
- The sequence blinks red/green 3 times
- After completion, the LED turns blue and stays blue
Note: If no landing_color is specified, the LED holds the last step's color
Note: landing_color is ignored for infinite sequences.
Loop Count
Control how many times a sequence repeats:
// Run once and stop
.loop_count
// Run 5 times and stop
.loop_count
// Run forever
.loop_count
Note: If no Loop Count is specified, the sequence will default to LoopCount::Finite(1)
Function-Based Sequences
Function-based sequences use custom functions to compute colors algorithmically based on elapsed time. This enables mathematical animations, procedural patterns, and dynamic effects that would be difficult to express with discrete steps.
Creating a Function-Based Sequence
// Define base color
let white = new
// Define color function
// Define timing function
// Create sequence
let sequence = from_function;
The Two Functions
Function-based sequences requires two custom function definitions:
1. Color Function: fn(Srgb, Duration) -> Srgb
Computes the LED color for a given elapsed time:
- First parameter: The base color
- Second parameter: Time elapsed since sequence started
- Returns: The color to display at this time
This design allows the same function to be reused with different base colors:
let red = new;
let blue = new;
// Same function, different colors
let red_pulse = from_function;
let blue_pulse = from_function;
2. Timing Function: fn(Duration) -> Option<Duration>
Tells the sequencer when it needs to be serviced again:
- Parameter: Time elapsed since sequence started
- Returns: The duration until next service at this time
Some(Duration::ZERO)- Continuous animation, call service() at your desired frame rateSome(duration)- Static color period - the LED needs updating after this durationNone- Animation complete - Sequence is done - No further service is needed
Example with completion:
Role of Base Color
For function-based sequences, the "base color" passed to from_function() serves as the color that gets passed to your color function. It's not used for interpolation like in step-based sequences—instead, it's available for your function to modulate, blend, use as a reference or ignore.
This allows for flexible color-agnostic functions:
// Organic fire flicker using multiple sine waves
let dim_red = from_function;
Step-based vs. Function-based Sequences
Use step-based sequences when:
- Simple stuff like just setting a static color or blinking
- You only need instant color changes or linear transitions
- You have a fixed set of color waypoints
- Your animation fits naturally into discrete stages
Use function-based sequences when:
- You need smooth mathematical animations (sine waves, easing functions)
- You want algorithmic patterns that don't fit into discrete steps
- You want to reuse the same animation logic with different colors
- Your animation depends on complex calculations
Servicing the Sequencer
The service() method is the heart of the sequencer. It calculates the appropriate color for the current time, updates its LED and tells you when to call it again.
Understanding the Return Value
let led = new;
let timer = new;
let mut sequencer = new;
sequencer.load;
sequencer.start.unwrap;
loop
Note: For function-based sequences, the service() method will call the timing function internally and forward its return value.
Multi-LED Servicing
When managing multiple LEDs, coordinate timing across all sequencers:
use ServiceTiming;
let mut has_continuous = false;
let mut min_delay = None;
let mut all_complete = true;
for sequencer in sequencers.iter_mut
if has_continuous else if let Some = min_delay else if all_complete
State Machine
The sequencer implements a state machine that validates operation preconditions and prevents invalid state transitions.
States
Idle: No sequence loaded, LED is offLoaded: Sequence loaded but not started, LED is offRunning: Sequence actively executing, LED displays animated colorsPaused: Sequence paused at current colorComplete: Finite sequence finished, LED displays landing color or last step color
Sequencer operations and resulting State changes
| Method | Required State | Result State |
|---|---|---|
load() |
Any | Loaded |
start() |
Loaded |
Running |
service() |
Running |
Running or Complete |
pause() |
Running |
Paused |
resume() |
Paused |
Running |
restart() |
Running, Paused, or Complete |
Running |
stop() |
Running, Paused, or Complete |
Loaded |
clear() |
Any | Idle |
Calling a method from an invalid state returns Err(SequencerError::InvalidState).
Checking State
match sequencer.get_state
// Convenience methods
if sequencer.is_running
if sequencer.is_paused
Pause and Resume with Timing Compensation
The pause/resume functionality maintains perfect timing continuity, as if the pause never occurred.
How It Works
When you call pause():
- The current time is recorded as
pause_start_time - The LED stays at the current color
- State transitions to
Paused
When you call resume():
- The pause duration is calculated:
now - pause_start_time - The sequence's
start_timeis adjusted forward by the pause duration - State transitions to
Running - The sequence continues from exactly where it left off
Use Cases
- Interactive color capture (pause to "freeze" a color)
- User-controlled animation playback
- Event-driven synchronization across multiple LEDs
Multi-LED Control
The library supports multiple patterns for controlling multiple LEDs independently.
Pattern 1: Separate Sequencers
The simplest approach—create a sequencer for each LED:
let mut sequencer_1 = new;
let mut sequencer_2 = new;
sequencer_1.load;
sequencer_2.load;
sequencer_1.start?;
sequencer_2.start?;
loop
Here's a more explanatory version:
Pattern 2: Heterogeneous Collections (Advanced)
When you have multiple LEDs connected to different hardware peripherals (e.g., one LED on TIM1, another on TIM3), you face a type system challenge: each LED has a different concrete type (PwmRgbLed<TIM1> vs PwmRgbLed<TIM3>), which means you can't store them in the same Vec or array.
The solution is an enum wrapper that unifies different LED types under a single type while maintaining zero-cost abstraction:
// Wrapper enum for different LED types
// Now all sequencers have the same type and can be stored together!
let mut sequencers: = Vecnew;
sequencers.push?;
sequencers.push?;
// Access individual LEDs by index
for in sequencers.iter_mut.enumerate
See examples/stm32f0-embassy/bin/rainbow_capture for a complete implementation.
Pattern 3: Command-Based Control
For task-based systems (like Embassy), use the SequencerCommand type for message passing:
use ;
// Define LED identifiers
// Create command channel
static COMMAND_CHANNEL: = new;
// Send commands from control task
COMMAND_CHANNEL.send.await;
COMMAND_CHANNEL.send.await;
// Handle commands in RGB task
let command = COMMAND_CHANNEL.receive.await;
if let Some = get_sequencer_mut
See examples/stm32f0-embassy/bin/mode_switcher for a complete implementation.
Querying Sequencer State
Beyond checking the state machine, you can query other aspects of a sequencer:
// Get the current LED color
let color = sequencer.current_color;
// Get elapsed time (if running)
if let Some = sequencer.elapsed_time
// Get a reference to the loaded sequence
if let Some = sequencer.current_sequence
// Check if a finite sequence has completed
if sequence.has_completed
These methods are useful for:
- Color capture: Getting the current color to create derived sequences
- Synchronization: Coordinating multiple LEDs based on elapsed time
- UI feedback: Displaying sequence progress to users
- Debugging: Inspecting sequence state during development
Choosing Sequence Capacity
The const generic parameter N determines how many steps a sequence can hold:
// Up to 8 steps
// Up to 16 steps
// Up to 32 steps
Guidelines
- Start with 8: Sufficient for most simple animations (blinks, pulses, basic cycles)
- Use 16: For complex multi-color sequences (rainbow cycles, multi-stage effects)
- Use 32+: For elaborate shows or data-driven animations
- Function-based: Don't need any steps — they compute colors algorithmically
Memory Impact
Calculate Exact Sizes: Use the memory calculator tool to see detailed breakdowns for different capacities and duration types.
Performance Considerations
Floating Point Math Requirements
IMPORTANT: This library uses f32 extensively for color math and interpolation. Performance varies by target:
Hardware FPU (Fast) ✅
Cortex-M4F, M7, M33 (e.g., STM32F4, STM32H7, nRF52) - Hardware-accelerated f32 operations, excellent performance.
No Hardware FPU (Slow) ⚠️
Cortex-M0/M0+, M3 (e.g., STM32F0, STM32F1, RP2040) - Software-emulated f32 is 10-100x slower.
Recommendations for non-FPU targets:
- Prefer Step transitions over Linear (avoids interpolation)
- Avoid math-heavy function-based sequences
The library works on all targets but is optimized for microcontrollers with FPU.
License
This project is licensed under the MIT License - see the LICENSE file for details.