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.
|
/// Timing cadence helpers used throughout the firmware.
|
||||||
pub mod timers {
|
pub mod timers {
|
||||||
pub const USB_REPORT_INTERVAL_MS: u32 = 10;
|
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 STATUS_LED_INTERVAL_MS: u32 = 10;
|
||||||
pub const IDLE_TIMEOUT_MS: u32 = 5_000;
|
pub const IDLE_TIMEOUT_MS: u32 = 5_000;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,8 +60,11 @@ fn main() -> ! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Timers driving periodic USB polls and status LED updates.
|
// 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();
|
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();
|
let mut status_tick = timer.count_down();
|
||||||
status_tick.start(timers::STATUS_LED_INTERVAL_MS.millis());
|
status_tick.start(timers::STATUS_LED_INTERVAL_MS.millis());
|
||||||
@ -82,58 +85,63 @@ fn main() -> ! {
|
|||||||
status_led.apply_summary(summary, status_time_ms);
|
status_led.apply_summary(summary, status_time_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
let should_scan = {
|
if scan_tick.wait().is_ok() {
|
||||||
const SUSPENDED_SCAN_PERIOD: u8 = 20;
|
let should_scan = {
|
||||||
if keyboard_state.usb().suspended {
|
const SUSPENDED_SCAN_PERIOD: u8 = 20;
|
||||||
suspended_scan_counter = (suspended_scan_counter + 1) % SUSPENDED_SCAN_PERIOD;
|
if keyboard_state.usb().suspended {
|
||||||
suspended_scan_counter == 0
|
suspended_scan_counter =
|
||||||
} else {
|
(suspended_scan_counter + 1) % SUSPENDED_SCAN_PERIOD;
|
||||||
suspended_scan_counter = 0;
|
suspended_scan_counter == 0
|
||||||
true
|
} else {
|
||||||
}
|
suspended_scan_counter = 0;
|
||||||
};
|
true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if usb_tick.wait().is_ok() && should_scan {
|
if should_scan {
|
||||||
// Scan the key matrix, handle bootloader chord, and produce a report.
|
// Scan the key matrix, handle bootloader chord, and produce a report.
|
||||||
button_matrix.scan_matrix(&mut delay);
|
button_matrix.scan_matrix(&mut delay);
|
||||||
let pressed_keys = button_matrix.buttons_pressed();
|
let pressed_keys = button_matrix.buttons_pressed();
|
||||||
|
|
||||||
if bootloader::chord_requested(&pressed_keys) {
|
if bootloader::chord_requested(&pressed_keys) {
|
||||||
if !keyboard_state.usb().suspended {
|
if !keyboard_state.usb().suspended {
|
||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
let clear_report: KeyReport =
|
let clear_report: KeyReport =
|
||||||
[Keyboard::NoEventIndicated; hardware::NUMBER_OF_KEYS];
|
[Keyboard::NoEventIndicated; hardware::NUMBER_OF_KEYS];
|
||||||
match keyboard.device().write_report(clear_report) {
|
match keyboard.device().write_report(clear_report) {
|
||||||
Ok(_) => break,
|
Ok(_) => break,
|
||||||
Err(UsbHidError::WouldBlock) | Err(UsbHidError::Duplicate) => {
|
Err(UsbHidError::WouldBlock) | Err(UsbHidError::Duplicate) => {
|
||||||
let _ = keyboard.tick();
|
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() {
|
match keyboard.tick() {
|
||||||
Err(UsbHidError::WouldBlock) | Ok(_) => {}
|
Err(UsbHidError::WouldBlock) | Ok(_) => {}
|
||||||
Err(_) => {
|
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_RED: RGB8 = RGB8 { r: 20, g: 0, b: 0 };
|
||||||
const COLOR_PURPLE: RGB8 = RGB8 { r: 0, g: 10, b: 10 };
|
const COLOR_PURPLE: RGB8 = RGB8 { r: 0, g: 10, b: 10 };
|
||||||
const BREATH_PERIOD_MS: u32 = 3200;
|
const BREATH_PERIOD_MS: u32 = 3200;
|
||||||
|
const BREATH_PAUSE_MS: u32 = 3000;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum StatusMode {
|
pub enum StatusMode {
|
||||||
@ -105,8 +106,9 @@ where
|
|||||||
let mode = summary_to_mode(summary);
|
let mode = summary_to_mode(summary);
|
||||||
let elapsed = if self.current_mode != mode {
|
let elapsed = if self.current_mode != mode {
|
||||||
self.current_mode = mode;
|
self.current_mode = mode;
|
||||||
self.mode_started_at = Some(current_time_ms);
|
let start = mode_start_time(mode, current_time_ms);
|
||||||
0
|
self.mode_started_at = Some(start);
|
||||||
|
current_time_ms.saturating_sub(start)
|
||||||
} else {
|
} else {
|
||||||
let start = self.mode_started_at.unwrap_or(current_time_ms);
|
let start = self.mode_started_at.unwrap_or(current_time_ms);
|
||||||
current_time_ms.saturating_sub(start)
|
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 {
|
fn blink(color: RGB8, elapsed_ms: u32, period_ms: u32) -> RGB8 {
|
||||||
// Toggle between the provided colour and off at the requested period.
|
// Toggle between the provided colour and off at the requested period.
|
||||||
if period_ms == 0 {
|
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 period = period_ms.max(1);
|
||||||
let phase = (elapsed_ms % period) as f32 / period as f32;
|
let cycle = period.saturating_add(BREATH_PAUSE_MS);
|
||||||
let brightness = if phase < 0.5 {
|
let phase = elapsed_ms % cycle;
|
||||||
1.0 - (phase * 2.0)
|
if phase >= period {
|
||||||
} else {
|
return COLOR_OFF;
|
||||||
(phase - 0.5) * 2.0
|
}
|
||||||
};
|
|
||||||
let brightness_factor = ((brightness.clamp(0.0, 1.0) * 255.0) + 0.5) as u8;
|
|
||||||
|
|
||||||
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 {
|
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 {
|
RGB8 {
|
||||||
r: (u16::from(color.r) * u16::from(factor) / 255) as u8,
|
r: scaled[0],
|
||||||
g: (u16::from(color.g) * u16::from(factor) / 255) as u8,
|
g: scaled[1],
|
||||||
b: (u16::from(color.b) * u16::from(factor) / 255) as u8,
|
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