//! Minimal status LED driver for the CMDR keyboard. use rp2040_hal::{ gpio::AnyPin, pio::{PIO, PIOExt, StateMachineIndex, UninitStateMachine}, }; use smart_leds::{RGB8, SmartLedsWrite}; use ws2812_pio::Ws2812Direct; const COLOR_OFF: RGB8 = RGB8 { r: 0, g: 0, b: 0 }; const COLOR_GREEN: RGB8 = RGB8 { r: 10, g: 7, b: 0 }; const COLOR_BLUE: RGB8 = RGB8 { r: 10, g: 4, b: 10 }; const COLOR_ORANGE: RGB8 = RGB8 { r: 5, g: 10, b: 0 }; const COLOR_RED: RGB8 = RGB8 { r: 20, g: 0, b: 0 }; const COLOR_PURPLE: RGB8 = RGB8 { r: 0, g: 10, b: 10 }; const BREATH_PERIOD_MS: u32 = 3200; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum StatusMode { Off, Active, Idle, Suspended, Error, Bootloader, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct StatusSummary { pub caps_lock_active: bool, pub sticky_armed: bool, pub sticky_latched: bool, pub usb_initialized: bool, pub usb_active: bool, pub usb_suspended: bool, pub idle_mode: bool, } impl StatusSummary { pub const fn new( caps_lock_active: bool, sticky_armed: bool, sticky_latched: bool, usb_initialized: bool, usb_active: bool, usb_suspended: bool, idle_mode: bool, ) -> Self { Self { caps_lock_active, sticky_armed, sticky_latched, usb_initialized, usb_active, usb_suspended, idle_mode, } } } pub struct StatusLed where I: AnyPin, P: PIOExt, SM: StateMachineIndex, { ws2812_direct: Ws2812Direct, current_mode: StatusMode, mode_started_at: Option, } impl StatusLed where I: AnyPin, P: PIOExt, SM: StateMachineIndex, { pub fn new( pin: I, pio: &mut PIO

, sm: UninitStateMachine<(P, SM)>, clock_freq: fugit::HertzU32, ) -> Self { let mut led = Self { ws2812_direct: Ws2812Direct::new(pin, pio, sm, clock_freq), current_mode: StatusMode::Off, mode_started_at: None, }; led.write_color(COLOR_OFF); led } pub fn update(&mut self, mode: StatusMode) { self.current_mode = mode; self.mode_started_at = None; let color = mode_color(mode, 0); self.write_color(color); } pub fn apply_summary(&mut self, summary: StatusSummary, current_time_ms: u32) { let mode = summary_to_mode(summary); let elapsed = if self.current_mode != mode { self.current_mode = mode; self.mode_started_at = Some(current_time_ms); 0 } else { let start = self.mode_started_at.unwrap_or(current_time_ms); current_time_ms.saturating_sub(start) }; let base_color = mode_color(mode, elapsed); let color = highlight_color(summary).unwrap_or(base_color); self.write_color(color); } fn write_color(&mut self, color: RGB8) { let _ = self.ws2812_direct.write([color].iter().copied()); } } fn summary_to_mode(summary: StatusSummary) -> StatusMode { if summary.usb_suspended { StatusMode::Suspended } else if !summary.usb_initialized { StatusMode::Off } else if summary.idle_mode { StatusMode::Idle } else if summary.usb_active { StatusMode::Active } else { StatusMode::Off } } fn highlight_color(summary: StatusSummary) -> Option { if summary.sticky_latched { Some(COLOR_PURPLE) } else if summary.sticky_armed { Some(COLOR_BLUE) } else if summary.caps_lock_active { Some(COLOR_ORANGE) } else { None } } fn mode_color(mode: StatusMode, elapsed_ms: u32) -> RGB8 { match mode { StatusMode::Off => COLOR_OFF, StatusMode::Active => COLOR_GREEN, StatusMode::Idle => breathe(COLOR_GREEN, elapsed_ms, BREATH_PERIOD_MS), StatusMode::Suspended => blink(COLOR_BLUE, elapsed_ms, 2000), StatusMode::Error => COLOR_RED, StatusMode::Bootloader => COLOR_PURPLE, } } fn blink(color: RGB8, elapsed_ms: u32, period_ms: u32) -> RGB8 { if period_ms == 0 { return color; } let phase = (elapsed_ms / (period_ms / 2).max(1)) % 2; if phase == 0 { color } else { COLOR_OFF } } fn breathe(color: RGB8, elapsed_ms: u32, period_ms: u32) -> RGB8 { if period_ms == 0 { return color; } let period = period_ms.max(1); let time_in_period = (elapsed_ms % period) as f32; let period_f = period as f32; let phase = time_in_period / period_f; let brightness = if phase < 0.5 { 1.0 - (phase * 2.0) } else { (phase - 0.5) * 2.0 }; let clamped = brightness.max(0.0).min(1.0); let ramp = (clamped * 255.0 + 0.5) as u8; scale_color(color, ramp) } fn scale_color(color: RGB8, factor: u8) -> RGB8 { RGB8 { r: (u16::from(color.r) * u16::from(factor) / 255) as u8, g: (u16::from(color.g) * u16::from(factor) / 255) as u8, b: (u16::from(color.b) * u16::from(factor) / 255) as u8, } }