Files
xblink/src/pattern/mod.rs
michael e7bc8834b2 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>
2026-03-05 12:17:19 -08:00

263 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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(())
}