//! 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, pub engine2: Option, pub engine3: Option, /// 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( lp: &mut Lp5562, pattern: &Pattern, delay: &mut impl embedded_hal::delay::DelayNs, ) -> Result<(), crate::led::lp5562::Error> where I2C: I2c, { // 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(()) }