cmdr-joystick/rp2040/src/buttons.rs

601 lines
21 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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 (nonmatrix) 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 ====================
/// Highlevel 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 (nonmatrix) button states from hardware pins.
pub fn update_extra_buttons<L, R>(&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<bool> {
// 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 readonly 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: autorelease 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;
}
}
// Autorelease 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.
}
}