use crate::effect::{validate_buffer, validate_duty, validate_num_leds, Effect, EffectError};
use crate::util::fill_solid;
use rgb::RGB8;
#[derive(Debug, Clone, PartialEq)]
pub struct FlashEffect {
num_leds: usize,
color: RGB8,
off_color: RGB8,
on_ticks: u8,
off_ticks: u8,
counter: u8,
}
impl FlashEffect {
pub fn new(num_leds: usize) -> Result<Self, EffectError> {
validate_num_leds(num_leds)?;
Ok(Self {
num_leds,
color: RGB8::new(255, 255, 255),
off_color: RGB8::new(0, 0, 0),
on_ticks: 4,
off_ticks: 4,
counter: 0,
})
}
pub fn with_color(mut self, color: RGB8) -> Self {
self.color = color;
self
}
pub fn with_off_color(mut self, off_color: RGB8) -> Self {
self.off_color = off_color;
self
}
pub fn set_color(&mut self, color: RGB8) {
self.color = color;
}
pub fn with_duty(mut self, on_ticks: u8, off_ticks: u8) -> Result<Self, EffectError> {
validate_duty(on_ticks, off_ticks)?;
self.on_ticks = on_ticks;
self.off_ticks = off_ticks;
Ok(self)
}
pub fn num_leds(&self) -> usize {
self.num_leds
}
pub fn is_on(&self) -> bool {
self.counter < self.on_ticks
}
pub fn current(&self, buffer: &mut [RGB8]) -> Result<(), EffectError> {
validate_buffer(buffer, self.num_leds)?;
let color = if self.is_on() {
self.color
} else {
self.off_color
};
fill_solid(&mut buffer[..self.num_leds], color);
Ok(())
}
pub fn update(&mut self, buffer: &mut [RGB8]) -> Result<(), EffectError> {
self.current(buffer)?;
let cycle = self.on_ticks as u16 + self.off_ticks as u16;
self.counter = ((self.counter as u16 + 1) % cycle) as u8;
Ok(())
}
pub fn reset(&mut self) {
self.counter = 0;
}
}
impl Effect for FlashEffect {
fn update(&mut self, buffer: &mut [RGB8]) -> Result<(), EffectError> {
self.update(buffer)
}
fn current(&self, buffer: &mut [RGB8]) -> Result<(), EffectError> {
self.current(buffer)
}
fn reset(&mut self) {
self.reset();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_with_zero_leds_returns_error() {
assert_eq!(FlashEffect::new(0).unwrap_err(), EffectError::ZeroLeds);
}
#[test]
fn test_new_with_valid_leds_succeeds() {
let effect = FlashEffect::new(8).unwrap();
assert_eq!(effect.num_leds(), 8);
}
#[test]
fn test_zero_duty_returns_error() {
let result = FlashEffect::new(8).unwrap().with_duty(0, 4);
assert_eq!(result.unwrap_err(), EffectError::ZeroDuty);
let result = FlashEffect::new(8).unwrap().with_duty(4, 0);
assert_eq!(result.unwrap_err(), EffectError::ZeroDuty);
}
#[test]
fn test_buffer_too_small_returns_error() {
let effect = FlashEffect::new(12).unwrap();
let mut buffer = [RGB8::default(); 8];
assert_eq!(
effect.current(&mut buffer).unwrap_err(),
EffectError::BufferTooSmall {
required: 12,
actual: 8
}
);
}
#[test]
fn test_on_phase_fills_with_color() {
let effect = FlashEffect::new(4)
.unwrap()
.with_color(RGB8::new(255, 0, 0));
let mut buffer = [RGB8::default(); 4];
effect.current(&mut buffer).unwrap();
for pixel in &buffer {
assert_eq!(*pixel, RGB8::new(255, 0, 0));
}
}
#[test]
fn test_off_phase_fills_with_off_color() {
let mut effect = FlashEffect::new(4)
.unwrap()
.with_color(RGB8::new(255, 0, 0))
.with_duty(1, 1)
.unwrap();
let mut buffer = [RGB8::default(); 4];
effect.update(&mut buffer).unwrap();
effect.current(&mut buffer).unwrap();
for pixel in &buffer {
assert_eq!(*pixel, RGB8::new(0, 0, 0));
}
}
#[test]
fn test_counter_cycles_through_full_period() {
let mut effect = FlashEffect::new(4).unwrap().with_duty(2, 3).unwrap();
let mut buffer = [RGB8::default(); 4];
assert!(effect.is_on()); effect.update(&mut buffer).unwrap();
assert!(effect.is_on()); effect.update(&mut buffer).unwrap();
assert!(!effect.is_on()); effect.update(&mut buffer).unwrap();
assert!(!effect.is_on()); effect.update(&mut buffer).unwrap();
assert!(!effect.is_on()); effect.update(&mut buffer).unwrap();
assert!(effect.is_on()); }
#[test]
fn test_custom_off_color() {
let mut effect = FlashEffect::new(4)
.unwrap()
.with_color(RGB8::new(255, 0, 0))
.with_off_color(RGB8::new(0, 0, 255))
.with_duty(1, 1)
.unwrap();
let mut buffer = [RGB8::default(); 4];
effect.update(&mut buffer).unwrap();
effect.current(&mut buffer).unwrap();
for pixel in &buffer {
assert_eq!(*pixel, RGB8::new(0, 0, 255));
}
}
#[test]
fn test_reset_restores_initial_state() {
let mut effect = FlashEffect::new(4).unwrap().with_duty(2, 2).unwrap();
let mut buffer = [RGB8::default(); 4];
for _ in 0..5 {
effect.update(&mut buffer).unwrap();
}
effect.reset();
assert!(effect.is_on(), "reset should return to on phase");
}
#[test]
fn test_is_on_tracks_phase() {
let effect = FlashEffect::new(4).unwrap().with_duty(3, 2).unwrap();
assert!(effect.is_on());
}
#[test]
fn test_set_color_does_not_reset_counter() {
let mut effect = FlashEffect::new(4)
.unwrap()
.with_color(RGB8::new(255, 0, 0))
.with_duty(4, 4)
.unwrap();
let mut buffer = [RGB8::default(); 4];
effect.update(&mut buffer).unwrap();
effect.update(&mut buffer).unwrap();
effect.set_color(RGB8::new(0, 255, 0));
effect.current(&mut buffer).unwrap();
for pixel in &buffer {
assert_eq!(
*pixel,
RGB8::new(0, 255, 0),
"new color should show without phase reset"
);
}
}
#[test]
fn test_trait_object_update() {
let mut effect = FlashEffect::new(4)
.unwrap()
.with_color(RGB8::new(255, 0, 0))
.with_duty(1, 1)
.unwrap();
let effect_ref: &mut dyn Effect = &mut effect;
let mut buf1 = [RGB8::default(); 4];
let mut buf2 = [RGB8::default(); 4];
effect_ref.update(&mut buf1).unwrap();
effect_ref.update(&mut buf2).unwrap();
assert_ne!(buf1, buf2, "flash should toggle between updates");
}
#[test]
fn test_new_with_too_many_leds_returns_error() {
use crate::effect::MAX_LEDS;
let result = FlashEffect::new(MAX_LEDS + 1);
assert_eq!(
result.unwrap_err(),
EffectError::TooManyLeds {
requested: MAX_LEDS + 1,
max: MAX_LEDS
}
);
}
#[test]
fn test_oversized_buffer_accepted() {
let sentinel = RGB8::new(0xDE, 0xAD, 0xFF);
let effect = FlashEffect::new(4).unwrap();
let mut buffer = [sentinel; 8];
effect.current(&mut buffer).unwrap();
for i in 4..8 {
assert_eq!(
buffer[i], sentinel,
"LED {} beyond num_leds must not be modified",
i
);
}
}
}