M2 complete: LP5562 engine patterns running autonomously
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>
This commit is contained in:
262
src/pattern/mod.rs
Normal file
262
src/pattern/mod.rs
Normal file
@@ -0,0 +1,262 @@
|
||||
//! 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user