diff --git a/rp2040/Cargo.toml b/rp2040/Cargo.toml index 85a17b6..9bc57f2 100644 --- a/rp2040/Cargo.toml +++ b/rp2040/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "cmdr-keyboard" version = "0.2.0" -edition = "2024" +edition = "2021" [dependencies] cortex-m = "0.7.2" diff --git a/rp2040/src/keyboard.rs b/rp2040/src/keyboard.rs index a91b797..7d06258 100644 --- a/rp2040/src/keyboard.rs +++ b/rp2040/src/keyboard.rs @@ -1,9 +1,91 @@ //! Keyboard state management and HID report generation. +use crate::hardware; use crate::{NUMBER_OF_KEYS, KeyMatrix, KeyReport}; use crate::layout; use crate::status::StatusSummary; use usbd_human_interface_device::page::Keyboard; +use usb_device::device::UsbDeviceState; + +/// Tracks USB lifecycle state (suspend/idle/activity) for the keyboard HID device. +pub struct UsbState { + pub initialized: bool, + pub active: bool, + pub suspended: bool, + pub wake_on_input: bool, + pub idle_mode: bool, + activity: bool, + activity_elapsed_ms: u32, +} + +impl UsbState { + pub const fn new() -> Self { + Self { + initialized: false, + active: false, + suspended: false, + wake_on_input: false, + idle_mode: false, + activity: false, + activity_elapsed_ms: 0, + } + } + + pub fn on_poll(&mut self) { + if !self.initialized { + self.initialized = true; + } + if !self.active { + self.mark_activity(); + } + self.active = true; + } + + pub fn mark_activity(&mut self) { + self.activity = true; + self.activity_elapsed_ms = 0; + self.idle_mode = false; + } + + pub fn handle_input_activity(&mut self) { + self.mark_activity(); + if self.suspended && self.wake_on_input { + self.wake_on_input = false; + } + } + + pub fn on_suspend_change(&mut self, state: UsbDeviceState) { + let was_suspended = self.suspended; + self.suspended = state == UsbDeviceState::Suspend; + + match (was_suspended, self.suspended) { + (true, false) => { + self.mark_activity(); + self.wake_on_input = false; + } + (false, true) => { + self.idle_mode = true; + self.activity = false; + self.wake_on_input = true; + } + _ => {} + } + } + + pub fn advance_idle_timer(&mut self, interval_ms: u32) { + if !self.activity { + return; + } + self.activity_elapsed_ms = self.activity_elapsed_ms.saturating_add(interval_ms); + if self.activity_elapsed_ms >= hardware::timers::IDLE_TIMEOUT_MS { + self.activity = false; + self.activity_elapsed_ms = 0; + self.idle_mode = true; + } + } + + pub fn acknowledge_report(&mut self) {} +} /// Captures per-key state transitions and the function layer active when it was pressed. #[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] @@ -38,6 +120,7 @@ pub struct KeyboardState { sticky_key: Keyboard, caps_lock_active: bool, started: bool, + usb: UsbState, } impl KeyboardState { @@ -49,6 +132,7 @@ impl KeyboardState { sticky_key: Keyboard::NoEventIndicated, caps_lock_active: false, started: false, + usb: UsbState::new(), } } @@ -79,11 +163,18 @@ impl KeyboardState { pub fn mark_started(&mut self) { // Note that the HID interface has successfully exchanged reports. self.started = true; + self.usb.mark_activity(); + self.usb.active = true; + self.usb.initialized = true; } pub fn mark_stopped(&mut self) { // Reset the flag when USB communication fails. self.started = false; + self.usb.active = false; + self.usb.initialized = false; + self.usb.activity = false; + self.usb.idle_mode = false; } pub fn started(&self) -> bool { @@ -107,22 +198,17 @@ impl KeyboardState { }; } - pub fn status_summary( - &self, - usb_initialized: bool, - usb_active: bool, - usb_suspended: bool, - idle_mode: bool, - ) -> StatusSummary { + pub fn status_summary(&self) -> StatusSummary { // Produce a condensed summary consumed by the status LED driver. + let usb_active = self.usb.active && !self.usb.idle_mode; StatusSummary::new( self.caps_lock_active, matches!(self.sticky_state, StickyState::Armed), matches!(self.sticky_state, StickyState::Latched), - usb_initialized, + self.usb.initialized, usb_active, - usb_suspended, - idle_mode, + self.usb.suspended, + self.usb.idle_mode, ) } @@ -187,6 +273,14 @@ impl KeyboardState { active_fn_keys.min(2) } + + pub fn usb_state(&mut self) -> &mut UsbState { + &mut self.usb + } + + pub fn usb(&self) -> &UsbState { + &self.usb + } } impl Default for KeyboardState { @@ -246,8 +340,7 @@ mod tests { let mut state = KeyboardState::new(); state.update_caps_lock(true); state.mark_started(); - - let summary = state.status_summary(true, true, false, false); + let summary = state.status_summary(); assert!(summary.caps_lock_active); assert!(summary.usb_active); assert!(summary.usb_initialized); diff --git a/rp2040/src/main.rs b/rp2040/src/main.rs index 3dcfbfe..a1e5b01 100644 --- a/rp2040/src/main.rs +++ b/rp2040/src/main.rs @@ -4,6 +4,7 @@ //! Email: cm@cmtec.se //! License: Please refer to LICENSE in root directory +//! Firmware entry orchestrating the CMDR Keyboard runtime loop. #![no_std] #![no_main] @@ -13,45 +14,18 @@ use embedded_hal_0_2::timer::CountDown; use fugit::ExtU32; use panic_halt as _; use usb_device::UsbError; -use usb_device::device::UsbDeviceState; use usb_device::prelude::*; use usbd_human_interface_device::device::keyboard::NKROBootKeyboardConfig; use usbd_human_interface_device::page::Keyboard; use usbd_human_interface_device::prelude::UsbHidError; use usbd_human_interface_device::prelude::*; -// The boot2 image must live in the dedicated ROM section, which requires these attributes. -#[unsafe(link_section = ".boot2")] -#[unsafe(no_mangle)] +// Embed the boot2 image for the W25Q080 flash; required for RP2040 to boot from external flash. +#[link_section = ".boot2"] +#[no_mangle] #[used] pub static BOOT2_FIRMWARE: [u8; 256] = rp2040_boot2::BOOT_LOADER_W25Q080; - -fn handle_usb_state_changes( - usb_dev: &UsbDevice, - usb_suspended: &mut bool, - wake_on_input: &mut bool, - last_activity_ms: &mut u32, - status_time_ms: u32, -) { - // Track suspend/resume transitions and refresh idle timers when USB wakes. - let current_suspended = usb_dev.state() == UsbDeviceState::Suspend; - let was_suspended = *usb_suspended; - - match (was_suspended, current_suspended) { - (true, false) => { - *last_activity_ms = status_time_ms; - *wake_on_input = false; - } - (false, true) => { - *wake_on_input = true; - } - _ => {} - } - - *usb_suspended = current_suspended; -} - #[rp2040_hal::entry] fn main() -> ! { // Bring up the board peripherals and split them into reusable parts. @@ -94,38 +68,30 @@ fn main() -> ! { status_tick.start(timers::STATUS_LED_INTERVAL_MS.millis()); let mut status_time_ms: u32 = 0; - let mut usb_initialized = false; - let mut usb_suspended = false; - let mut wake_on_input = false; - let mut last_activity_ms: u32 = 0; - let mut suspended_scan_divider: u8 = 0; + let mut suspended_scan_counter: u8 = 0; loop { if status_tick.wait().is_ok() { // Update the status LED summary on its cadence. status_time_ms = status_time_ms.saturating_add(timers::STATUS_LED_INTERVAL_MS); - let idle_elapsed = status_time_ms.saturating_sub(last_activity_ms); - let idle_mode = usb_initialized && idle_elapsed >= timers::IDLE_TIMEOUT_MS; - let usb_active = usb_initialized && !idle_mode; - status_led.apply_summary( - keyboard_state.status_summary( - usb_initialized, - usb_active, - usb_suspended, - idle_mode, - ), - status_time_ms, - ); + { + keyboard_state + .usb_state() + .advance_idle_timer(timers::STATUS_LED_INTERVAL_MS); + } + let summary = keyboard_state.status_summary(); + status_led.apply_summary(summary, status_time_ms); } - // When suspended, thin out scans to reduce power but keep responsiveness. - const SUSPENDED_SCAN_PERIOD: u8 = 20; - let should_scan = if !usb_suspended { - suspended_scan_divider = 0; - true - } else { - suspended_scan_divider = (suspended_scan_divider + 1) % SUSPENDED_SCAN_PERIOD; - suspended_scan_divider == 0 + 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 { @@ -134,7 +100,7 @@ fn main() -> ! { let pressed_keys = button_matrix.buttons_pressed(); if bootloader::chord_requested(&pressed_keys) { - if !usb_suspended { + 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) { @@ -152,24 +118,18 @@ fn main() -> ! { } if pressed_keys.iter().any(|pressed| *pressed) { - last_activity_ms = status_time_ms; - if wake_on_input && usb_suspended { - wake_on_input = false; - } + keyboard_state.usb_state().handle_input_activity(); } let keyboard_report = keyboard_state.process_scan(pressed_keys); - if !usb_suspended { + 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(_) => { - usb_initialized = true; - } + Ok(_) => {} Err(_) => { keyboard_state.mark_stopped(); - usb_initialized = false; } } } @@ -178,34 +138,26 @@ fn main() -> ! { Err(UsbHidError::WouldBlock) | Ok(_) => {} Err(_) => { keyboard_state.mark_stopped(); - usb_initialized = false; } } } if usb_dev.poll(&mut [&mut keyboard]) { + keyboard_state.usb_state().on_poll(); // Consume OUT reports (e.g., LED indicators) and track host activity. match keyboard.device().read_report() { Err(UsbError::WouldBlock) => {} Err(_) => { keyboard_state.mark_stopped(); - usb_initialized = false; } Ok(leds) => { keyboard_state.update_caps_lock(leds.caps_lock); keyboard_state.mark_started(); - usb_initialized = true; - last_activity_ms = status_time_ms; } } } - - handle_usb_state_changes( - &usb_dev, - &mut usb_suspended, - &mut wake_on_input, - &mut last_activity_ms, - status_time_ms, - ); + keyboard_state + .usb_state() + .on_suspend_change(usb_dev.state()); } }