//! Button processing for CMDR Joystick //! //! Responsibilities //! - Integrate button matrix results and extra pins //! - Track pressed/previous states and USB change flags //! - Detect short/long presses with minimal hold timing //! - Filter HAT switches so only one direction remains active //! - Evaluate special combinations (bootloader, calibration, etc.) //! - Expose a compact state consumed by USB report generation use crate::button_matrix::{ButtonMatrix, MatrixPins}; use crate::hardware::{AXIS_CENTER, BUTTON_COLS, BUTTON_ROWS, NUMBER_OF_BUTTONS}; use crate::mapping::*; use embedded_hal::digital::InputPin; use rp2040_hal::timer::Timer; // Total buttons including the two extra (non‑matrix) buttons pub const TOTAL_BUTTONS: usize = NUMBER_OF_BUTTONS + 2; pub type JoystickButtonMatrix = ButtonMatrix< MatrixPins<{ BUTTON_ROWS }, { BUTTON_COLS }>, { BUTTON_ROWS }, { BUTTON_COLS }, { NUMBER_OF_BUTTONS }, >; // ==================== BUTTON STRUCT ==================== #[derive(Copy, Clone, Default)] pub struct Button { pub pressed: bool, pub previous_pressed: bool, pub usb_changed: bool, pub usb_button: usize, // For short press pub usb_button_long: usize, // For long press pub enable_long_press: bool, // Flag to enable special behavior pub enable_long_hold: bool, // Flag to enable special behavior // Internals pub press_start_time: u32, // When physical press started pub long_press_handled: bool, // True if long press activated pub active_usb_button: usize, // Currently active USB button pub usb_press_active: bool, // Is USB press currently "down" pub usb_press_start_time: u32, // When USB press was sent } // ==================== SPECIAL ACTIONS ==================== /// High‑level actions triggered by dedicated button combinations. #[derive(Debug, PartialEq)] pub enum SpecialAction { None, Bootloader, StartCalibration, CancelCalibration, ThrottleHold(u16), // Value to hold VirtualThrottleToggle, CalibrationSetModeM10, CalibrationSetModeM7, CalibrationSave, } // ==================== BUTTON MANAGER ==================== /// Aggregates and processes all buttons, exposing a stable API to the rest of the firmware. pub struct ButtonManager { pub buttons: [Button; TOTAL_BUTTONS], } impl Default for ButtonManager { fn default() -> Self { // Build a button manager with default-initialized buttons and state flags. Self::new() } } impl ButtonManager { pub fn new() -> Self { let mut buttons = [Button::default(); TOTAL_BUTTONS]; // Configure button mappings using existing mapping functionality configure_button_mappings(&mut buttons); Self { buttons } } /// Update button states from the button matrix snapshot. pub fn update_from_matrix(&mut self, matrix: &mut JoystickButtonMatrix) { for (index, key) in matrix.buttons_pressed().iter().enumerate() { self.buttons[index].pressed = *key; } } /// Update extra (non‑matrix) button states from hardware pins. pub fn update_extra_buttons(&mut self, left_pin: &mut L, right_pin: &mut R) where L: InputPin, R: InputPin, L::Error: core::fmt::Debug, R::Error: core::fmt::Debug, { self.buttons[BUTTON_FRONT_LEFT_EXTRA].pressed = left_pin.is_low().unwrap_or(false); self.buttons[BUTTON_FRONT_RIGHT_EXTRA].pressed = right_pin.is_low().unwrap_or(false); } /// Filter HAT switches so only a single direction (or center) can be active. pub fn filter_hat_switches(&mut self) { const LEFT_HAT_DIRECTIONS: [usize; 4] = [ BUTTON_TOP_LEFT_HAT_UP, BUTTON_TOP_LEFT_HAT_RIGHT, BUTTON_TOP_LEFT_HAT_DOWN, BUTTON_TOP_LEFT_HAT_LEFT, ]; const RIGHT_HAT_DIRECTIONS: [usize; 4] = [ BUTTON_TOP_RIGHT_HAT_UP, BUTTON_TOP_RIGHT_HAT_RIGHT, BUTTON_TOP_RIGHT_HAT_DOWN, BUTTON_TOP_RIGHT_HAT_LEFT, ]; self.reconcile_hat(&LEFT_HAT_DIRECTIONS, BUTTON_TOP_LEFT_HAT); self.reconcile_hat(&RIGHT_HAT_DIRECTIONS, BUTTON_TOP_RIGHT_HAT); } fn reconcile_hat(&mut self, directions: &[usize; 4], center: usize) { // Normalize hat inputs by clearing the center and conflicting directions. let pressed_count = directions .iter() .filter(|&&index| self.buttons[index].pressed) .count(); if pressed_count == 0 { return; } self.buttons[center].pressed = false; if pressed_count >= 2 { for &index in directions.iter() { self.buttons[index].pressed = false; } } } /// Update press types (short/long) and USB change flags; returns whether USB should be updated. pub fn process_button_logic(&mut self, current_time: u32) -> bool { let mut usb_activity = false; for button in self.buttons.iter_mut() { update_button_press_type(button, current_time); if button.pressed != button.previous_pressed { button.usb_changed = true; } if button.usb_changed { usb_activity = true; } button.previous_pressed = button.pressed; } usb_activity } /// Convenience wrapper that derives `current_time` from the hardware timer. pub fn process_button_logic_with_timer(&mut self, timer: &Timer) -> bool { let current_time = (timer.get_counter().ticks() / 1000) as u32; self.process_button_logic(current_time) } /// Check for special button combinations (bootloader, calibration state/mode, VT, etc.). pub fn check_special_combinations( &self, unprocessed_axis_value: u16, calibration_active: bool, ) -> SpecialAction { // Secondary way to enter bootloader if self.buttons[BUTTON_FRONT_LEFT_LOWER].pressed && self.buttons[BUTTON_TOP_LEFT_MODE].pressed && self.buttons[BUTTON_TOP_RIGHT_MODE].pressed { return SpecialAction::Bootloader; } // Calibration mode toggle (start/cancel with same button combination) if self.buttons[BUTTON_FRONT_LEFT_UPPER].pressed && self.buttons[BUTTON_TOP_LEFT_MODE].pressed && self.buttons[BUTTON_TOP_RIGHT_MODE].pressed { if calibration_active { return SpecialAction::CancelCalibration; } else { return SpecialAction::StartCalibration; } } // Check for throttle hold button press if let Some(th_button) = self.get_button_press_event(TH_BUTTON) { if th_button { if unprocessed_axis_value != AXIS_CENTER { return SpecialAction::ThrottleHold(unprocessed_axis_value); } else { return SpecialAction::ThrottleHold(AXIS_CENTER); } } } // Check for virtual throttle button press if let Some(vt_button) = self.get_button_press_event(VT_BUTTON) { if vt_button { return SpecialAction::VirtualThrottleToggle; } } // Calibration mode selection (only during calibration) if calibration_active { if let Some(up_button) = self.get_button_press_event(BUTTON_TOP_LEFT_UP) { if up_button { return SpecialAction::CalibrationSetModeM10; } } if let Some(down_button) = self.get_button_press_event(BUTTON_TOP_LEFT_DOWN) { if down_button { return SpecialAction::CalibrationSetModeM7; } } if let Some(save_button) = self.get_button_press_event(BUTTON_TOP_RIGHT_HAT) { if save_button { return SpecialAction::CalibrationSave; } } } SpecialAction::None } /// Get a change event for a button: Some(true)=press, Some(false)=release, None=no change. fn get_button_press_event(&self, button_index: usize) -> Option { // Report the updated pressed state whenever it differs from the previous sample. let button = &self.buttons[button_index]; if button.pressed != button.previous_pressed { Some(button.pressed) } else { None } } /// Get mutable access to the internal buttons array. pub fn buttons_mut(&mut self) -> &mut [Button; TOTAL_BUTTONS] { &mut self.buttons } /// Get read‑only access to the internal buttons array. pub fn buttons(&self) -> &[Button; TOTAL_BUTTONS] { &self.buttons } } // ==================== BUTTON PRESS TYPE DETECTION ==================== /// Update button press type and manage USB press lifecycle. /// /// Behavior /// - On physical press: record start time and defer decision /// - Long press: if enabled and held >= threshold, activate `usb_button_long` /// - Short press: on release (and if long not activated), activate `usb_button` /// - USB press lifetime: auto‑release after a minimal hold so the host sees a pulse fn update_button_press_type(button: &mut Button, current_time: u32) { // Transition a single button between short/long press USB outputs. const LONG_PRESS_THRESHOLD: u32 = 200; // Pressing button if button.pressed && !button.previous_pressed { button.press_start_time = current_time; button.long_press_handled = false; } // While held: trigger long press if applicable if button.pressed && button.enable_long_press && !button.long_press_handled && current_time - button.press_start_time >= LONG_PRESS_THRESHOLD { button.active_usb_button = button.usb_button_long; button.usb_press_start_time = current_time; button.usb_press_active = true; button.usb_changed = true; button.long_press_handled = true; } // Releasing button if !button.pressed && button.previous_pressed { // If long press wasn't triggered, it's a short press if (!button.enable_long_press || !button.long_press_handled) && button.usb_button != 0 { button.active_usb_button = button.usb_button; button.usb_press_start_time = current_time; button.usb_press_active = true; button.usb_changed = true; } // If long press was active, release now if button.long_press_handled && button.usb_press_active { button.usb_changed = true; button.usb_press_active = false; button.active_usb_button = 0; } } // Auto‑release generated USB press after minimum hold time const USB_MIN_HOLD_MS: u32 = 50; let elapsed = current_time.saturating_sub(button.usb_press_start_time); let should_release = if button.long_press_handled { !button.enable_long_hold && elapsed >= USB_MIN_HOLD_MS } else { !button.pressed && elapsed >= USB_MIN_HOLD_MS }; if button.usb_press_active && should_release { button.usb_changed = true; button.usb_press_active = false; button.active_usb_button = 0; } } // ==================== CONSTANTS ==================== // Special button functions (from main.rs) pub const TH_BUTTON: usize = BUTTON_TOP_LEFT_MODE; pub const VT_BUTTON: usize = BUTTON_TOP_RIGHT_MODE; // ==================== TESTS ==================== #[cfg(all(test, feature = "std"))] mod tests { use super::*; #[test] fn test_button_manager_creation() { // Button manager should allocate an entry for every physical button. let manager = ButtonManager::new(); assert_eq!(manager.buttons.len(), TOTAL_BUTTONS); } #[test] fn test_button_default_state() { // Default button instances start unpressed with no lingering USB state. let button = Button::default(); assert!(!button.pressed); assert!(!button.previous_pressed); assert!(!button.usb_changed); assert!(!button.long_press_handled); } #[test] fn test_special_action_combinations() { // The bootloader combo should trigger when all required buttons are pressed. let mut manager = ButtonManager::new(); // Test bootloader combination manager.buttons[BUTTON_FRONT_LEFT_LOWER].pressed = true; manager.buttons[BUTTON_TOP_LEFT_MODE].pressed = true; manager.buttons[BUTTON_TOP_RIGHT_MODE].pressed = true; let action = manager.check_special_combinations(AXIS_CENTER, false); assert_eq!(action, SpecialAction::Bootloader); } #[test] fn test_calibration_combination() { // Calibration combo should generate the start-calibration action. let mut manager = ButtonManager::new(); // Test calibration combination manager.buttons[BUTTON_FRONT_LEFT_UPPER].pressed = true; manager.buttons[BUTTON_TOP_LEFT_MODE].pressed = true; manager.buttons[BUTTON_TOP_RIGHT_MODE].pressed = true; let action = manager.check_special_combinations(AXIS_CENTER, false); assert_eq!(action, SpecialAction::StartCalibration); } #[test] fn test_throttle_hold_center() { // Throttle hold combo should capture the centered axis value when centered. let mut manager = ButtonManager::new(); manager.buttons[TH_BUTTON].pressed = true; manager.buttons[TH_BUTTON].previous_pressed = false; let action = manager.check_special_combinations(AXIS_CENTER, false); assert_eq!(action, SpecialAction::ThrottleHold(AXIS_CENTER)); } #[test] fn test_throttle_hold_value() { // Off-center throttle hold should capture the live axis value for hold. let mut manager = ButtonManager::new(); manager.buttons[TH_BUTTON].pressed = true; manager.buttons[TH_BUTTON].previous_pressed = false; let test_value = 3000u16; let action = manager.check_special_combinations(test_value, false); assert_eq!(action, SpecialAction::ThrottleHold(test_value)); } #[test] fn test_virtual_throttle_toggle() { // Virtual throttle button should emit the toggle action when pressed. let mut manager = ButtonManager::new(); manager.buttons[VT_BUTTON].pressed = true; manager.buttons[VT_BUTTON].previous_pressed = false; let action = manager.check_special_combinations(AXIS_CENTER, false); assert_eq!(action, SpecialAction::VirtualThrottleToggle); } #[test] fn test_hat_switch_filtering_left() { // Left hat should clear center and conflicting directions when multiple inputs are active. let mut manager = ButtonManager::new(); // Press multiple directional buttons on left hat manager.buttons[BUTTON_TOP_LEFT_HAT_UP].pressed = true; manager.buttons[BUTTON_TOP_LEFT_HAT_RIGHT].pressed = true; manager.filter_hat_switches(); // Multiple directions cancel the hat output completely let pressed_count = (BUTTON_TOP_LEFT_HAT_UP..=BUTTON_TOP_LEFT_HAT_LEFT) .filter(|&i| manager.buttons[i].pressed) .count(); assert_eq!(pressed_count, 0); assert!(!manager.buttons[BUTTON_TOP_LEFT_HAT].pressed); } #[test] fn test_hat_switch_filtering_right() { // Right hat should behave the same way, disabling conflicts and the center button. let mut manager = ButtonManager::new(); // Press multiple directional buttons on right hat manager.buttons[BUTTON_TOP_RIGHT_HAT_UP].pressed = true; manager.buttons[BUTTON_TOP_RIGHT_HAT_DOWN].pressed = true; manager.filter_hat_switches(); // Multiple directions cancel the hat output completely let pressed_count = (BUTTON_TOP_RIGHT_HAT_UP..=BUTTON_TOP_RIGHT_HAT_LEFT) .filter(|&i| manager.buttons[i].pressed) .count(); assert_eq!(pressed_count, 0); assert!(!manager.buttons[BUTTON_TOP_RIGHT_HAT].pressed); } #[test] fn test_hat_center_button_filtering() { // Pressing a direction should suppress the corresponding hat center button. let mut manager = ButtonManager::new(); // Press directional button and center button manager.buttons[BUTTON_TOP_LEFT_HAT].pressed = true; manager.buttons[BUTTON_TOP_LEFT_HAT_UP].pressed = true; manager.filter_hat_switches(); // Center button should be disabled when directional is pressed assert!(!manager.buttons[BUTTON_TOP_LEFT_HAT].pressed); // But single direction should remain active assert!(manager.buttons[BUTTON_TOP_LEFT_HAT_UP].pressed); } #[test] fn test_hat_switch_single_direction_allowed() { // A single direction press must remain active for both hats. let mut manager = ButtonManager::new(); // Press only one directional button on left hat manager.buttons[BUTTON_TOP_LEFT_HAT_UP].pressed = true; manager.filter_hat_switches(); // Single direction should remain active assert!(manager.buttons[BUTTON_TOP_LEFT_HAT_UP].pressed); // Test same for right hat let mut manager = ButtonManager::new(); manager.buttons[BUTTON_TOP_RIGHT_HAT_DOWN].pressed = true; manager.filter_hat_switches(); // Single direction should remain active assert!(manager.buttons[BUTTON_TOP_RIGHT_HAT_DOWN].pressed); } #[test] fn test_hat_center_button_works_alone() { // When no direction is pressed, the center button should report as pressed. let mut manager = ButtonManager::new(); // Press only center button (no directions) manager.buttons[BUTTON_TOP_LEFT_HAT].pressed = true; manager.filter_hat_switches(); // Center button should remain active when no directions are pressed assert!(manager.buttons[BUTTON_TOP_LEFT_HAT].pressed); } #[test] fn test_button_press_type_short_press() { // Short presses should emit the primary USB button and flag a USB change. let mut button = Button::default(); button.usb_button = 1; button.enable_long_press = false; // Press button button.pressed = true; update_button_press_type(&mut button, 100); button.previous_pressed = button.pressed; // Update state // Release button quickly (before long press threshold) button.pressed = false; update_button_press_type(&mut button, 150); assert_eq!(button.active_usb_button, 1); assert!(button.usb_press_active); assert!(button.usb_changed); } #[test] fn test_button_press_type_long_press() { // Long presses should switch to the alternate USB button and mark handled state. let mut button = Button::default(); button.usb_button = 1; button.usb_button_long = 2; button.enable_long_press = true; // Press button button.pressed = true; update_button_press_type(&mut button, 100); button.previous_pressed = button.pressed; // Update state // Hold for long press threshold update_button_press_type(&mut button, 350); // 250ms later assert_eq!(button.active_usb_button, 2); // Long press button assert!(button.usb_press_active); assert!(button.long_press_handled); } #[test] fn test_button_press_type_long_press_auto_release_once() { // Non-hold long presses should auto-release once after triggering the long press. let mut button = Button::default(); button.usb_button_long = 2; button.enable_long_press = true; button.enable_long_hold = false; // Press the button button.pressed = true; update_button_press_type(&mut button, 0); button.previous_pressed = button.pressed; // Hold long enough to trigger the long press path update_button_press_type(&mut button, 250); assert!(button.usb_press_active); assert_eq!(button.active_usb_button, 2); // Clear the changed flag to emulate USB stack observing it button.usb_changed = false; // Keep holding and ensure we auto-release exactly once update_button_press_type(&mut button, 320); assert!(!button.usb_press_active); assert!(button.usb_changed); button.usb_changed = false; update_button_press_type(&mut button, 400); assert!(!button.usb_press_active); assert!(!button.usb_changed); } #[test] fn test_timer_integration_method_exists() { // Document that the timer-backed helper stays callable without hardware wiring. let manager = ButtonManager::new(); // This test verifies the timer integration method signature and basic functionality // without requiring actual hardware timer setup in the test environment. // The method should delegate to process_button_logic with proper time calculation. // Verify the ButtonManager exists and has the expected structure assert_eq!(manager.buttons.len(), TOTAL_BUTTONS); // The process_button_logic_with_timer method requires actual Timer hardware // which isn't available in the test environment, but the method compilation // is verified through the cargo check above. } }