use crate::lighting::effects::*;
use crate::lighting::engine::tests::common::create_test_fixture;
use crate::lighting::engine::EffectEngine;
use std::collections::HashMap;
use std::time::Duration;
fn create_fixture_with_strobe_offset(
name: &str,
universe: u16,
address: u16,
max_freq: f64,
min_freq: f64,
dmx_offset: u8,
) -> FixtureInfo {
let mut channels = HashMap::new();
channels.insert("dimmer".to_string(), 1);
channels.insert("red".to_string(), 2);
channels.insert("green".to_string(), 3);
channels.insert("blue".to_string(), 4);
channels.insert("white".to_string(), 5);
channels.insert("strobe".to_string(), 6);
let mut fixture = FixtureInfo::new(
name.to_string(),
universe,
address,
"RGBW_Strobe".to_string(),
channels,
Some(max_freq),
);
fixture.min_strobe_frequency = Some(min_freq);
fixture.strobe_dmx_offset = Some(dmx_offset);
fixture
}
#[test]
fn test_strobe_boundary_at_duty_cycle_transition() {
let mut engine = EffectEngine::new();
let mut channels = HashMap::new();
channels.insert("dimmer".to_string(), 1);
channels.insert("red".to_string(), 2);
channels.insert("green".to_string(), 3);
channels.insert("blue".to_string(), 4);
let fixture = FixtureInfo::new(
"test_fixture".to_string(),
1,
1,
"RGB".to_string(),
channels,
None,
);
engine.register_fixture(fixture);
let effect = EffectInstance::new(
"test_effect".to_string(),
EffectType::Strobe {
frequency: TempoAwareFrequency::Fixed(2.0),
duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
engine.start_effect(effect).unwrap();
let commands = engine.update(Duration::from_millis(0), None).unwrap();
let dimmer_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
assert_eq!(dimmer_cmd.value, 255, "At t=0ms strobe should be ON");
let commands = engine.update(Duration::from_millis(249), None).unwrap();
let dimmer_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
assert_eq!(
dimmer_cmd.value, 255,
"At t=249ms strobe should still be ON"
);
let commands = engine.update(Duration::from_millis(2), None).unwrap();
let dimmer_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
assert_eq!(dimmer_cmd.value, 0, "At t=251ms strobe should be OFF");
let commands = engine.update(Duration::from_millis(249), None).unwrap();
let dimmer_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
assert_eq!(
dimmer_cmd.value, 255,
"At t=500ms strobe should be ON again"
);
}
#[test]
fn test_strobe_effect() {
let mut engine = EffectEngine::new();
let fixture = create_test_fixture("test_fixture", 1, 1);
engine.register_fixture(fixture);
let effect = EffectInstance::new(
"test_effect".to_string(),
EffectType::Strobe {
frequency: TempoAwareFrequency::Fixed(2.0), duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
engine.start_effect(effect).unwrap();
let commands = engine.update(Duration::from_millis(16), None).unwrap();
assert_eq!(commands.len(), 1);
let strobe_cmd = commands.iter().find(|cmd| cmd.channel == 6).unwrap();
assert_eq!(strobe_cmd.value, 25);
}
#[test]
fn test_clear_layer_resets_strobe_channel() {
let mut engine = EffectEngine::new();
let fixture = create_test_fixture("test_fixture", 1, 1);
engine.register_fixture(fixture);
let mut strobe_effect = EffectInstance::new(
"strobe_effect".to_string(),
EffectType::Strobe {
frequency: TempoAwareFrequency::Fixed(5.0), duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
strobe_effect.layer = EffectLayer::Foreground;
engine.start_effect(strobe_effect).unwrap();
let commands_before = engine.update(Duration::from_millis(16), None).unwrap();
let strobe_cmd_before = commands_before.iter().find(|cmd| cmd.channel == 6);
assert!(
strobe_cmd_before.is_some(),
"Should have strobe command before clear"
);
let strobe_value_before = strobe_cmd_before.unwrap().value;
assert!(
strobe_value_before > 0,
"Strobe channel should be non-zero before clear: {}",
strobe_value_before
);
engine.clear_layer(EffectLayer::Foreground);
assert_eq!(engine.active_effects_count(), 0);
assert!(!engine.has_effect("strobe_effect"));
let commands_after = engine.update(Duration::from_millis(16), None).unwrap();
let strobe_cmd_after = commands_after.iter().find(|cmd| cmd.channel == 6);
assert!(
strobe_cmd_after.is_none(),
"No strobe command after clear (no active effects)"
);
}
#[test]
fn test_clear_all_layers_resets_strobe_channel() {
let mut engine = EffectEngine::new();
let fixture = create_test_fixture("test_fixture", 1, 1);
engine.register_fixture(fixture);
let mut bg_strobe = EffectInstance::new(
"bg_strobe".to_string(),
EffectType::Strobe {
frequency: TempoAwareFrequency::Fixed(3.0),
duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
bg_strobe.layer = EffectLayer::Background;
let mut fg_strobe = EffectInstance::new(
"fg_strobe".to_string(),
EffectType::Strobe {
frequency: TempoAwareFrequency::Fixed(4.0),
duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
fg_strobe.layer = EffectLayer::Foreground;
engine.start_effect(bg_strobe).unwrap();
engine.start_effect(fg_strobe).unwrap();
let commands_before = engine.update(Duration::from_millis(16), None).unwrap();
let strobe_cmd_before = commands_before.iter().find(|cmd| cmd.channel == 6);
assert!(
strobe_cmd_before.is_some(),
"Should have strobe command before clear"
);
let strobe_value_before = strobe_cmd_before.unwrap().value;
assert!(
strobe_value_before > 0,
"Strobe channel should be non-zero before clear: {}",
strobe_value_before
);
engine.clear_all_layers();
assert_eq!(engine.active_effects_count(), 0);
let commands_after = engine.update(Duration::from_millis(16), None).unwrap();
let strobe_cmd_after = commands_after.iter().find(|cmd| cmd.channel == 6);
assert!(
strobe_cmd_after.is_none(),
"No strobe command after clear_all_layers (no active effects)"
);
}
#[test]
fn test_strobe_with_dmx_offset() {
let mut engine = EffectEngine::new();
let fixture = create_fixture_with_strobe_offset("test_fixture", 1, 1, 25.0, 0.4, 7);
engine.register_fixture(fixture);
let effect = EffectInstance::new(
"test_effect".to_string(),
EffectType::Strobe {
frequency: TempoAwareFrequency::Fixed(10.0),
duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
engine.start_effect(effect).unwrap();
let commands = engine.update(Duration::from_millis(16), None).unwrap();
let strobe_cmd = commands.iter().find(|cmd| cmd.channel == 6).unwrap();
assert_eq!(
strobe_cmd.value, 248,
"10Hz strobe with offset should produce DMX 248"
);
}
#[test]
fn test_strobe_without_offset_unchanged() {
let mut engine = EffectEngine::new();
let fixture = create_test_fixture("test_fixture", 1, 1);
engine.register_fixture(fixture);
let effect = EffectInstance::new(
"test_effect".to_string(),
EffectType::Strobe {
frequency: TempoAwareFrequency::Fixed(2.0),
duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
engine.start_effect(effect).unwrap();
let commands = engine.update(Duration::from_millis(16), None).unwrap();
let strobe_cmd = commands.iter().find(|cmd| cmd.channel == 6).unwrap();
assert_eq!(
strobe_cmd.value, 25,
"2Hz strobe without offset should still produce DMX 25"
);
}
fn create_pixelbrick_fixture(name: &str, universe: u16, address: u16) -> FixtureInfo {
let mut channels = HashMap::new();
channels.insert("red".to_string(), 1);
channels.insert("green".to_string(), 2);
channels.insert("blue".to_string(), 3);
channels.insert("strobe".to_string(), 4);
let mut fixture = FixtureInfo::new(
name.to_string(),
universe,
address,
"Astera-PixelBrick".to_string(),
channels,
Some(25.0),
);
fixture.min_strobe_frequency = Some(0.4);
fixture.strobe_dmx_offset = Some(7);
fixture
}
#[test]
fn test_pixelbrick_strobe_with_concurrent_effects() {
let mut engine = EffectEngine::new();
let fixture = create_pixelbrick_fixture("Brick1", 1, 1);
engine.register_fixture(fixture);
let mut bg_cycle = EffectInstance::new(
"bg_cycle".to_string(),
EffectType::ColorCycle {
colors: vec![Color::new(255, 255, 255), Color::new(255, 68, 0)],
speed: TempoAwareSpeed::Fixed(0.5),
direction: CycleDirection::Forward,
transition: CycleTransition::Fade,
duration: Duration::from_secs(10),
},
vec!["Brick1".to_string()],
None,
None,
None,
);
bg_cycle.layer = EffectLayer::Background;
bg_cycle.blend_mode = BlendMode::Replace;
engine.start_effect(bg_cycle).unwrap();
let mut mid_chase = EffectInstance::new(
"mid_chase".to_string(),
EffectType::Chase {
pattern: ChasePattern::Random,
speed: TempoAwareSpeed::Fixed(1.0),
direction: ChaseDirection::LeftToRight,
transition: CycleTransition::Fade,
duration: Duration::from_secs(10),
},
vec!["Brick1".to_string()],
None,
None,
None,
);
mid_chase.layer = EffectLayer::Midground;
mid_chase.blend_mode = BlendMode::Add;
engine.start_effect(mid_chase).unwrap();
let mut fg_chase = EffectInstance::new(
"fg_chase".to_string(),
EffectType::Chase {
pattern: ChasePattern::Random,
speed: TempoAwareSpeed::Fixed(2.0),
direction: ChaseDirection::LeftToRight,
transition: CycleTransition::Fade,
duration: Duration::from_secs(10),
},
vec!["Brick1".to_string()],
None,
None,
None,
);
fg_chase.layer = EffectLayer::Foreground;
fg_chase.blend_mode = BlendMode::Screen;
engine.start_effect(fg_chase).unwrap();
let mut fg_pulse = EffectInstance::new(
"fg_pulse".to_string(),
EffectType::Pulse {
base_level: 0.5,
pulse_amplitude: 0.5,
frequency: TempoAwareFrequency::Fixed(3.0),
duration: Duration::from_secs(5),
},
vec!["Brick1".to_string()],
None,
None,
None,
);
fg_pulse.layer = EffectLayer::Foreground;
fg_pulse.blend_mode = BlendMode::Overlay;
engine.start_effect(fg_pulse).unwrap();
let mut fg_strobe = EffectInstance::new(
"fg_strobe".to_string(),
EffectType::Strobe {
frequency: TempoAwareFrequency::Fixed(10.0),
duration: Duration::from_secs(5),
},
vec!["Brick1".to_string()],
None,
None,
None,
);
fg_strobe.layer = EffectLayer::Foreground;
fg_strobe.blend_mode = BlendMode::Overlay;
engine.start_effect(fg_strobe).unwrap();
for frame in 0..10 {
let commands = engine.update(Duration::from_millis(16), None).unwrap();
let strobe_cmd = commands.iter().find(|cmd| cmd.channel == 4);
assert!(
strobe_cmd.is_some(),
"Frame {}: strobe DMX channel 4 missing from output (got {} commands: {:?})",
frame,
commands.len(),
commands
.iter()
.map(|c| (c.channel, c.value))
.collect::<Vec<_>>()
);
let strobe_value = strobe_cmd.unwrap().value;
assert_eq!(
strobe_value, 248,
"Frame {}: strobe DMX value should be 248, got {}",
frame, strobe_value
);
let has_red = commands.iter().any(|cmd| cmd.channel == 1);
let has_green = commands.iter().any(|cmd| cmd.channel == 2);
let has_blue = commands.iter().any(|cmd| cmd.channel == 3);
assert!(has_red, "Frame {}: red channel missing", frame);
assert!(has_green, "Frame {}: green channel missing", frame);
assert!(has_blue, "Frame {}: blue channel missing", frame);
}
}
#[test]
fn test_esaweg_timeline_strobe_sequence() {
let mut engine = EffectEngine::new();
let fixture = create_pixelbrick_fixture("Brick1", 1, 1);
engine.register_fixture(fixture);
let dt = Duration::from_millis(23);
engine.clear_all_layers();
let mut bg_cycle = EffectInstance::new(
"bg_cycle".to_string(),
EffectType::ColorCycle {
colors: vec![Color::new(255, 255, 255), Color::new(255, 68, 0)],
speed: TempoAwareSpeed::Fixed(0.5),
direction: CycleDirection::Forward,
transition: CycleTransition::Fade,
duration: Duration::from_secs(10),
},
vec!["Brick1".to_string()],
None,
None,
None,
);
bg_cycle.layer = EffectLayer::Background;
bg_cycle.blend_mode = BlendMode::Replace;
engine.start_effect(bg_cycle).unwrap();
let mut left_chase = EffectInstance::new(
"left_chase".to_string(),
EffectType::Chase {
pattern: ChasePattern::Random,
speed: TempoAwareSpeed::Fixed(2.0),
direction: ChaseDirection::LeftToRight,
transition: CycleTransition::Fade,
duration: Duration::from_secs(10),
},
vec!["Brick1".to_string()],
None,
None,
None,
);
left_chase.layer = EffectLayer::Foreground;
left_chase.blend_mode = BlendMode::Screen;
engine.start_effect(left_chase).unwrap();
let mut right_chase = EffectInstance::new(
"right_chase".to_string(),
EffectType::Chase {
pattern: ChasePattern::Random,
speed: TempoAwareSpeed::Fixed(2.0),
direction: ChaseDirection::RightToLeft,
transition: CycleTransition::Fade,
duration: Duration::from_secs(10),
},
vec!["Brick1".to_string()],
None,
None,
None,
);
right_chase.layer = EffectLayer::Foreground;
right_chase.blend_mode = BlendMode::Screen;
engine.start_effect(right_chase).unwrap();
let mut mid_chase = EffectInstance::new(
"mid_chase".to_string(),
EffectType::Chase {
pattern: ChasePattern::Random,
speed: TempoAwareSpeed::Fixed(1.0),
direction: ChaseDirection::LeftToRight,
transition: CycleTransition::Fade,
duration: Duration::from_secs(10),
},
vec!["Brick1".to_string()],
None,
None,
None,
);
mid_chase.layer = EffectLayer::Midground;
mid_chase.blend_mode = BlendMode::Add;
engine.start_effect(mid_chase).unwrap();
for _ in 0..20 {
let commands = engine.update(dt, None).unwrap();
let strobe = commands.iter().find(|c| c.channel == 4);
assert!(
strobe.is_none(),
"Strobe channel should not be present before @228/1"
);
}
let mut fg_pulse = EffectInstance::new(
"fg_pulse".to_string(),
EffectType::Pulse {
base_level: 0.5,
pulse_amplitude: 0.5,
frequency: TempoAwareFrequency::Fixed(3.0),
duration: Duration::from_secs(5),
},
vec!["Brick1".to_string()],
None,
None,
None,
);
fg_pulse.layer = EffectLayer::Foreground;
fg_pulse.blend_mode = BlendMode::Overlay;
engine.start_effect(fg_pulse).unwrap();
for _ in 0..40 {
let commands = engine.update(dt, None).unwrap();
let strobe = commands.iter().find(|c| c.channel == 4);
assert!(
strobe.is_none(),
"Strobe channel should not be present before @236/1"
);
}
let mut fg_strobe = EffectInstance::new(
"fg_strobe".to_string(),
EffectType::Strobe {
frequency: TempoAwareFrequency::Fixed(10.0),
duration: Duration::from_secs(5),
},
vec!["Brick1".to_string()],
None,
None,
None,
);
fg_strobe.layer = EffectLayer::Foreground;
fg_strobe.blend_mode = BlendMode::Overlay;
engine.start_effect(fg_strobe).unwrap();
assert_eq!(
engine.active_effects_count(),
6,
"All 6 effects should be active: bg_cycle, left_chase, right_chase, mid_chase, fg_pulse, fg_strobe"
);
let mut strobe_present_count = 0;
for frame in 0..61 {
let commands = engine.update(dt, None).unwrap();
let strobe_cmd = commands.iter().find(|c| c.channel == 4);
assert!(
strobe_cmd.is_some(),
"Frame {} after strobe start: strobe channel 4 should be present (got {:?})",
frame,
commands
.iter()
.map(|c| (c.channel, c.value))
.collect::<Vec<_>>()
);
let strobe_value = strobe_cmd.unwrap().value;
assert_eq!(
strobe_value, 248,
"Frame {} after strobe start: strobe should be DMX 248, got {}",
frame, strobe_value
);
strobe_present_count += 1;
assert!(
commands.iter().any(|c| c.channel == 1),
"Frame {}: red missing",
frame
);
}
assert_eq!(
strobe_present_count, 61,
"Strobe should be present on all 61 frames"
);
engine.clear_all_layers();
let mut black_static = EffectInstance::new(
"black_static".to_string(),
EffectType::Static {
parameters: {
let mut p = HashMap::new();
p.insert("red".to_string(), 0.0);
p.insert("green".to_string(), 0.0);
p.insert("blue".to_string(), 0.0);
p
},
duration: Duration::from_secs(5),
},
vec!["Brick1".to_string()],
None,
None,
None,
);
black_static.layer = EffectLayer::Background;
black_static.blend_mode = BlendMode::Replace;
engine.start_effect(black_static).unwrap();
let commands = engine.update(dt, None).unwrap();
let strobe_cmd = commands.iter().find(|c| c.channel == 4);
assert!(
strobe_cmd.is_none(),
"Strobe channel should not be present after clear (no active strobe)"
);
let red_cmd = commands.iter().find(|c| c.channel == 1);
assert!(
red_cmd.is_some(),
"Red channel should be present after clear"
);
assert_eq!(red_cmd.unwrap().value, 0, "Red should be 0 (black)");
}
#[test]
fn test_pixelbrick_orange_static_with_strobe_dmx_values() {
let mut engine = EffectEngine::new();
let fixture = create_pixelbrick_fixture("Brick1", 1, 1);
engine.register_fixture(fixture);
let orange_static = EffectInstance::new(
"orange_static".to_string(),
EffectType::Static {
parameters: {
let mut p = HashMap::new();
p.insert("red".to_string(), 1.0);
p.insert("green".to_string(), 0.4);
p.insert("blue".to_string(), 0.0);
p
},
duration: Duration::from_secs(5),
},
vec!["Brick1".to_string()],
None,
None,
None,
);
engine.start_effect(orange_static).unwrap();
let mut strobe = EffectInstance::new(
"strobe_10hz".to_string(),
EffectType::Strobe {
frequency: TempoAwareFrequency::Fixed(10.0),
duration: Duration::from_secs(5),
},
vec!["Brick1".to_string()],
None,
None,
None,
);
strobe.layer = EffectLayer::Foreground;
strobe.blend_mode = BlendMode::Overlay;
engine.start_effect(strobe).unwrap();
let commands = engine.update(Duration::from_millis(16), None).unwrap();
let mut sorted: Vec<(u16, u8)> = commands.iter().map(|c| (c.channel, c.value)).collect();
sorted.sort_by_key(|&(ch, _)| ch);
eprintln!("DMX commands for PixelBrick orange+strobe: {:?}", sorted);
assert_eq!(
sorted.len(),
4,
"Expected 4 DMX commands (R,G,B,strobe), got {}: {:?}",
sorted.len(),
sorted
);
let red = sorted.iter().find(|&&(ch, _)| ch == 1).unwrap();
assert_eq!(red.1, 255, "Red channel should be 255, got {}", red.1);
let green = sorted.iter().find(|&&(ch, _)| ch == 2).unwrap();
assert_eq!(green.1, 102, "Green channel should be 102, got {}", green.1);
let blue = sorted.iter().find(|&&(ch, _)| ch == 3).unwrap();
assert_eq!(blue.1, 0, "Blue channel should be 0, got {}", blue.1);
let strobe_val = sorted.iter().find(|&&(ch, _)| ch == 4).unwrap();
assert_eq!(
strobe_val.1, 248,
"Strobe channel should be 248, got {}",
strobe_val.1
);
}