190 lines
5.1 KiB
Rust

//! 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<P, SM, I>
where
I: AnyPin<Function = P::PinFunction>,
P: PIOExt,
SM: StateMachineIndex,
{
ws2812_direct: Ws2812Direct<P, SM, I>,
current_mode: StatusMode,
mode_started_at: Option<u32>,
}
impl<P, SM, I> StatusLed<P, SM, I>
where
I: AnyPin<Function = P::PinFunction>,
P: PIOExt,
SM: StateMachineIndex,
{
pub fn new(
pin: I,
pio: &mut PIO<P>,
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<RGB8> {
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,
}
}