Change back to 10ms polling
This commit is contained in:
parent
7578b39af9
commit
99381e262b
@ -35,7 +35,8 @@ pub mod usb {
|
||||
/// Timing cadence helpers used throughout the firmware.
|
||||
pub mod timers {
|
||||
pub const USB_REPORT_INTERVAL_MS: u32 = 10;
|
||||
pub const USB_TICK_INTERVAL_US: u32 = 250;
|
||||
pub const USB_TICK_INTERVAL_MS: u32 = 1;
|
||||
pub const SCAN_TICK_INTERVAL_US: u32 = 250;
|
||||
pub const STATUS_LED_INTERVAL_MS: u32 = 10;
|
||||
pub const IDLE_TIMEOUT_MS: u32 = 5_000;
|
||||
}
|
||||
|
||||
@ -60,8 +60,11 @@ fn main() -> ! {
|
||||
}
|
||||
|
||||
// Timers driving periodic USB polls and status LED updates.
|
||||
let mut scan_tick = timer.count_down();
|
||||
scan_tick.start(timers::SCAN_TICK_INTERVAL_US.micros());
|
||||
|
||||
let mut usb_tick = timer.count_down();
|
||||
usb_tick.start(timers::USB_TICK_INTERVAL_US.micros());
|
||||
usb_tick.start(timers::USB_TICK_INTERVAL_MS.millis());
|
||||
|
||||
let mut status_tick = timer.count_down();
|
||||
status_tick.start(timers::STATUS_LED_INTERVAL_MS.millis());
|
||||
@ -82,10 +85,12 @@ fn main() -> ! {
|
||||
status_led.apply_summary(summary, status_time_ms);
|
||||
}
|
||||
|
||||
if scan_tick.wait().is_ok() {
|
||||
let should_scan = {
|
||||
const SUSPENDED_SCAN_PERIOD: u8 = 20;
|
||||
if keyboard_state.usb().suspended {
|
||||
suspended_scan_counter = (suspended_scan_counter + 1) % SUSPENDED_SCAN_PERIOD;
|
||||
suspended_scan_counter =
|
||||
(suspended_scan_counter + 1) % SUSPENDED_SCAN_PERIOD;
|
||||
suspended_scan_counter == 0
|
||||
} else {
|
||||
suspended_scan_counter = 0;
|
||||
@ -93,7 +98,7 @@ fn main() -> ! {
|
||||
}
|
||||
};
|
||||
|
||||
if usb_tick.wait().is_ok() && should_scan {
|
||||
if should_scan {
|
||||
// Scan the key matrix, handle bootloader chord, and produce a report.
|
||||
button_matrix.scan_matrix(&mut delay);
|
||||
let pressed_keys = button_matrix.buttons_pressed();
|
||||
@ -133,7 +138,10 @@ fn main() -> ! {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if usb_tick.wait().is_ok() {
|
||||
match keyboard.tick() {
|
||||
Err(UsbHidError::WouldBlock) | Ok(_) => {}
|
||||
Err(_) => {
|
||||
|
||||
@ -14,6 +14,7 @@ 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;
|
||||
const BREATH_PAUSE_MS: u32 = 3000;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum StatusMode {
|
||||
@ -105,8 +106,9 @@ where
|
||||
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
|
||||
let start = mode_start_time(mode, current_time_ms);
|
||||
self.mode_started_at = Some(start);
|
||||
current_time_ms.saturating_sub(start)
|
||||
} else {
|
||||
let start = self.mode_started_at.unwrap_or(current_time_ms);
|
||||
current_time_ms.saturating_sub(start)
|
||||
@ -162,6 +164,13 @@ fn mode_color(mode: StatusMode, elapsed_ms: u32) -> RGB8 {
|
||||
}
|
||||
}
|
||||
|
||||
fn mode_start_time(mode: StatusMode, now: u32) -> u32 {
|
||||
match mode {
|
||||
StatusMode::Idle => now.saturating_sub(BREATH_PERIOD_MS / 2),
|
||||
_ => now,
|
||||
}
|
||||
}
|
||||
|
||||
fn blink(color: RGB8, elapsed_ms: u32, period_ms: u32) -> RGB8 {
|
||||
// Toggle between the provided colour and off at the requested period.
|
||||
if period_ms == 0 {
|
||||
@ -178,22 +187,176 @@ fn breathe(color: RGB8, elapsed_ms: u32, period_ms: u32) -> RGB8 {
|
||||
}
|
||||
|
||||
let period = period_ms.max(1);
|
||||
let phase = (elapsed_ms % period) as f32 / period as f32;
|
||||
let brightness = if phase < 0.5 {
|
||||
1.0 - (phase * 2.0)
|
||||
} else {
|
||||
(phase - 0.5) * 2.0
|
||||
};
|
||||
let brightness_factor = ((brightness.clamp(0.0, 1.0) * 255.0) + 0.5) as u8;
|
||||
let cycle = period.saturating_add(BREATH_PAUSE_MS);
|
||||
let phase = elapsed_ms % cycle;
|
||||
if phase >= period {
|
||||
return COLOR_OFF;
|
||||
}
|
||||
|
||||
scale_color(color, brightness_factor)
|
||||
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(color, ramp)
|
||||
}
|
||||
|
||||
fn scale_color(color: RGB8, factor: u8) -> RGB8 {
|
||||
// Linearly scale each colour component by the provided brightness factor.
|
||||
// Scale components while maintaining colour balance at low brightness.
|
||||
if factor == 0 {
|
||||
return COLOR_OFF;
|
||||
}
|
||||
|
||||
let components = [color.r, color.g, color.b];
|
||||
let mut scaled = [0u8; 3];
|
||||
let mut remainders = [0u16; 3];
|
||||
let mut total_floor: u16 = 0;
|
||||
let mut total_base: u16 = 0;
|
||||
let factor_u16 = factor as u16;
|
||||
|
||||
for (index, &component) in components.iter().enumerate() {
|
||||
total_base += component as u16;
|
||||
if component == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let value = component as u32 * factor 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 * factor_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: (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,
|
||||
r: scaled[0],
|
||||
g: scaled[1],
|
||||
b: scaled[2],
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "std"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn breathing_pause_turns_led_off() {
|
||||
// The breathing pause should hold the LED dark between cycles.
|
||||
let pause = breathe(COLOR_GREEN, BREATH_PERIOD_MS + BREATH_PAUSE_MS / 2, BREATH_PERIOD_MS);
|
||||
assert_eq!(pause, COLOR_OFF);
|
||||
|
||||
let restart = breathe(
|
||||
COLOR_GREEN,
|
||||
BREATH_PERIOD_MS + BREATH_PAUSE_MS + BREATH_PERIOD_MS / 4,
|
||||
BREATH_PERIOD_MS,
|
||||
);
|
||||
assert!(restart.g > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn breathing_waveform_is_symmetric() {
|
||||
// Breathing should ramp up from dark, peak at the midpoint, and return to dark.
|
||||
let start = breathe(COLOR_GREEN, 0, BREATH_PERIOD_MS);
|
||||
let quarter = breathe(COLOR_GREEN, BREATH_PERIOD_MS / 4, BREATH_PERIOD_MS);
|
||||
let half = breathe(COLOR_GREEN, BREATH_PERIOD_MS / 2, BREATH_PERIOD_MS);
|
||||
let end = breathe(COLOR_GREEN, BREATH_PERIOD_MS, BREATH_PERIOD_MS);
|
||||
|
||||
assert_eq!(start.g, 0);
|
||||
assert!(quarter.g > start.g);
|
||||
assert_eq!(half, COLOR_GREEN);
|
||||
assert_eq!(end.g, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scale_preserves_color_mix() {
|
||||
// Scaling at low brightness must keep the same colour ratios.
|
||||
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_factor_switches_off() {
|
||||
// A zero factor should always blank the LED regardless of colour.
|
||||
let off = scale_color(COLOR_BLUE, 0);
|
||||
assert_eq!(off, COLOR_OFF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_mode_backdates_start_time() {
|
||||
// Idle mode should start mid-breath so the LED resumes smoothly.
|
||||
let now = 10_000;
|
||||
let expected = now.saturating_sub(BREATH_PERIOD_MS / 2);
|
||||
assert_eq!(mode_start_time(StatusMode::Idle, now), expected);
|
||||
assert_eq!(mode_start_time(StatusMode::Active, now), now);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user