Updated breathing effect

This commit is contained in:
Christoffer Martinsson 2025-09-28 18:54:05 +02:00
parent ed8f0c4aea
commit 3216d007d0
2 changed files with 143 additions and 7 deletions

View File

@ -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]

View File

@ -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.