404 lines
14 KiB
Rust
404 lines
14 KiB
Rust
//! 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 12‑bit unsigned values to 16‑bit 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 right‑X 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 per‑button 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
|
||
}
|
||
}
|