Change back to 10ms polling

This commit is contained in:
Christoffer Martinsson 2025-09-28 18:52:59 +02:00
parent 7578b39af9
commit 99381e262b
3 changed files with 232 additions and 60 deletions

View File

@ -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;
}

View File

@ -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,58 +85,63 @@ fn main() -> ! {
status_led.apply_summary(summary, status_time_ms);
}
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 == 0
} else {
suspended_scan_counter = 0;
true
}
};
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 == 0
} else {
suspended_scan_counter = 0;
true
}
};
if usb_tick.wait().is_ok() && 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();
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();
if bootloader::chord_requested(&pressed_keys) {
if !keyboard_state.usb().suspended {
for _ in 0..3 {
let clear_report: KeyReport =
[Keyboard::NoEventIndicated; hardware::NUMBER_OF_KEYS];
match keyboard.device().write_report(clear_report) {
Ok(_) => break,
Err(UsbHidError::WouldBlock) | Err(UsbHidError::Duplicate) => {
let _ = keyboard.tick();
if bootloader::chord_requested(&pressed_keys) {
if !keyboard_state.usb().suspended {
for _ in 0..3 {
let clear_report: KeyReport =
[Keyboard::NoEventIndicated; hardware::NUMBER_OF_KEYS];
match keyboard.device().write_report(clear_report) {
Ok(_) => break,
Err(UsbHidError::WouldBlock) | Err(UsbHidError::Duplicate) => {
let _ = keyboard.tick();
}
Err(_) => break,
}
Err(_) => break,
}
}
delay.delay_ms(5);
bootloader::enter(&mut status_led);
}
if pressed_keys.iter().any(|pressed| *pressed) {
keyboard_state.usb_state().handle_input_activity();
}
let keyboard_report = keyboard_state.process_scan(pressed_keys);
if !keyboard_state.usb().suspended {
// Try to send the generated report to the host.
match keyboard.device().write_report(keyboard_report) {
Err(UsbHidError::WouldBlock) | Err(UsbHidError::Duplicate) => {}
Ok(_) => {}
Err(_) => {
keyboard_state.mark_stopped();
}
}
}
delay.delay_ms(5);
bootloader::enter(&mut status_led);
}
if pressed_keys.iter().any(|pressed| *pressed) {
keyboard_state.usb_state().handle_input_activity();
}
let keyboard_report = keyboard_state.process_scan(pressed_keys);
if !keyboard_state.usb().suspended {
// Try to send the generated report to the host.
match keyboard.device().write_report(keyboard_report) {
Err(UsbHidError::WouldBlock) | Err(UsbHidError::Duplicate) => {}
Ok(_) => {}
Err(_) => {
keyboard_state.mark_stopped();
}
}
}
}
if usb_tick.wait().is_ok() {
match keyboard.tick() {
Err(UsbHidError::WouldBlock) | Ok(_) => {}
Err(_) => {

View File

@ -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);
}
}