use crate::effect::{validate_buffer, validate_num_leds, validate_speed, Effect, EffectError};
use crate::util::{scale_brightness, sine_wave};
use rgb::RGB8;
#[derive(Debug, Clone, PartialEq)]
pub struct PulseEffect {
num_leds: usize,
color: RGB8,
phase: u8,
speed: u8,
min_brightness: u8,
max_brightness: u8,
}
impl PulseEffect {
pub fn new(num_leds: usize) -> Result<Self, EffectError> {
validate_num_leds(num_leds)?;
Ok(Self {
num_leds,
color: RGB8::new(255, 255, 255),
phase: 0,
speed: 2,
min_brightness: 0,
max_brightness: 255,
})
}
pub fn with_color(mut self, color: RGB8) -> Self {
self.color = color;
self
}
pub fn with_speed(mut self, speed: u8) -> Result<Self, EffectError> {
validate_speed(speed)?;
self.speed = speed;
Ok(self)
}
pub fn with_min_brightness(mut self, min: u8) -> Self {
self.min_brightness = min;
self
}
pub fn with_max_brightness(mut self, max: u8) -> Self {
self.max_brightness = max;
self
}
pub fn set_color(&mut self, color: RGB8) {
self.color = color;
}
pub fn num_leds(&self) -> usize {
self.num_leds
}
fn current_brightness(&self) -> u8 {
let lo = self.min_brightness.min(self.max_brightness) as u16;
let hi = self.min_brightness.max(self.max_brightness) as u16;
let sine_val = sine_wave(self.phase) as u16;
(lo + (sine_val * (hi - lo)) / 255) as u8
}
pub fn current(&self, buffer: &mut [RGB8]) -> Result<(), EffectError> {
validate_buffer(buffer, self.num_leds)?;
let brightness = self.current_brightness();
let pixel = scale_brightness(self.color, brightness);
for led in buffer.iter_mut().take(self.num_leds) {
*led = pixel;
}
Ok(())
}
pub fn update(&mut self, buffer: &mut [RGB8]) -> Result<(), EffectError> {
self.current(buffer)?;
self.phase = self.phase.wrapping_add(self.speed);
Ok(())
}
pub fn reset(&mut self) {
self.phase = 0;
}
}
impl Effect for PulseEffect {
fn update(&mut self, buffer: &mut [RGB8]) -> Result<(), EffectError> {
PulseEffect::update(self, buffer)
}
fn current(&self, buffer: &mut [RGB8]) -> Result<(), EffectError> {
PulseEffect::current(self, buffer)
}
fn reset(&mut self) {
PulseEffect::reset(self);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_with_zero_leds_returns_error() {
assert_eq!(PulseEffect::new(0).unwrap_err(), EffectError::ZeroLeds);
}
#[test]
fn test_new_with_valid_leds_succeeds() {
let effect = PulseEffect::new(12).unwrap();
assert_eq!(effect.num_leds(), 12);
}
#[test]
fn test_with_speed_zero_returns_error() {
let result = PulseEffect::new(12).unwrap().with_speed(0);
assert_eq!(result.unwrap_err(), EffectError::ZeroStep);
}
#[test]
fn test_buffer_too_small_returns_error() {
let effect = PulseEffect::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_all_leds_same_color() {
let mut effect = PulseEffect::new(6)
.unwrap()
.with_color(RGB8::new(255, 0, 0));
let mut buffer = [RGB8::default(); 6];
for _ in 0..10 {
effect.update(&mut buffer).unwrap();
}
for i in 1..6 {
assert_eq!(buffer[0], buffer[i], "LED {} should match LED 0", i);
}
}
#[test]
fn test_breathing_changes_brightness() {
let mut effect = PulseEffect::new(1)
.unwrap()
.with_color(RGB8::new(255, 0, 0))
.with_speed(20)
.unwrap();
let mut buffer = [RGB8::default(); 1];
let mut values = Vec::new();
for _ in 0..20 {
effect.update(&mut buffer).unwrap();
values.push(buffer[0].r);
}
let min = *values.iter().min().unwrap();
let max = *values.iter().max().unwrap();
assert!(
max > min,
"brightness should vary: min={}, max={}",
min,
max
);
}
#[test]
fn test_inverted_brightness_range_does_not_underflow() {
let mut effect = PulseEffect::new(1)
.unwrap()
.with_color(RGB8::new(255, 255, 255))
.with_min_brightness(200)
.with_max_brightness(100)
.with_speed(1)
.unwrap();
let mut buffer = [RGB8::default(); 1];
for _ in 0..256 {
effect.update(&mut buffer).unwrap();
let brightness = buffer[0].r.max(buffer[0].g).max(buffer[0].b);
assert!(
brightness <= 200,
"brightness {} should be <= 200",
brightness
);
}
}
#[test]
fn test_min_brightness_floor() {
let mut effect = PulseEffect::new(1)
.unwrap()
.with_color(RGB8::new(255, 255, 255))
.with_min_brightness(100)
.with_speed(1)
.unwrap();
let mut buffer = [RGB8::default(); 1];
for _ in 0..256 {
effect.update(&mut buffer).unwrap();
let brightness = buffer[0].r.max(buffer[0].g).max(buffer[0].b);
assert!(
brightness >= 100,
"brightness {} should be >= min 100",
brightness
);
}
}
#[test]
fn test_max_brightness_ceiling() {
let mut effect = PulseEffect::new(1)
.unwrap()
.with_color(RGB8::new(255, 255, 255))
.with_max_brightness(100)
.with_speed(1)
.unwrap();
let mut buffer = [RGB8::default(); 1];
for _ in 0..256 {
effect.update(&mut buffer).unwrap();
let brightness = buffer[0].r.max(buffer[0].g).max(buffer[0].b);
assert!(
brightness <= 100,
"brightness {} should be <= max 100",
brightness
);
}
}
#[test]
fn test_reset_restores_initial_state() {
let mut effect = PulseEffect::new(4)
.unwrap()
.with_color(RGB8::new(0, 255, 0))
.with_speed(10)
.unwrap();
let mut initial = [RGB8::default(); 4];
effect.current(&mut initial).unwrap();
let mut temp = [RGB8::default(); 4];
for _ in 0..20 {
effect.update(&mut temp).unwrap();
}
effect.reset();
let mut after_reset = [RGB8::default(); 4];
effect.current(&mut after_reset).unwrap();
assert_eq!(initial, after_reset);
}
#[test]
fn test_current_does_not_advance() {
let effect = PulseEffect::new(4)
.unwrap()
.with_color(RGB8::new(0, 255, 0));
let mut buf1 = [RGB8::default(); 4];
let mut buf2 = [RGB8::default(); 4];
effect.current(&mut buf1).unwrap();
effect.current(&mut buf2).unwrap();
assert_eq!(buf1, buf2);
}
#[test]
fn test_set_color_does_not_reset_phase() {
let mut effect = PulseEffect::new(4)
.unwrap()
.with_color(RGB8::new(255, 0, 0))
.with_speed(20)
.unwrap();
let mut buffer = [RGB8::default(); 4];
for _ in 0..5 {
effect.update(&mut buffer).unwrap();
}
let mut before = [RGB8::default(); 4];
effect.current(&mut before).unwrap();
effect.set_color(RGB8::new(0, 255, 0));
let mut after = [RGB8::default(); 4];
effect.current(&mut after).unwrap();
let brightness_before = before[0].r.max(before[0].g).max(before[0].b);
let brightness_after = after[0].r.max(after[0].g).max(after[0].b);
assert_eq!(
brightness_before, brightness_after,
"phase should be unchanged after set_color"
);
assert_ne!(before[0], after[0], "color should have changed");
}
#[test]
fn test_trait_object_update() {
let mut effect = PulseEffect::new(4)
.unwrap()
.with_color(RGB8::new(255, 0, 0))
.with_speed(50)
.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();
}
#[test]
fn test_new_with_too_many_leds_returns_error() {
use crate::effect::MAX_LEDS;
let result = PulseEffect::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 = PulseEffect::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
);
}
}
}