190 lines
5.1 KiB
Rust
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,
|
|
}
|
|
}
|