use rgb::RGB8;
pub const MAX_LEDS: usize = 256;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EffectError {
ZeroLeds,
TooManyLeds {
requested: usize,
max: usize,
},
ZeroStep,
ZeroDuty,
BufferTooSmall {
required: usize,
actual: usize,
},
TooManySections {
requested: usize,
max: usize,
},
}
impl core::fmt::Display for EffectError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
EffectError::ZeroLeds => write!(f, "number of LEDs must be greater than 0"),
EffectError::TooManyLeds { requested, max } => {
write!(
f,
"too many LEDs: requested {}, maximum supported is {}",
requested, max
)
}
EffectError::ZeroStep => write!(f, "speed must be greater than 0"),
EffectError::ZeroDuty => {
write!(f, "on and off durations must both be greater than 0")
}
EffectError::BufferTooSmall { required, actual } => {
write!(
f,
"buffer too small: need {} LEDs, got {}",
required, actual
)
}
EffectError::TooManySections { requested, max } => {
write!(
f,
"too many sections: requested {}, maximum is {}",
requested, max
)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Direction {
#[default]
Clockwise,
CounterClockwise,
}
pub trait Effect {
fn update(&mut self, buffer: &mut [RGB8]) -> Result<(), EffectError>;
fn current(&self, buffer: &mut [RGB8]) -> Result<(), EffectError>;
fn reset(&mut self);
}
pub(crate) fn validate_speed(speed: u8) -> Result<(), EffectError> {
if speed == 0 {
return Err(EffectError::ZeroStep);
}
Ok(())
}
pub(crate) fn validate_num_leds(num_leds: usize) -> Result<(), EffectError> {
if num_leds == 0 {
return Err(EffectError::ZeroLeds);
}
if num_leds > MAX_LEDS {
return Err(EffectError::TooManyLeds {
requested: num_leds,
max: MAX_LEDS,
});
}
Ok(())
}
pub(crate) fn validate_buffer(buffer: &[RGB8], num_leds: usize) -> Result<(), EffectError> {
if buffer.len() < num_leds {
return Err(EffectError::BufferTooSmall {
required: num_leds,
actual: buffer.len(),
});
}
Ok(())
}
pub(crate) fn advance_position(
position: u8,
speed: u8,
num_leds: usize,
direction: Direction,
) -> u8 {
match direction {
Direction::Clockwise => (position as usize + speed as usize).rem_euclid(num_leds) as u8,
Direction::CounterClockwise => {
(position as isize - speed as isize).rem_euclid(num_leds as isize) as u8
}
}
}
pub(crate) fn validate_duty(on_ticks: u8, off_ticks: u8) -> Result<(), EffectError> {
if on_ticks == 0 || off_ticks == 0 {
return Err(EffectError::ZeroDuty);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_speed_zero() {
assert_eq!(validate_speed(0).unwrap_err(), EffectError::ZeroStep);
}
#[test]
fn test_validate_speed_valid() {
assert!(validate_speed(1).is_ok());
assert!(validate_speed(255).is_ok());
}
#[test]
fn test_validate_num_leds_zero() {
assert_eq!(validate_num_leds(0).unwrap_err(), EffectError::ZeroLeds);
}
#[test]
fn test_validate_num_leds_valid() {
assert!(validate_num_leds(1).is_ok());
assert!(validate_num_leds(12).is_ok());
assert!(validate_num_leds(MAX_LEDS).is_ok());
}
#[test]
fn test_validate_num_leds_too_many() {
assert_eq!(
validate_num_leds(MAX_LEDS + 1).unwrap_err(),
EffectError::TooManyLeds {
requested: MAX_LEDS + 1,
max: MAX_LEDS
}
);
}
#[test]
fn test_validate_buffer_ok() {
let buffer = [RGB8::default(); 12];
assert!(validate_buffer(&buffer, 12).is_ok());
assert!(validate_buffer(&buffer, 8).is_ok());
}
#[test]
fn test_validate_buffer_too_small() {
let buffer = [RGB8::default(); 8];
assert_eq!(
validate_buffer(&buffer, 12).unwrap_err(),
EffectError::BufferTooSmall {
required: 12,
actual: 8
}
);
}
#[test]
fn test_validate_duty_zero_on() {
assert_eq!(validate_duty(0, 4).unwrap_err(), EffectError::ZeroDuty);
}
#[test]
fn test_validate_duty_zero_off() {
assert_eq!(validate_duty(4, 0).unwrap_err(), EffectError::ZeroDuty);
}
#[test]
fn test_validate_duty_both_zero() {
assert_eq!(validate_duty(0, 0).unwrap_err(), EffectError::ZeroDuty);
}
#[test]
fn test_validate_duty_valid() {
assert!(validate_duty(1, 1).is_ok());
assert!(validate_duty(4, 4).is_ok());
assert!(validate_duty(255, 1).is_ok());
}
#[test]
fn test_direction_default_is_clockwise() {
assert_eq!(Direction::default(), Direction::Clockwise);
}
#[test]
fn test_advance_position_clockwise() {
assert_eq!(advance_position(0, 1, 8, Direction::Clockwise), 1);
assert_eq!(advance_position(5, 3, 8, Direction::Clockwise), 0);
}
#[test]
fn test_advance_position_counter_clockwise() {
assert_eq!(advance_position(3, 1, 8, Direction::CounterClockwise), 2);
assert_eq!(advance_position(0, 1, 8, Direction::CounterClockwise), 7);
}
#[test]
fn test_advance_position_wraps_with_large_speed() {
assert_eq!(advance_position(0, 10, 8, Direction::Clockwise), 2);
assert_eq!(advance_position(0, 10, 8, Direction::CounterClockwise), 6);
}
#[test]
fn test_error_display() {
assert_eq!(
format!("{}", EffectError::ZeroLeds),
"number of LEDs must be greater than 0"
);
assert_eq!(
format!(
"{}",
EffectError::TooManyLeds {
requested: 300,
max: 256
}
),
"too many LEDs: requested 300, maximum supported is 256"
);
assert_eq!(
format!("{}", EffectError::ZeroStep),
"speed must be greater than 0"
);
assert_eq!(
format!("{}", EffectError::ZeroDuty),
"on and off durations must both be greater than 0"
);
assert_eq!(
format!(
"{}",
EffectError::BufferTooSmall {
required: 12,
actual: 8
}
),
"buffer too small: need 12 LEDs, got 8"
);
assert_eq!(
format!(
"{}",
EffectError::TooManySections {
requested: 10,
max: 8
}
),
"too many sections: requested 10, maximum is 8"
);
}
}