//! WS2812 status LED driver adapted from the joystick firmware. 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 }; #[allow(dead_code)] #[derive(PartialEq, Eq, Copy, Clone, Debug)] pub enum StatusMode { Off = 0, Normal = 1, NormalFlash = 2, Activity = 3, ActivityFlash = 4, Idle = 5, Other = 6, OtherFlash = 7, Warning = 8, Error = 9, Bootloader = 10, Power = 11, Suspended = 12, } #[derive(Clone, Copy, Debug, Default)] pub struct SystemState { pub usb_active: bool, pub usb_initialized: bool, pub usb_suspended: bool, pub idle_mode: bool, pub calibration_active: bool, pub throttle_hold_enable: bool, pub vt_enable: bool, pub caps_lock_active: bool, pub sticky_armed: bool, pub sticky_latched: bool, } #[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 fn to_system_state(self) -> SystemState { SystemState { usb_active: self.usb_active, usb_initialized: self.usb_initialized, usb_suspended: self.usb_suspended, idle_mode: self.idle_mode, calibration_active: false, throttle_hold_enable: false, vt_enable: false, caps_lock_active: self.caps_lock_active, sticky_armed: self.sticky_armed, sticky_latched: self.sticky_latched, } } } #[derive(Copy, Clone)] enum LedEffect { Solid, Blink { period_ms: u32 }, Heartbeat { period_ms: u32 }, } #[derive(Copy, Clone)] struct ModeDescriptor { color: RGB8, effect: LedEffect, } const HEARTBEAT_POWER_MS: u32 = 800; const HEARTBEAT_IDLE_MS: u32 = 3200; impl LedEffect { fn update_interval_ms(self) -> u32 { match self { LedEffect::Solid => 0, LedEffect::Blink { period_ms } => period_ms / 2, LedEffect::Heartbeat { .. } => 10, } } fn color_for(self, base: RGB8, elapsed_ms: u32) -> RGB8 { match self { LedEffect::Solid => base, LedEffect::Blink { period_ms } => { if period_ms == 0 { return base; } let half = (period_ms / 2).max(1); if elapsed_ms % period_ms < half { base } else { COLOR_OFF } } LedEffect::Heartbeat { period_ms } => { let period = period_ms.max(1); let phase = elapsed_ms % period; let half = (period / 2).max(1); let ramp = if phase < half { ((phase * 255) / half) as u8 } else { (((period - phase) * 255) / half) as u8 }; scale_color(base, ramp) } } } } const fn descriptor_for(mode: StatusMode, base_mode: StatusMode) -> ModeDescriptor { match mode { StatusMode::Off => ModeDescriptor { color: COLOR_OFF, effect: LedEffect::Solid, }, StatusMode::Normal => ModeDescriptor { color: COLOR_GREEN, effect: LedEffect::Solid, }, StatusMode::NormalFlash => ModeDescriptor { color: COLOR_GREEN, effect: LedEffect::Blink { period_ms: 1000 }, }, StatusMode::Activity => ModeDescriptor { color: COLOR_BLUE, effect: LedEffect::Solid, }, StatusMode::ActivityFlash => ModeDescriptor { color: COLOR_BLUE, effect: LedEffect::Blink { period_ms: 600 }, }, StatusMode::Idle => match base_mode { StatusMode::Activity | StatusMode::ActivityFlash => ModeDescriptor { color: COLOR_BLUE, effect: LedEffect::Heartbeat { period_ms: HEARTBEAT_IDLE_MS, }, }, StatusMode::Other | StatusMode::OtherFlash => ModeDescriptor { color: COLOR_ORANGE, effect: LedEffect::Heartbeat { period_ms: HEARTBEAT_IDLE_MS, }, }, StatusMode::NormalFlash | StatusMode::Normal => ModeDescriptor { color: COLOR_GREEN, effect: LedEffect::Heartbeat { period_ms: HEARTBEAT_IDLE_MS, }, }, StatusMode::Warning | StatusMode::Error => ModeDescriptor { color: COLOR_RED, effect: LedEffect::Heartbeat { period_ms: HEARTBEAT_IDLE_MS, }, }, _ => ModeDescriptor { color: COLOR_GREEN, effect: LedEffect::Heartbeat { period_ms: HEARTBEAT_IDLE_MS, }, }, }, StatusMode::Other => ModeDescriptor { color: COLOR_ORANGE, effect: LedEffect::Solid, }, StatusMode::OtherFlash => ModeDescriptor { color: COLOR_ORANGE, effect: LedEffect::Blink { period_ms: 600 }, }, StatusMode::Warning => ModeDescriptor { color: COLOR_RED, effect: LedEffect::Blink { period_ms: 500 }, }, StatusMode::Error => ModeDescriptor { color: COLOR_RED, effect: LedEffect::Solid, }, StatusMode::Bootloader => ModeDescriptor { color: COLOR_PURPLE, effect: LedEffect::Solid, }, StatusMode::Power => ModeDescriptor { color: COLOR_GREEN, effect: LedEffect::Heartbeat { period_ms: HEARTBEAT_POWER_MS, }, }, StatusMode::Suspended => ModeDescriptor { color: COLOR_OFF, effect: LedEffect::Solid, }, } } fn scale_color(base: RGB8, brightness: u8) -> RGB8 { let scale = brightness as u16; RGB8 { r: ((base.r as u16 * scale) / 255) as u8, g: ((base.g as u16 * scale) / 255) as u8, b: ((base.b as u16 * scale) / 255) as u8, } } fn determine_base_mode(system_state: SystemState) -> StatusMode { if system_state.usb_suspended { StatusMode::Suspended } else if system_state.caps_lock_active { StatusMode::Warning } else if system_state.sticky_latched { StatusMode::ActivityFlash } else if system_state.sticky_armed { StatusMode::Activity } else if system_state.calibration_active { StatusMode::ActivityFlash } else if !system_state.usb_initialized { StatusMode::Power } else if !system_state.usb_active { StatusMode::NormalFlash } else if system_state.vt_enable { StatusMode::Activity } else if system_state.throttle_hold_enable { StatusMode::Other } else { StatusMode::Normal } } pub struct StatusLed
where
I: AnyPin ,
current_mode: StatusMode,
mode_started_at: Option StatusLed
where
I: AnyPin ,
sm: UninitStateMachine<(P, SM)>,
clock_freq: fugit::HertzU32,
) -> Self {
let ws2812_direct = Ws2812Direct::new(pin, pio, sm, clock_freq);
let mut status = Self {
ws2812_direct,
current_mode: StatusMode::Off,
mode_started_at: None,
last_update_time: None,
};
status.write_color(COLOR_OFF);
status
}
pub fn update_from_system_state(&mut self, system_state: SystemState, current_time: u32) {
let base_mode = determine_base_mode(system_state);
let desired_mode = if system_state.idle_mode
&& system_state.usb_initialized
&& base_mode != StatusMode::Off
&& base_mode != StatusMode::Power
{
(StatusMode::Idle, base_mode)
} else {
(base_mode, base_mode)
};
self.set_mode(desired_mode.0, desired_mode.1, current_time);
}
pub fn set_mode(&mut self, mode: StatusMode, base_mode: StatusMode, current_time: u32) {
let force_update = mode != self.current_mode;
self.current_mode = mode;
let descriptor = descriptor_for(mode, base_mode);
if force_update {
let start_time = match descriptor.effect {
LedEffect::Heartbeat { period_ms } => {
let half = (period_ms / 2).max(1);
current_time.saturating_sub(half)
}
_ => current_time,
};
self.mode_started_at = Some(start_time);
self.last_update_time = None;
}
self.update_display(current_time, force_update, base_mode);
}
pub fn update_display(&mut self, current_time: u32, force_update: bool, base_mode: StatusMode) {
let descriptor = descriptor_for(self.current_mode, base_mode);
let interval = descriptor.effect.update_interval_ms();
if !force_update {
if interval == 0 {
return;
}
if let Some(last) = self.last_update_time
&& current_time.saturating_sub(last) < interval
{
return;
}
}
let elapsed = self
.mode_started_at
.map(|start| current_time.saturating_sub(start))
.unwrap_or(0);
let color = descriptor.effect.color_for(descriptor.color, elapsed);
self.write_color(color);
self.last_update_time = Some(current_time);
}
fn write_color(&mut self, color: RGB8) {
let _ = self.ws2812_direct.write([color].iter().copied());
}
pub fn apply_summary(&mut self, summary: StatusSummary, current_time: u32) {
self.update_from_system_state(summary.to_system_state(), current_time);
}
}
impl StatusLed
where
I: AnyPin