Updated breathing effect
This commit is contained in:
parent
ed8f0c4aea
commit
3216d007d0
@ -1,4 +1,9 @@
|
||||
//! Firmware entry orchestrating the CMDR Joystick runtime loop.
|
||||
//! Project: CMtec CMDR Joystick
|
||||
//! Date: 2025-03-09
|
||||
//! Author: Christoffer Martinsson
|
||||
//! Email: cm@cmtec.se
|
||||
//! License: Please refer to LICENSE in root directory
|
||||
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
@ -14,6 +19,7 @@ use panic_halt as _;
|
||||
use usb_device::prelude::*;
|
||||
use usbd_human_interface_device::prelude::{UsbHidClassBuilder, UsbHidError};
|
||||
|
||||
// Embed the boot2 image for the W25Q080 flash; required for RP2040 to boot from external flash.
|
||||
#[link_section = ".boot2"]
|
||||
#[no_mangle]
|
||||
#[used]
|
||||
|
||||
@ -64,6 +64,7 @@ struct ModeDescriptor {
|
||||
|
||||
const HEARTBEAT_POWER_MS: u32 = 800;
|
||||
const HEARTBEAT_IDLE_MS: u32 = 3200;
|
||||
const HEARTBEAT_PAUSE_MS: u32 = 3000;
|
||||
|
||||
impl LedEffect {
|
||||
fn update_interval_ms(self) -> u32 {
|
||||
@ -92,7 +93,12 @@ impl LedEffect {
|
||||
}
|
||||
LedEffect::Heartbeat { period_ms } => {
|
||||
let period = period_ms.max(1);
|
||||
let phase = elapsed_ms % period;
|
||||
let cycle = period.saturating_add(HEARTBEAT_PAUSE_MS);
|
||||
let phase = elapsed_ms % cycle;
|
||||
if phase >= period {
|
||||
return COLOR_OFF;
|
||||
}
|
||||
|
||||
let half = (period / 2).max(1);
|
||||
let ramp = if phase < half {
|
||||
((phase * 255) / half) as u8
|
||||
@ -199,12 +205,94 @@ const fn descriptor_for(mode: StatusMode, base_mode: StatusMode) -> ModeDescript
|
||||
}
|
||||
|
||||
fn scale_color(base: RGB8, brightness: u8) -> RGB8 {
|
||||
// Scale each RGB component proportionally to the requested brightness factor.
|
||||
let scale = brightness as u16;
|
||||
if brightness == 0 {
|
||||
return COLOR_OFF;
|
||||
}
|
||||
|
||||
let components = [base.r, base.g, base.b];
|
||||
let mut scaled = [0u8; 3];
|
||||
let mut remainders = [0u16; 3];
|
||||
let mut total_floor: u16 = 0;
|
||||
let mut total_base: u16 = 0;
|
||||
let brightness_u16 = brightness as u16;
|
||||
|
||||
for (index, &component) in components.iter().enumerate() {
|
||||
total_base += component as u16;
|
||||
if component == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let value = component as u32 * brightness as u32;
|
||||
let div = (value / 255) as u16;
|
||||
let rem = (value % 255) as u16;
|
||||
|
||||
scaled[index] = div as u8;
|
||||
remainders[index] = rem;
|
||||
total_floor += div;
|
||||
}
|
||||
|
||||
if total_base == 0 {
|
||||
return COLOR_OFF;
|
||||
}
|
||||
|
||||
let mut target_total = ((total_base as u32 * brightness_u16 as u32) + 127) / 255;
|
||||
if target_total > total_base as u32 {
|
||||
target_total = total_base as u32;
|
||||
}
|
||||
|
||||
let mut extra = target_total.saturating_sub(total_floor as u32) as u16;
|
||||
|
||||
while extra > 0 {
|
||||
let mut best_index: Option<usize> = None;
|
||||
let mut best_remainder = 0u16;
|
||||
|
||||
for idx in 0..components.len() {
|
||||
if components[idx] == 0 {
|
||||
continue;
|
||||
}
|
||||
if remainders[idx] == 0 {
|
||||
continue;
|
||||
}
|
||||
if scaled[idx] as u16 >= components[idx] as u16 {
|
||||
continue;
|
||||
}
|
||||
|
||||
if remainders[idx] > best_remainder {
|
||||
best_remainder = remainders[idx];
|
||||
best_index = Some(idx);
|
||||
}
|
||||
}
|
||||
|
||||
let Some(idx) = best_index else {
|
||||
break;
|
||||
};
|
||||
|
||||
scaled[idx] += 1;
|
||||
remainders[idx] = 0;
|
||||
extra -= 1;
|
||||
}
|
||||
|
||||
if extra > 0 {
|
||||
for idx in 0..components.len() {
|
||||
if extra == 0 {
|
||||
break;
|
||||
}
|
||||
if components[idx] == 0 {
|
||||
continue;
|
||||
}
|
||||
if scaled[idx] as u16 >= components[idx] as u16 {
|
||||
continue;
|
||||
}
|
||||
|
||||
scaled[idx] += 1;
|
||||
extra -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
r: scaled[0],
|
||||
g: scaled[1],
|
||||
b: scaled[2],
|
||||
}
|
||||
}
|
||||
|
||||
@ -429,6 +517,27 @@ mod tests {
|
||||
assert_eq!(end.g, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heartbeat_pause_keeps_led_off() {
|
||||
// Added pause should hold the LED dark between breaths while allowing a clean restart.
|
||||
let base = StatusMode::Normal;
|
||||
let descriptor = descriptor_for(StatusMode::Idle, base);
|
||||
let LedEffect::Heartbeat { period_ms } = descriptor.effect else {
|
||||
panic!("Idle should use heartbeat effect");
|
||||
};
|
||||
|
||||
let pause_sample = descriptor
|
||||
.effect
|
||||
.color_for(descriptor.color, period_ms + HEARTBEAT_PAUSE_MS / 2);
|
||||
assert_eq!(pause_sample, COLOR_OFF);
|
||||
|
||||
let cycle = period_ms + HEARTBEAT_PAUSE_MS;
|
||||
let restart = descriptor
|
||||
.effect
|
||||
.color_for(descriptor.color, cycle + period_ms / 4);
|
||||
assert!(restart.g > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blink_effect_toggles() {
|
||||
// Blink descriptor should alternate between the color and off state.
|
||||
@ -443,6 +552,27 @@ mod tests {
|
||||
assert_eq!(off, COLOR_OFF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_brightness_preserves_color_mix() {
|
||||
// Low brightness scaling must not drop any non-zero channel from the blend.
|
||||
let base = COLOR_ORANGE;
|
||||
let dimmed = scale_color(base, 50);
|
||||
assert!(dimmed.r > 0);
|
||||
assert!(dimmed.g > 0);
|
||||
assert_eq!(dimmed.b, 0);
|
||||
assert!(dimmed.r <= base.r);
|
||||
assert!(dimmed.g <= base.g);
|
||||
assert_eq!(dimmed.r as u16 * base.g as u16, dimmed.g as u16 * base.r as u16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_brightness_turns_off_led() {
|
||||
// Zero brightness should fully blank the LED regardless of the base color.
|
||||
let base = COLOR_BLUE;
|
||||
let off = scale_color(base, 0);
|
||||
assert_eq!(off, COLOR_OFF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn determine_base_mode_before_usb() {
|
||||
// Before USB comes up the controller should stay in Power mode.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user