Add src/pattern/ module with 3 engine patterns: - Breathe: smooth ramp up/down (~2.5s cycle, 1 engine) - Heartbeat: double-pulse with long rest (~1.6s cycle, 1 engine) - RGB cycle: phase-offset breathing via trigger sync (3 engines) Patterns support both RGBW and 3-channel monochrome LED modes via LED_MAP configuration. Main loop cycles through all 3 patterns (8s each) while LP5562 runs them autonomously — MCU just idles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
263 lines
11 KiB
Rust
263 lines
11 KiB
Rust
//! Predefined LP5562 engine patterns for xblink.
|
||
//!
|
||
//! Each pattern is a set of engine programs + LED_MAP configuration.
|
||
//! The LP5562 runs these autonomously — the MCU can sleep after loading.
|
||
|
||
use crate::led::lp5562::{
|
||
Channel, EngineCommand, EngineId, EngineProgram, LedMapping, Lp5562, Prescale, RampDirection,
|
||
};
|
||
use embedded_hal::i2c::I2c;
|
||
|
||
/// LED hardware configuration.
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub enum LedMode {
|
||
/// Single RGBW LED (e.g., LP5562EVM D1). Engines map to R, G, B; W is I2C-direct.
|
||
Rgbw,
|
||
/// 3 independent monochrome LEDs. Engines map to B, G, R channels (one each).
|
||
Mono3,
|
||
}
|
||
|
||
/// A complete pattern: up to 3 engine programs + LED mapping.
|
||
pub struct Pattern {
|
||
pub engine1: Option<EngineProgram>,
|
||
pub engine2: Option<EngineProgram>,
|
||
pub engine3: Option<EngineProgram>,
|
||
/// LED_MAP: which engine (or I2C direct) drives each channel.
|
||
/// Index: [B, G, R, W] → LedMapping value.
|
||
pub map_b: LedMapping,
|
||
pub map_g: LedMapping,
|
||
pub map_r: LedMapping,
|
||
pub map_w: LedMapping,
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Breathing: smooth ramp up/down, ~2.5s cycle
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Breathing pattern — one engine, smooth sine-like ramp.
|
||
///
|
||
/// Slow prescale (15.6ms/step):
|
||
/// Ramp up: step_time=1, increment=4 → 64 steps × 15.6ms ≈ 1.0s (0→252)
|
||
/// Ramp down: step_time=1, increment=4 → 64 steps × 15.6ms ≈ 1.0s (252→0)
|
||
/// Wait: step_time=32 → 32 × 15.6ms ≈ 0.5s pause at bottom
|
||
/// Total: ~2.5s per cycle
|
||
fn breathe_program() -> EngineProgram {
|
||
EngineProgram::from_commands(&[
|
||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 4), // 0→252, ~1s
|
||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 4), // 252→0, ~1s
|
||
EngineCommand::wait(Prescale::Slow, 32), // ~0.5s pause
|
||
EngineCommand::branch(0, 0), // loop forever
|
||
])
|
||
}
|
||
|
||
/// Load breathing pattern. All active channels breathe in sync.
|
||
pub fn breathe(mode: LedMode) -> Pattern {
|
||
let prog = breathe_program();
|
||
match mode {
|
||
LedMode::Rgbw => Pattern {
|
||
engine1: Some(prog),
|
||
engine2: None,
|
||
engine3: None,
|
||
// Engine1 drives R, G, B; W = I2C direct (off or static)
|
||
map_b: LedMapping::Engine1,
|
||
map_g: LedMapping::Engine1,
|
||
map_r: LedMapping::Engine1,
|
||
map_w: LedMapping::I2c,
|
||
},
|
||
LedMode::Mono3 => Pattern {
|
||
engine1: Some(prog),
|
||
engine2: None,
|
||
engine3: None,
|
||
// Engine1 drives all 3 mono LEDs in sync
|
||
map_b: LedMapping::Engine1,
|
||
map_g: LedMapping::Engine1,
|
||
map_r: LedMapping::Engine1,
|
||
map_w: LedMapping::I2c,
|
||
},
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Heartbeat: double-pulse with long pause, ~1.6s cycle
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Heartbeat pattern — fast double-pulse, long rest.
|
||
///
|
||
/// Fast prescale (0.49ms/step):
|
||
/// set_pwm 255 → snap on
|
||
/// wait fast, st=20 → 20 × 0.49ms ≈ 10ms hold
|
||
/// set_pwm 0 → snap off
|
||
/// wait fast, st=40 → 40 × 0.49ms ≈ 20ms gap
|
||
/// set_pwm 255 → second beat
|
||
/// wait fast, st=20 → 10ms hold
|
||
/// set_pwm 0 → snap off
|
||
/// Slow prescale for the long rest:
|
||
/// wait slow, st=63 → 63 × 15.6ms ≈ 1.0s
|
||
/// wait slow, st=32 → 32 × 15.6ms ≈ 0.5s (total rest ~1.5s)
|
||
/// branch 0 → loop
|
||
fn heartbeat_program() -> EngineProgram {
|
||
EngineProgram::from_commands(&[
|
||
EngineCommand::set_pwm(255), // 0: first beat ON
|
||
EngineCommand::wait(Prescale::Fast, 20), // 1: hold ~10ms
|
||
EngineCommand::set_pwm(0), // 2: first beat OFF
|
||
EngineCommand::wait(Prescale::Fast, 40), // 3: gap ~20ms
|
||
EngineCommand::set_pwm(255), // 4: second beat ON
|
||
EngineCommand::wait(Prescale::Fast, 20), // 5: hold ~10ms
|
||
EngineCommand::set_pwm(0), // 6: second beat OFF
|
||
EngineCommand::wait(Prescale::Slow, 63), // 7: rest ~1.0s
|
||
EngineCommand::wait(Prescale::Slow, 32), // 8: rest ~0.5s
|
||
EngineCommand::branch(0, 0), // 9: loop forever
|
||
])
|
||
}
|
||
|
||
/// Load heartbeat pattern.
|
||
pub fn heartbeat(mode: LedMode) -> Pattern {
|
||
let prog = heartbeat_program();
|
||
match mode {
|
||
LedMode::Rgbw => Pattern {
|
||
engine1: Some(prog),
|
||
engine2: None,
|
||
engine3: None,
|
||
map_b: LedMapping::Engine1,
|
||
map_g: LedMapping::Engine1,
|
||
map_r: LedMapping::Engine1,
|
||
map_w: LedMapping::I2c,
|
||
},
|
||
LedMode::Mono3 => Pattern {
|
||
engine1: Some(prog),
|
||
engine2: None,
|
||
engine3: None,
|
||
map_b: LedMapping::Engine1,
|
||
map_g: LedMapping::Engine1,
|
||
map_r: LedMapping::Engine1,
|
||
map_w: LedMapping::I2c,
|
||
},
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// RGB cycle / staggered chase: 3 engines, trigger-synced phase offset
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Phase-offset breathing using triggers for synchronization.
|
||
///
|
||
/// Engine 1 (leader): breathe up, send trigger, breathe down, wait, send trigger, loop
|
||
/// Engine 2 (follower): wait for trigger, breathe up, breathe down, wait, wait for trigger, loop
|
||
/// Engine 3 (follower): wait for trigger from E2, breathe up, breathe down, wait, loop
|
||
///
|
||
/// The trigger chain creates a 3-phase offset: E1 starts, signals E2, E2 signals E3.
|
||
fn rgb_cycle_engine1() -> EngineProgram {
|
||
EngineProgram::from_commands(&[
|
||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 4), // 0: ramp up ~1s
|
||
EngineCommand::trigger(0, 0b010), // 1: send to E2
|
||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 4), // 2: ramp down ~1s
|
||
EngineCommand::wait(Prescale::Slow, 32), // 3: pause ~0.5s
|
||
EngineCommand::wait(Prescale::Slow, 32), // 4: pause ~0.5s
|
||
EngineCommand::branch(0, 0), // 5: loop forever
|
||
])
|
||
}
|
||
|
||
fn rgb_cycle_engine2() -> EngineProgram {
|
||
EngineProgram::from_commands(&[
|
||
EngineCommand::trigger(0b001, 0), // 0: wait for E1
|
||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 4), // 1: ramp up ~1s
|
||
EngineCommand::trigger(0, 0b100), // 2: send to E3
|
||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 4), // 3: ramp down ~1s
|
||
EngineCommand::wait(Prescale::Slow, 32), // 4: pause ~0.5s
|
||
EngineCommand::branch(0, 0), // 5: loop forever
|
||
])
|
||
}
|
||
|
||
fn rgb_cycle_engine3() -> EngineProgram {
|
||
EngineProgram::from_commands(&[
|
||
EngineCommand::trigger(0b010, 0), // 0: wait for E2
|
||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 4), // 1: ramp up ~1s
|
||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 4), // 2: ramp down ~1s
|
||
EngineCommand::wait(Prescale::Slow, 32), // 3: pause ~0.5s
|
||
EngineCommand::branch(0, 0), // 4: loop forever
|
||
])
|
||
}
|
||
|
||
/// Load RGB cycle (RGBW mode) or staggered chase (mono mode).
|
||
pub fn rgb_cycle(mode: LedMode) -> Pattern {
|
||
match mode {
|
||
LedMode::Rgbw => Pattern {
|
||
engine1: Some(rgb_cycle_engine1()),
|
||
engine2: Some(rgb_cycle_engine2()),
|
||
engine3: Some(rgb_cycle_engine3()),
|
||
// Each engine drives one color channel
|
||
map_b: LedMapping::Engine1,
|
||
map_g: LedMapping::Engine2,
|
||
map_r: LedMapping::Engine3,
|
||
map_w: LedMapping::I2c,
|
||
},
|
||
LedMode::Mono3 => Pattern {
|
||
engine1: Some(rgb_cycle_engine1()),
|
||
engine2: Some(rgb_cycle_engine2()),
|
||
engine3: Some(rgb_cycle_engine3()),
|
||
// Each engine drives one physical LED
|
||
map_b: LedMapping::Engine1,
|
||
map_g: LedMapping::Engine2,
|
||
map_r: LedMapping::Engine3,
|
||
map_w: LedMapping::I2c,
|
||
},
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Staggered breathe: 3 engines, same breathe program, phase-offset
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Load staggered breathing — all 3 channels breathe independently with phase offset.
|
||
pub fn staggered_breathe(mode: LedMode) -> Pattern {
|
||
// Same trigger-chain structure as rgb_cycle but using the same
|
||
// visual effect (breathe) on each channel. In RGBW mode this creates
|
||
// a color-shifting breathe; in mono mode it's a traveling wave.
|
||
rgb_cycle(mode)
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Pattern loader
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Load a pattern into the LP5562 and start engines running.
|
||
///
|
||
/// Caller must have already called `lp.enable()` and `lp.init_direct_control()`.
|
||
/// This function handles the full sequence: stop engines → set LED map → load programs → run.
|
||
pub fn load_pattern<I2C, E>(
|
||
lp: &mut Lp5562<I2C>,
|
||
pattern: &Pattern,
|
||
delay: &mut impl embedded_hal::delay::DelayNs,
|
||
) -> Result<(), crate::led::lp5562::Error<E>>
|
||
where
|
||
I2C: I2c<Error = E>,
|
||
{
|
||
// Stop all engines first
|
||
lp.stop_engine(EngineId::Engine1).map_err(crate::led::lp5562::Error::I2c)?;
|
||
lp.stop_engine(EngineId::Engine2).map_err(crate::led::lp5562::Error::I2c)?;
|
||
lp.stop_engine(EngineId::Engine3).map_err(crate::led::lp5562::Error::I2c)?;
|
||
delay.delay_us(200); // >153us between OP_MODE writes
|
||
|
||
// Set LED mapping
|
||
lp.set_led_mapping(Channel::Blue, pattern.map_b).map_err(crate::led::lp5562::Error::I2c)?;
|
||
lp.set_led_mapping(Channel::Green, pattern.map_g).map_err(crate::led::lp5562::Error::I2c)?;
|
||
lp.set_led_mapping(Channel::Red, pattern.map_r).map_err(crate::led::lp5562::Error::I2c)?;
|
||
lp.set_led_mapping(Channel::White, pattern.map_w).map_err(crate::led::lp5562::Error::I2c)?;
|
||
|
||
// Load and run each engine that has a program
|
||
if let Some(ref prog) = pattern.engine1 {
|
||
lp.run_engine(EngineId::Engine1, prog)?;
|
||
delay.delay_us(200);
|
||
}
|
||
if let Some(ref prog) = pattern.engine2 {
|
||
lp.run_engine(EngineId::Engine2, prog)?;
|
||
delay.delay_us(200);
|
||
}
|
||
if let Some(ref prog) = pattern.engine3 {
|
||
lp.run_engine(EngineId::Engine3, prog)?;
|
||
delay.delay_us(200);
|
||
}
|
||
|
||
Ok(())
|
||
}
|