//! 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 } }