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:
michael
2026-03-05 12:17:19 -08:00
parent e45273bc71
commit e7bc8834b2
3 changed files with 295 additions and 22 deletions

View File

@@ -1,6 +1,6 @@
# xblink Project Status
**Current Milestone**: M2Engine Patterns
**Current Milestone**: M3Hardcoded 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

View File

@@ -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<D: embedded_hal::delay::DelayNs>(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(_) => {

262
src/pattern/mod.rs Normal file
View 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(())
}