cmdr-joystick/rp2040/src/usb_report.rs

404 lines
14 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.

//! USB HID report generation for CMDR Joystick
//!
//! Converts processed axis values and button states into a `JoystickReport`
//! that matches the HID descriptor defined in `usb_joystick_device.rs`.
//! Also contains support for virtual throttle mode and HAT directions.
use crate::axis::{
remap, GimbalAxis, GIMBAL_AXIS_LEFT_X, GIMBAL_AXIS_LEFT_Y, GIMBAL_AXIS_RIGHT_X,
GIMBAL_AXIS_RIGHT_Y,
};
use crate::buttons::{Button, TOTAL_BUTTONS};
use crate::hardware::{ADC_MAX, ADC_MIN, AXIS_CENTER};
use crate::mapping::{USB_HAT_LEFT, USB_HAT_UP};
use crate::usb_joystick_device::JoystickReport;
// ==================== USB REPORT GENERATION ====================
/// Convert 12bit unsigned values to 16bit signed for USB HID joystick reports.
///
/// Maps 12-bit ADC values (0-4095) to 16-bit signed USB HID values (-32768 to 32767).
/// This is specifically for USB joystick axis reporting.
///
/// # Arguments
/// * `val` - 12-bit ADC value (0-4095)
///
/// # Returns
/// 16-bit signed value suitable for USB HID joystick reports
///
/// # Panics
/// Panics if val > 0x0FFF (not a valid 12-bit value)
pub fn axis_12bit_to_i16(val: u16) -> i16 {
assert!(val <= 0x0FFF); // Ensure it's 12-bit
// Map 0..4095 → -32768..32767 using integer math
// Formula: ((val * 65535) / 4095) - 32768
let scaled = ((val as u32 * 65535) / 4095) as i32 - 32768;
scaled as i16
}
/// Generate a complete USB HID joystick report from the current system state.
///
/// # Arguments
/// * `matrix_keys` - Array of button states from the button matrix
/// * `axis` - Array of gimbal axis values
/// * `virtual_ry` - Virtual RY axis value
/// * `virtual_rz` - Virtual RZ axis value
/// * `vt_enable` - Virtual throttle mode enable flag
///
/// # Returns
/// A complete `JoystickReport` ready to be sent via USB HID
pub fn get_joystick_report(
matrix_keys: &mut [Button; TOTAL_BUTTONS],
axis: &mut [GimbalAxis; 4],
virtual_ry: u16,
virtual_rz: u16,
vt_enable: &bool,
) -> JoystickReport {
// Convert axis values to 16-bit signed integers for USB HID
let x: i16 = axis_12bit_to_i16(axis[GIMBAL_AXIS_LEFT_X].value);
let y: i16 = axis_12bit_to_i16(axis[GIMBAL_AXIS_LEFT_Y].value);
let mut z: i16 = axis_12bit_to_i16(axis[GIMBAL_AXIS_RIGHT_X].value);
let rx: i16 = axis_12bit_to_i16(ADC_MAX - axis[GIMBAL_AXIS_RIGHT_Y].value);
let ry: i16 = axis_12bit_to_i16(virtual_ry);
let rz: i16 = axis_12bit_to_i16(virtual_rz);
let mut slider: i16 = axis_12bit_to_i16(ADC_MIN);
let mut hat: u8 = 8; // Hat center position
// Virtual throttle mode:
// - Disable Z axis
// - Map rightX to Slider with symmetric behavior around center
if *vt_enable {
if axis[GIMBAL_AXIS_RIGHT_X].value >= AXIS_CENTER {
slider = axis_12bit_to_i16(remap(
axis[GIMBAL_AXIS_RIGHT_X].value,
AXIS_CENTER,
ADC_MAX,
ADC_MIN,
ADC_MAX,
));
} else {
slider = axis_12bit_to_i16(
ADC_MAX
- remap(
axis[GIMBAL_AXIS_RIGHT_X].value,
ADC_MIN,
AXIS_CENTER,
ADC_MIN,
ADC_MAX,
),
);
}
z = 0;
}
// Process buttons and build the USB button bitmask + HAT value
let mut buttons: u32 = 0;
for key in matrix_keys.iter_mut() {
if key.enable_long_press {
if key.active_usb_button != 0 {
// Check if key is assigned as hat switch
if key.active_usb_button >= USB_HAT_UP && key.active_usb_button <= USB_HAT_LEFT {
hat = (key.active_usb_button as u8 - USB_HAT_UP as u8) * 2;
} else {
buttons |= 1 << (key.active_usb_button - 1);
}
}
} else if key.pressed && key.usb_button != 0 {
// Check if key is assigned as hat switch
if key.usb_button >= USB_HAT_UP && key.usb_button <= USB_HAT_LEFT {
hat = (key.usb_button as u8 - USB_HAT_UP as u8) * 2;
} else {
buttons |= 1 << (key.usb_button - 1);
}
}
}
// Reset perbutton USB change flags for the next iteration
for key in matrix_keys.iter_mut() {
key.usb_changed = false;
}
JoystickReport {
x,
y,
z,
rx,
ry,
rz,
slider,
hat,
buttons,
}
}
// ==================== TESTS ====================
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use crate::buttons::Button;
#[test]
fn test_axis_12bit_to_i16_boundaries() {
// Test minimum value
assert_eq!(axis_12bit_to_i16(0), -32768);
// Test maximum value
assert_eq!(axis_12bit_to_i16(4095), 32767);
// Test center value
let center_result = axis_12bit_to_i16(2047);
assert!(center_result < 100 && center_result > -100); // Should be close to 0
}
#[test]
fn test_axis_12bit_to_i16_conversion() {
// Test known conversion
let result = axis_12bit_to_i16(2047); // Half of 4095
assert!(result < 100 && result > -100); // Should be close to 0
let result = axis_12bit_to_i16(1023); // Quarter
assert!(result < -16000);
let result = axis_12bit_to_i16(3071); // Three quarters
assert!(result > 16000);
}
#[test]
fn test_remap_function() {
// Test basic remapping
assert_eq!(remap(500, 0, 1000, 0, 2000), 1000);
assert_eq!(remap(0, 0, 1000, 0, 2000), 0);
assert_eq!(remap(1000, 0, 1000, 0, 2000), 2000);
// Test center remapping
assert_eq!(remap(2048, 0, 4095, 0, 4095), 2048);
// Test reverse remapping behavior: when out_min > out_max, constrain will
// clamp results to the "valid" range based on the constraint logic
assert_eq!(remap(0, 0, 1000, 2000, 0), 0); // Calculated 2000, constrained to 0
assert_eq!(remap(1000, 0, 1000, 2000, 0), 2000); // Calculated 0, constrained to 2000
}
#[test]
fn test_joystick_report_basic_axes() {
// Remap helper scales values between integer ranges with clamping.
let mut buttons = [Button::default(); TOTAL_BUTTONS];
let mut axes = [GimbalAxis::new(); 4];
// Set test axis values
axes[GIMBAL_AXIS_LEFT_X].value = 2048; // Center
axes[GIMBAL_AXIS_LEFT_Y].value = 1024; // Quarter
axes[GIMBAL_AXIS_RIGHT_X].value = 3072; // Three quarter
axes[GIMBAL_AXIS_RIGHT_Y].value = 4095; // Max
let virtual_ry = 1000;
let virtual_rz = 2000;
let vt_enable = false;
let report =
get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable);
// Verify axis conversions
assert_eq!(report.x, axis_12bit_to_i16(2048));
assert_eq!(report.y, axis_12bit_to_i16(1024));
assert_eq!(report.z, axis_12bit_to_i16(3072));
assert_eq!(report.rx, axis_12bit_to_i16(0)); // ADC_MAX - 4095 = 0
assert_eq!(report.ry, axis_12bit_to_i16(1000));
assert_eq!(report.rz, axis_12bit_to_i16(2000));
assert_eq!(report.slider, axis_12bit_to_i16(ADC_MIN));
assert_eq!(report.hat, 8); // Center
assert_eq!(report.buttons, 0);
}
#[test]
fn test_virtual_throttle_mode() {
// Slider combines throttle hold and virtual throttle for report serialization.
let mut buttons = [Button::default(); TOTAL_BUTTONS];
let mut axes = [GimbalAxis::new(); 4];
// Set right X axis above center for virtual throttle test
axes[GIMBAL_AXIS_RIGHT_X].value = 3072; // Above center
let virtual_ry = 0;
let virtual_rz = 0;
let vt_enable = true;
let report =
get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable);
// In VT mode, z should be 0
assert_eq!(report.z, 0);
// Slider should be calculated from right X axis
let expected_slider_value = remap(3072, AXIS_CENTER, ADC_MAX, ADC_MIN, ADC_MAX);
assert_eq!(report.slider, axis_12bit_to_i16(expected_slider_value));
}
#[test]
fn test_virtual_throttle_below_center() {
// When VT mode is enabled below center, slider should invert along the left half of travel.
let mut buttons = [Button::default(); TOTAL_BUTTONS];
let mut axes = [GimbalAxis::new(); 4];
// Set right X axis below center for virtual throttle test
axes[GIMBAL_AXIS_RIGHT_X].value = 1024; // Below center
let virtual_ry = 0;
let virtual_rz = 0;
let vt_enable = true;
let report =
get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable);
// In VT mode, z should be 0
assert_eq!(report.z, 0);
// Slider should be calculated from right X axis with inversion
let remapped_value = remap(1024, ADC_MIN, AXIS_CENTER, ADC_MIN, ADC_MAX);
let expected_slider_value = ADC_MAX - remapped_value;
assert_eq!(report.slider, axis_12bit_to_i16(expected_slider_value));
}
#[test]
fn test_button_mapping_regular_buttons() {
// Regular buttons should set their HID bitmask without disturbing hat state.
let mut buttons = [Button::default(); TOTAL_BUTTONS];
let mut axes = [GimbalAxis::new(); 4];
// Set some buttons pressed
buttons[0].pressed = true;
buttons[0].usb_button = 1; // USB button 1
buttons[1].pressed = true;
buttons[1].usb_button = 5; // USB button 5
let virtual_ry = 0;
let virtual_rz = 0;
let vt_enable = false;
let report =
get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable);
// Check button bits are set correctly
assert_eq!(report.buttons & (1 << 0), 1 << 0); // Button 1
assert_eq!(report.buttons & (1 << 4), 1 << 4); // Button 5
assert_eq!(report.hat, 8); // Center (no hat buttons)
}
#[test]
fn test_hat_switch_mapping() {
// A single hat direction should map to the appropriate HID hat value.
let mut buttons = [Button::default(); TOTAL_BUTTONS];
let mut axes = [GimbalAxis::new(); 4];
// Set a HAT switch button
buttons[0].pressed = true;
buttons[0].usb_button = USB_HAT_UP;
let virtual_ry = 0;
let virtual_rz = 0;
let vt_enable = false;
let report =
get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable);
// Check HAT direction is set correctly
let expected_hat = (USB_HAT_UP as u8 - USB_HAT_UP as u8) * 2; // Should be 0 (up)
assert_eq!(report.hat, expected_hat);
assert_eq!(report.buttons, 0); // No regular buttons
}
#[test]
fn test_long_press_button_handling() {
// Buttons configured for long press should surface the alternate USB id.
let mut buttons = [Button::default(); TOTAL_BUTTONS];
let mut axes = [GimbalAxis::new(); 4];
// Set a button with long press enabled
buttons[0].enable_long_press = true;
buttons[0].active_usb_button = 3; // USB button 3
let virtual_ry = 0;
let virtual_rz = 0;
let vt_enable = false;
let report =
get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable);
// Check long press button is handled
assert_eq!(report.buttons & (1 << 2), 1 << 2); // Button 3
}
#[test]
fn test_usb_changed_flag_reset() {
// Packaging a report should clear the usb_changed flags once consumed.
let mut buttons = [Button::default(); TOTAL_BUTTONS];
let mut axes = [GimbalAxis::new(); 4];
// Set USB changed flags
buttons[0].usb_changed = true;
buttons[1].usb_changed = true;
let virtual_ry = 0;
let virtual_rz = 0;
let vt_enable = false;
get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable);
// Verify flags are reset
assert!(!buttons[0].usb_changed);
assert!(!buttons[1].usb_changed);
}
#[test]
fn test_edge_case_hat_values() {
// Additional hat directions should map to the correct encoded value.
let mut buttons = [Button::default(); TOTAL_BUTTONS];
let mut axes = [GimbalAxis::new(); 4];
// Test different HAT switch values
buttons[0].pressed = true;
buttons[0].usb_button = USB_HAT_LEFT;
let virtual_ry = 0;
let virtual_rz = 0;
let vt_enable = false;
let report =
get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable);
// Check HAT left direction
let expected_hat = (USB_HAT_LEFT as u8 - USB_HAT_UP as u8) * 2;
assert_eq!(report.hat, expected_hat);
}
#[test]
fn test_multiple_buttons_and_hat() {
// Report should accommodate simultaneous button presses and hat direction.
let mut buttons = [Button::default(); TOTAL_BUTTONS];
let mut axes = [GimbalAxis::new(); 4];
// Mix regular buttons and HAT switch
buttons[0].pressed = true;
buttons[0].usb_button = 1; // Regular button
buttons[1].pressed = true;
buttons[1].usb_button = USB_HAT_UP; // HAT switch
buttons[2].pressed = true;
buttons[2].usb_button = 8; // Another regular button
let virtual_ry = 0;
let virtual_rz = 0;
let vt_enable = false;
let report =
get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable);
// Check both regular buttons and HAT
assert_eq!(report.buttons & (1 << 0), 1 << 0); // Button 1
assert_eq!(report.buttons & (1 << 7), 1 << 7); // Button 8
assert_eq!(report.hat, 0); // HAT up direction
}
}