use crate::effect::{validate_buffer, validate_num_leds, validate_speed, Effect, EffectError};
use crate::util::{draw_scanner_head, scanner_bounce};
use rgb::RGB8;
#[derive(Debug, Clone, PartialEq)]
pub struct KnightRiderEffect {
num_leds: usize,
color: RGB8,
pos_a: u8,
forward_a: bool,
pos_b: u8,
forward_b: bool,
speed: u8,
tail_length: u8,
decay: u8,
}
impl KnightRiderEffect {
pub fn new(num_leds: usize) -> Result<Self, EffectError> {
validate_num_leds(num_leds)?;
let pos_b = num_leds.saturating_sub(1).min(u8::MAX as usize) as u8;
Ok(Self {
num_leds,
color: RGB8::new(255, 0, 0),
pos_a: 0,
forward_a: true,
pos_b,
forward_b: false,
speed: 1,
tail_length: 4,
decay: 192,
})
}
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_tail_length(mut self, tail_length: u8) -> Self {
let max_tail = self.num_leds.saturating_sub(1).min(u8::MAX as usize) as u8;
self.tail_length = tail_length.min(max_tail);
self
}
pub fn with_decay(mut self, decay: u8) -> Self {
self.decay = decay;
self
}
pub fn set_color(&mut self, color: RGB8) {
self.color = color;
}
pub fn num_leds(&self) -> usize {
self.num_leds
}
pub fn current(&self, buffer: &mut [RGB8]) -> Result<(), EffectError> {
validate_buffer(buffer, self.num_leds)?;
let n = self.num_leds;
buffer[..n].fill(RGB8::default());
self.draw_head(buffer, self.pos_a as usize, self.forward_a);
self.draw_head(buffer, self.pos_b as usize, self.forward_b);
Ok(())
}
pub fn update(&mut self, buffer: &mut [RGB8]) -> Result<(), EffectError> {
self.current(buffer)?;
self.advance();
Ok(())
}
pub fn reset(&mut self) {
self.pos_a = 0;
self.forward_a = true;
self.pos_b = self.num_leds.saturating_sub(1).min(u8::MAX as usize) as u8;
self.forward_b = false;
}
fn draw_head(&self, buffer: &mut [RGB8], head: usize, forward: bool) {
draw_scanner_head(
buffer,
self.num_leds,
head,
forward,
self.color,
self.tail_length,
self.decay,
);
}
fn advance(&mut self) {
let (new_pos, new_fwd) =
Self::advance_head(self.pos_a, self.forward_a, self.speed, self.num_leds);
self.pos_a = new_pos;
self.forward_a = new_fwd;
let (new_pos, new_fwd) =
Self::advance_head(self.pos_b, self.forward_b, self.speed, self.num_leds);
self.pos_b = new_pos;
self.forward_b = new_fwd;
}
fn advance_head(position: u8, forward: bool, speed: u8, num_leds: usize) -> (u8, bool) {
scanner_bounce(position, forward, speed, num_leds)
}
}
impl Effect for KnightRiderEffect {
fn update(&mut self, buffer: &mut [RGB8]) -> Result<(), EffectError> {
KnightRiderEffect::update(self, buffer)
}
fn current(&self, buffer: &mut [RGB8]) -> Result<(), EffectError> {
KnightRiderEffect::current(self, buffer)
}
fn reset(&mut self) {
KnightRiderEffect::reset(self);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::effect::MAX_LEDS;
const RED: RGB8 = RGB8::new(255, 0, 0);
const BLACK: RGB8 = RGB8::new(0, 0, 0);
#[test]
fn test_new_with_zero_leds_returns_error() {
assert_eq!(
KnightRiderEffect::new(0).unwrap_err(),
EffectError::ZeroLeds
);
}
#[test]
fn test_new_with_valid_leds_succeeds() {
let effect = KnightRiderEffect::new(12).unwrap();
assert_eq!(effect.num_leds(), 12);
}
#[test]
fn test_new_with_too_many_leds_returns_error() {
assert!(matches!(
KnightRiderEffect::new(MAX_LEDS + 1).unwrap_err(),
EffectError::TooManyLeds { .. }
));
}
#[test]
fn test_with_speed_zero_returns_error() {
assert_eq!(
KnightRiderEffect::new(12)
.unwrap()
.with_speed(0)
.unwrap_err(),
EffectError::ZeroStep
);
}
#[test]
fn test_buffer_too_small_returns_error() {
let effect = KnightRiderEffect::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_both_heads_lit_initially() {
let effect = KnightRiderEffect::new(12)
.unwrap()
.with_color(RED)
.with_tail_length(0);
let mut buffer = [RGB8::default(); 12];
effect.current(&mut buffer).unwrap();
assert_eq!(buffer[0], RED, "head A should be lit at index 0");
assert_eq!(buffer[11], RED, "head B should be lit at index 11");
}
#[test]
fn test_single_led_both_heads_at_same_position() {
let effect = KnightRiderEffect::new(1).unwrap().with_color(RED);
let mut buffer = [RGB8::default(); 1];
effect.current(&mut buffer).unwrap();
assert_eq!(buffer[0], RED);
}
#[test]
fn test_heads_move_toward_each_other_after_one_update() {
let mut effect = KnightRiderEffect::new(12)
.unwrap()
.with_color(RED)
.with_tail_length(0);
let mut buffer = [RGB8::default(); 12];
effect.update(&mut buffer).unwrap(); effect.current(&mut buffer).unwrap();
assert_eq!(buffer[1], RED, "head A should be at index 1");
assert_eq!(buffer[10], RED, "head B should be at index 10");
}
#[test]
fn test_speed_two_advances_by_two() {
let mut effect = KnightRiderEffect::new(12)
.unwrap()
.with_color(RED)
.with_tail_length(0)
.with_speed(2)
.unwrap();
let mut buffer = [RGB8::default(); 12];
effect.update(&mut buffer).unwrap(); effect.current(&mut buffer).unwrap();
assert_eq!(buffer[2], RED, "head A should be at index 2");
assert_eq!(buffer[9], RED, "head B should be at index 9");
}
#[test]
fn test_large_speed_no_panic() {
let mut effect = KnightRiderEffect::new(4)
.unwrap()
.with_color(RED)
.with_tail_length(0)
.with_speed(200)
.unwrap();
let mut buffer = [RGB8::default(); 4];
for _ in 0..20 {
effect.update(&mut buffer).unwrap();
}
effect.current(&mut buffer).unwrap();
assert!(
buffer.iter().any(|led| *led != BLACK),
"at least one LED should be lit after large-speed updates"
);
}
#[test]
fn test_head_a_reverses_at_top_boundary() {
let mut effect = KnightRiderEffect::new(4)
.unwrap()
.with_color(RED)
.with_tail_length(0);
let mut buffer = [RGB8::default(); 4];
for _ in 0..4 {
effect.update(&mut buffer).unwrap();
}
effect.current(&mut buffer).unwrap();
assert_eq!(buffer[3], BLACK, "index 3 should be off: head A bounced");
}
#[test]
fn test_head_b_reverses_at_bottom_boundary() {
let mut effect = KnightRiderEffect::new(4)
.unwrap()
.with_color(RED)
.with_tail_length(0);
let mut buffer = [RGB8::default(); 4];
for _ in 0..4 {
effect.update(&mut buffer).unwrap();
}
effect.current(&mut buffer).unwrap();
assert_eq!(buffer[0], BLACK, "index 0 should be off: head B bounced");
}
#[test]
fn test_tail_a_follows_forward_direction() {
let mut effect = KnightRiderEffect::new(12)
.unwrap()
.with_color(RED)
.with_tail_length(3)
.with_decay(255);
let mut buffer = [RGB8::default(); 12];
for _ in 0..4 {
effect.update(&mut buffer).unwrap();
}
effect.current(&mut buffer).unwrap();
assert_eq!(buffer[4], RED, "head A at 4");
assert!(buffer[3].r > 0, "tail A at 3 should be lit");
assert!(buffer[2].r > 0, "tail A at 2 should be lit");
assert!(buffer[1].r > 0, "tail A at 1 should be lit");
assert_eq!(buffer[5], BLACK, "index 5 should be off (ahead of head A)");
}
#[test]
fn test_tail_b_follows_backward_direction() {
let mut effect = KnightRiderEffect::new(12)
.unwrap()
.with_color(RED)
.with_tail_length(3)
.with_decay(255);
let mut buffer = [RGB8::default(); 12];
for _ in 0..4 {
effect.update(&mut buffer).unwrap();
}
effect.current(&mut buffer).unwrap();
assert_eq!(buffer[7], RED, "head B at 7");
assert!(buffer[8].r > 0, "tail B at 8 should be lit");
assert!(buffer[9].r > 0, "tail B at 9 should be lit");
assert!(buffer[10].r > 0, "tail B at 10 should be lit");
assert_eq!(buffer[6], BLACK, "index 6 should be off (ahead of head B)");
}
#[test]
fn test_oversized_buffer_accepted() {
let effect = KnightRiderEffect::new(4)
.unwrap()
.with_color(RED)
.with_tail_length(0);
let mut buffer = [RED; 8];
assert!(effect.current(&mut buffer).is_ok());
assert_eq!(buffer[0], RED, "head A at index 0");
assert_eq!(buffer[3], RED, "head B at index 3");
assert_eq!(buffer[4], RED, "beyond num_leds should be untouched");
}
#[test]
fn test_current_does_not_advance() {
let effect = KnightRiderEffect::new(8).unwrap();
let mut buf1 = [RGB8::default(); 8];
let mut buf2 = [RGB8::default(); 8];
effect.current(&mut buf1).unwrap();
effect.current(&mut buf2).unwrap();
assert_eq!(buf1, buf2, "current() must not change state");
}
#[test]
fn test_reset_restores_initial_state() {
let mut effect = KnightRiderEffect::new(8).unwrap().with_tail_length(0);
let mut initial = [RGB8::default(); 8];
effect.current(&mut initial).unwrap();
let mut temp = [RGB8::default(); 8];
for _ in 0..20 {
effect.update(&mut temp).unwrap();
}
effect.reset();
let mut after_reset = [RGB8::default(); 8];
effect.current(&mut after_reset).unwrap();
assert_eq!(
initial, after_reset,
"state after reset must match initial state"
);
}
#[test]
fn test_set_color_does_not_reset_position() {
let mut effect = KnightRiderEffect::new(12)
.unwrap()
.with_color(RED)
.with_tail_length(0)
.with_speed(3)
.unwrap();
let mut buffer = [RGB8::default(); 12];
for _ in 0..3 {
effect.update(&mut buffer).unwrap();
}
let mut before = [RGB8::default(); 12];
effect.current(&mut before).unwrap();
let mut lit_before = [usize::MAX; 2];
let mut lit_before_count = 0;
for (i, led) in before.iter().enumerate() {
if *led != BLACK {
lit_before[lit_before_count] = i;
lit_before_count += 1;
}
}
assert_eq!(
lit_before_count, 2,
"expected exactly two lit head positions"
);
effect.set_color(RGB8::new(0, 0, 255));
let mut after = [RGB8::default(); 12];
effect.current(&mut after).unwrap();
let mut lit_after = [usize::MAX; 2];
let mut lit_after_count = 0;
for (i, led) in after.iter().enumerate() {
if *led != BLACK {
lit_after[lit_after_count] = i;
lit_after_count += 1;
}
}
assert_eq!(
lit_after_count, 2,
"expected exactly two lit head positions"
);
assert_eq!(
lit_before, lit_after,
"lit positions must be unchanged after set_color"
);
}
#[test]
fn test_trait_object_update() {
let mut effect = KnightRiderEffect::new(8)
.unwrap()
.with_color(RED)
.with_speed(2)
.unwrap();
let effect_ref: &mut dyn Effect = &mut effect;
let mut buf1 = [RGB8::default(); 8];
let mut buf2 = [RGB8::default(); 8];
effect_ref.update(&mut buf1).unwrap();
effect_ref.update(&mut buf2).unwrap();
assert_ne!(
buf1, buf2,
"KnightRider should advance between trait-object updates"
);
}
#[test]
fn test_trait_reset_path() {
let mut effect = KnightRiderEffect::new(8).unwrap().with_tail_length(0);
let mut buf_before = [RGB8::default(); 8];
let mut buf_after = [RGB8::default(); 8];
let effect_ref: &mut dyn Effect = &mut effect;
effect_ref.update(&mut buf_before).unwrap();
effect_ref.reset();
effect_ref.update(&mut buf_after).unwrap();
assert_eq!(
buf_before, buf_after,
"trait reset must replay the same first step"
);
}
}