cmdr-keyboard/rp2040/src/keyboard.rs

378 lines
12 KiB
Rust

//! 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)]
pub struct KeyboardButton {
pub pressed: bool,
pub previous_pressed: bool,
pub fn_mode: u8,
}
/// Discrete states for the sticky modifier state machine.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum StickyState {
Inactive,
Armed,
Latched,
}
impl StickyState {
pub fn is_armed(self) -> bool {
matches!(self, StickyState::Armed)
}
pub fn is_latched(self) -> bool {
matches!(self, StickyState::Latched)
}
}
/// Manages keyboard-wide state, layer selection, and HID report composition.
pub struct KeyboardState {
buttons: [KeyboardButton; NUMBER_OF_KEYS],
sticky_state: StickyState,
sticky_key: Keyboard,
caps_lock_active: bool,
started: bool,
usb: UsbState,
}
impl KeyboardState {
pub fn new() -> Self {
// Initialise button, sticky, and host communication state.
Self {
buttons: [KeyboardButton::default(); NUMBER_OF_KEYS],
sticky_state: StickyState::Inactive,
sticky_key: Keyboard::NoEventIndicated,
caps_lock_active: false,
started: false,
usb: UsbState::new(),
}
}
pub fn process_scan(
&mut self,
pressed_keys: KeyMatrix,
) -> KeyReport {
// Update each button from the latest scan and build the keyboard report.
let fn_mode = Self::fn_mode(&pressed_keys);
for (index, pressed) in pressed_keys.iter().enumerate() {
self.buttons[index].pressed = *pressed;
}
self.build_report(fn_mode)
}
pub fn update_caps_lock(&mut self, active: bool) {
// Track the host LED state to drive the status indicator.
self.caps_lock_active = active;
}
pub fn caps_lock_active(&self) -> bool {
// Report whether the Caps Lock LED is currently active.
self.caps_lock_active
}
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 {
// Expose whether USB activity has been observed.
self.started
}
pub fn sticky_state(&self) -> StickyState {
// Current state of the sticky modifier toggle.
self.sticky_state
}
fn toggle_sticky_state(&mut self) {
// Advance through inactive → armed → latched lifecycle and clear when toggled off.
self.sticky_state = match self.sticky_state {
StickyState::Inactive => StickyState::Armed,
StickyState::Armed | StickyState::Latched => {
self.sticky_key = Keyboard::NoEventIndicated;
StickyState::Inactive
}
};
}
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),
self.usb.initialized,
usb_active,
self.usb.suspended,
self.usb.idle_mode,
)
}
fn build_report(&mut self, fn_mode: u8) -> KeyReport {
// Translate layer-aware button state into the NKRO HID report payload.
let mut report = [Keyboard::NoEventIndicated; NUMBER_OF_KEYS];
let mut sticky_toggle_requested = false;
for (index, button) in self.buttons.iter_mut().enumerate() {
let changed = button.pressed != button.previous_pressed;
let just_pressed = changed && button.pressed;
if just_pressed {
button.fn_mode = fn_mode;
match (index as u8, fn_mode) {
(idx, layer) if idx == layout::STICKY_BUTTON[0] && layer == layout::STICKY_BUTTON[1] => {
sticky_toggle_requested = true;
}
(idx, layer) if idx == layout::OS_LOCK_BUTTON[0] && layer == layout::OS_LOCK_BUTTON[1] => {
report[36] = layout::OS_LOCK_BUTTON_KEYS[0];
report[37] = layout::OS_LOCK_BUTTON_KEYS[1];
}
_ => {}
}
}
let layer_key = layout::MAP[button.fn_mode as usize][index];
if layer_key == Keyboard::NoEventIndicated {
button.previous_pressed = button.pressed;
continue;
}
if self.sticky_state == StickyState::Armed && button.pressed {
self.sticky_key = layer_key;
self.sticky_state = StickyState::Latched;
}
if button.pressed {
report[index] = layer_key;
}
button.previous_pressed = button.pressed;
}
if sticky_toggle_requested {
self.toggle_sticky_state();
}
const STICKY_REPORT_INDEX: usize = 46;
report[STICKY_REPORT_INDEX] = self.sticky_key;
report
}
fn fn_mode(pressed_keys: &KeyMatrix) -> u8 {
// Count the active FN buttons and clamp to the highest supported layer.
let active_fn_keys = layout::FN_BUTTONS
.iter()
.filter(|key_index| pressed_keys[**key_index as usize])
.count() as u8;
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 {
fn default() -> Self {
Self::new()
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
#[test]
fn fn_mode_caps_at_two() {
// Ensure the layer helper never exceeds the maximum FN layer value.
let mut pressed = [false; NUMBER_OF_KEYS];
pressed[layout::FN_BUTTONS[0] as usize] = true;
pressed[layout::FN_BUTTONS[1] as usize] = true;
pressed[layout::FN_BUTTONS[2] as usize] = true;
assert_eq!(KeyboardState::fn_mode(&pressed), 2);
}
#[test]
fn sticky_button_transitions_between_states() {
// Sticky button chord should arm, latch on next key, and clear when pressed again.
let mut state = KeyboardState::new();
let mut pressed = [false; NUMBER_OF_KEYS];
pressed[layout::FN_BUTTONS[0] as usize] = true;
pressed[layout::FN_BUTTONS[1] as usize] = true;
pressed[layout::STICKY_BUTTON[0] as usize] = true;
state.process_scan(pressed);
assert_eq!(state.sticky_state(), StickyState::Armed);
// Press another key to latch sticky
let mut pressed = [false; NUMBER_OF_KEYS];
pressed[layout::FN_BUTTONS[0] as usize] = true;
pressed[layout::FN_BUTTONS[1] as usize] = true;
pressed[0] = true;
state.process_scan(pressed);
assert_eq!(state.sticky_state(), StickyState::Latched);
// Press sticky again to clear
let mut pressed = [false; NUMBER_OF_KEYS];
pressed[layout::FN_BUTTONS[0] as usize] = true;
pressed[layout::FN_BUTTONS[1] as usize] = true;
pressed[layout::STICKY_BUTTON[0] as usize] = true;
state.process_scan(pressed);
assert_eq!(state.sticky_state(), StickyState::Inactive);
}
#[test]
fn status_summary_reflects_keyboard_state() {
// Status summary must mirror the internal keyboard and USB flags.
let mut state = KeyboardState::new();
state.update_caps_lock(true);
state.mark_started();
let summary = state.status_summary();
assert!(summary.caps_lock_active);
assert!(summary.usb_active);
assert!(summary.usb_initialized);
assert!(!summary.sticky_armed);
assert!(!summary.sticky_latched);
}
#[test]
fn sticky_key_is_exposed_in_reports_after_latch() {
// Ensure the latched sticky modifier key stays in the HID report stream until cleared.
let mut state = KeyboardState::new();
let mut pressed = [false; NUMBER_OF_KEYS];
pressed[layout::FN_BUTTONS[0] as usize] = true;
pressed[layout::FN_BUTTONS[1] as usize] = true;
pressed[layout::STICKY_BUTTON[0] as usize] = true;
state.process_scan(pressed);
let mut pressed = [false; NUMBER_OF_KEYS];
pressed[layout::FN_BUTTONS[0] as usize] = true;
pressed[layout::FN_BUTTONS[1] as usize] = true;
pressed[0] = true;
let report = state.process_scan(pressed);
assert_eq!(state.sticky_state(), StickyState::Latched);
assert_eq!(report[0], layout::MAP[2][0]);
assert_eq!(report[46], layout::MAP[2][0]);
let pressed = [false; NUMBER_OF_KEYS];
let report = state.process_scan(pressed);
assert_eq!(report[46], layout::MAP[2][0]);
}
}