diff --git a/docs/STATUS.md b/docs/STATUS.md index 2023844..a43ad4f 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -1,6 +1,6 @@ # xblink Project Status -**Current Milestone**: M2 — Engine Patterns +**Current Milestone**: M3 — Hardcoded Pattern Library **Last Updated**: 2026-03-05 --- @@ -28,11 +28,11 @@ ### M2: Engine Patterns -- [ ] Build breathing pattern (1 engine, ramp up/down, branch loop) -- [ ] Build heartbeat pattern (1 engine, fast ramp, slow decay) -- [ ] Build RGB color cycle (3 engines, trigger sync) -- [ ] Verify LP5562 runs patterns autonomously (MCU idle loop) -- [ ] Verify MCU can stop and switch patterns +- [x] Build breathing pattern (1 engine, ramp up/down, branch loop) +- [x] Build heartbeat pattern (1 engine, fast ramp, slow decay) +- [x] Build RGB cycle (3 engines, trigger-synced phase offset) +- [x] Verify LP5562 runs patterns autonomously (MCU idle loop) +- [x] Verify MCU can stop and switch patterns (8s cycle between all 3) ### M3: Hardcoded Pattern Library diff --git a/src/main.rs b/src/main.rs index 524b5e8..bcc4c40 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,9 @@ use hal::prelude::*; use pac::{CorePeripherals, Peripherals}; mod led; +mod pattern; use led::lp5562::{ClockSource, Lp5562, DEFAULT_ADDRESS}; +use pattern::LedMode; /// Blink the on-board LED n times (active-low: low=on, high=off) fn blink(led: &mut bsp::Led0, delay: &mut D, times: u8, ms: u16) { @@ -80,25 +82,34 @@ fn main() -> ! { Ok(()) => { // === DIAGNOSTIC: solid LED on = LP5562 init OK === led.set_low().unwrap(); - delay.delay_ms(1000u32); + delay.delay_ms(500u32); + led.set_high().unwrap(); - // M1 smoke test: cycle through RGBW channels - let on: u8 = 255; - let step: u16 = 500; + // LED mode: Rgbw for EVM RGB LED, Mono3 for 3 separate LEDs + let mode = LedMode::Rgbw; + // Cycle through patterns, 8 seconds each + let patterns = [ + pattern::breathe(mode), + pattern::heartbeat(mode), + pattern::rgb_cycle(mode), + ]; + + let mut idx = 0; loop { - lp.set_all_pwm(on, 0, 0, 0).ok(); // Blue - delay.delay_ms(step as u32); - lp.set_all_pwm(0, on, 0, 0).ok(); // Green - delay.delay_ms(step as u32); - lp.set_all_pwm(0, 0, on, 0).ok(); // Red - delay.delay_ms(step as u32); - lp.set_all_pwm(0, 0, 0, on).ok(); // White - delay.delay_ms(step as u32); - lp.set_all_pwm(on, on, on, on).ok(); // All on - delay.delay_ms(step as u32); - let _ = lp.all_off(); - delay.delay_ms(step as u32); + // Blink pattern index (1, 2, or 3) to indicate which pattern + blink(&mut led, &mut delay, (idx + 1) as u8, 150); + + // Load pattern — LP5562 runs autonomously + if pattern::load_pattern(&mut lp, &patterns[idx], &mut delay).is_err() { + // I2C error loading pattern — fast blink + blink(&mut led, &mut delay, 10, 50); + } + + // MCU idles while LP5562 runs the pattern + delay.delay_ms(8000u32); + + idx = (idx + 1) % patterns.len(); } } Err(_) => { diff --git a/src/pattern/mod.rs b/src/pattern/mod.rs new file mode 100644 index 0000000..7641d6c --- /dev/null +++ b/src/pattern/mod.rs @@ -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, + 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(()) +}