Added USB idle timeout

This commit is contained in:
Christoffer Martinsson 2025-09-16 11:21:17 +02:00
parent de138354b1
commit 3324de3e9d
10 changed files with 187 additions and 121 deletions

View File

@ -309,8 +309,7 @@ fn update_button_press_type(button: &mut Button, current_time: u32) {
!button.pressed && elapsed >= USB_MIN_HOLD_MS !button.pressed && elapsed >= USB_MIN_HOLD_MS
}; };
if button.usb_press_active && should_release if button.usb_press_active && should_release {
{
button.usb_changed = true; button.usb_changed = true;
button.usb_press_active = false; button.usb_press_active = false;
button.active_usb_button = 0; button.active_usb_button = 0;

View File

@ -11,7 +11,7 @@
//! behind simple `read_fn`/`write_fn` closures so the code can be unittested on //! behind simple `read_fn`/`write_fn` closures so the code can be unittested on
//! a host without hardware. //! a host without hardware.
use crate::axis::{GIMBAL_MODE_M7, GIMBAL_MODE_M10, GimbalAxis}; use crate::axis::{GimbalAxis, GIMBAL_MODE_M10, GIMBAL_MODE_M7};
use crate::hardware::NBR_OF_GIMBAL_AXIS; use crate::hardware::NBR_OF_GIMBAL_AXIS;
use crate::storage; use crate::storage;
use dyn_smooth::DynamicSmootherEcoI32; use dyn_smooth::DynamicSmootherEcoI32;
@ -148,10 +148,8 @@ impl CalibrationManager {
/// Load peraxis calibration data from EEPROM. /// Load peraxis calibration data from EEPROM.
/// ///
/// Updates the provided axes with loaded values; retains defaults on error. /// Updates the provided axes with loaded values; retains defaults on error.
pub fn load_axis_calibration<F>( pub fn load_axis_calibration<F>(axes: &mut [GimbalAxis; NBR_OF_GIMBAL_AXIS], read_fn: &mut F)
axes: &mut [GimbalAxis; NBR_OF_GIMBAL_AXIS], where
read_fn: &mut F,
) where
F: FnMut(u32) -> Result<u8, ()>, F: FnMut(u32) -> Result<u8, ()>,
{ {
for (index, axis) in axes.iter_mut().enumerate() { for (index, axis) in axes.iter_mut().enumerate() {
@ -176,7 +174,6 @@ impl CalibrationManager {
storage::read_gimbal_mode(read_fn).unwrap_or(GIMBAL_MODE_M10) storage::read_gimbal_mode(read_fn).unwrap_or(GIMBAL_MODE_M10)
} }
/// Reset each axis calibration to its current smoothed center. /// Reset each axis calibration to its current smoothed center.
fn reset_axis_calibration( fn reset_axis_calibration(
&self, &self,
@ -342,11 +339,6 @@ mod tests {
assert_eq!(manager.get_gimbal_mode(), GIMBAL_MODE_M10); assert_eq!(manager.get_gimbal_mode(), GIMBAL_MODE_M10);
} }
#[test] #[test]
fn test_load_axis_calibration_success() { fn test_load_axis_calibration_success() {
let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS]; let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
@ -362,7 +354,7 @@ mod tests {
4 => Ok(11), // max high byte 4 => Ok(11), // max high byte
5 => Ok(208), // center low byte (2000 = 0x07D0, low byte = 208) 5 => Ok(208), // center low byte (2000 = 0x07D0, low byte = 208)
6 => Ok(7), // center high byte 6 => Ok(7), // center high byte
_ => Err(()), // Other addresses fail _ => Err(()), // Other addresses fail
} }
}; };
@ -402,7 +394,7 @@ mod tests {
// Mock successful EEPROM read for M7 mode // Mock successful EEPROM read for M7 mode
let mut read_fn = |addr: u32| { let mut read_fn = |addr: u32| {
match addr { match addr {
25 => Ok(GIMBAL_MODE_M7), // Gimbal mode stored at address 25 (EEPROM_DATA_LENGTH) 25 => Ok(GIMBAL_MODE_M7), // Gimbal mode stored at address 25 (EEPROM_DATA_LENGTH)
_ => Err(()), _ => Err(()),
} }
}; };

View File

@ -95,6 +95,9 @@ pub mod timers {
/// USB HID report interval (ms). /// USB HID report interval (ms).
pub const USB_UPDATE_INTERVAL_MS: u32 = 10; pub const USB_UPDATE_INTERVAL_MS: u32 = 10;
/// USB activity timeout (ms) - stop sending reports after this period of inactivity.
pub const USB_ACTIVITY_TIMEOUT_MS: u32 = 10_000; // 10 seconds
} }
// ==================== USB DEVICE CONFIGURATION ==================== // ==================== USB DEVICE CONFIGURATION ====================

View File

@ -8,8 +8,6 @@
/// Axis processing for gimbal and virtual axes (smoothing, expo, holds). /// Axis processing for gimbal and virtual axes (smoothing, expo, holds).
pub mod axis; pub mod axis;
/// Button-to-USB mapping tables and HAT constants.
pub mod mapping;
/// Row/column scanned button matrix driver with debouncing. /// Row/column scanned button matrix driver with debouncing.
pub mod button_matrix; pub mod button_matrix;
/// Button state machine (short/long press, timing, special actions). /// Button state machine (short/long press, timing, special actions).
@ -20,6 +18,8 @@ pub mod calibration;
pub mod expo; pub mod expo;
/// Hardware constants, pin definitions, timing, and helper macros. /// Hardware constants, pin definitions, timing, and helper macros.
pub mod hardware; pub mod hardware;
/// Button-to-USB mapping tables and HAT constants.
pub mod mapping;
/// WS2812 status LED driver and status model. /// WS2812 status LED driver and status model.
pub mod status; pub mod status;
/// EEPROM storage layout and read/write helpers. /// EEPROM storage layout and read/write helpers.
@ -32,9 +32,9 @@ pub mod usb_report;
/// Re-exports for convenient access in `main` and downstream consumers. /// Re-exports for convenient access in `main` and downstream consumers.
pub use axis::{AxisManager, GimbalAxis, VirtualAxis}; pub use axis::{AxisManager, GimbalAxis, VirtualAxis};
pub use calibration::CalibrationManager; pub use calibration::CalibrationManager;
pub use expo::{ExpoLUT, apply_expo_curve, constrain, generate_expo_lut}; pub use expo::{apply_expo_curve, constrain, generate_expo_lut, ExpoLUT};
pub use storage::{read_axis_calibration, read_gimbal_mode, write_calibration_data, StorageError}; pub use storage::{read_axis_calibration, read_gimbal_mode, write_calibration_data, StorageError};
pub use usb_report::{get_joystick_report, axis_12bit_to_i16}; pub use usb_report::{axis_12bit_to_i16, get_joystick_report};
/// Common ADC range constants used across modules. /// Common ADC range constants used across modules.
pub const ADC_MIN: u16 = 0; pub const ADC_MIN: u16 = 0;

View File

@ -233,6 +233,8 @@ fn main() -> ! {
let mut usb_activity: bool = false; let mut usb_activity: bool = false;
let mut usb_active: bool = false; let mut usb_active: bool = false;
let mut vt_enable: bool = false; let mut vt_enable: bool = false;
let mut idle_mode: bool = false;
let mut usb_activity_timeout_count: u32 = 0;
let mut axis_manager = AxisManager::new(); let mut axis_manager = AxisManager::new();
let mut button_manager = ButtonManager::new(); let mut button_manager = ButtonManager::new();
@ -285,6 +287,11 @@ fn main() -> ! {
// Handle USB device polling and maintain connection state // Handle USB device polling and maintain connection state
if usb_dev.poll(&mut [&mut usb_hid_joystick]) { if usb_dev.poll(&mut [&mut usb_hid_joystick]) {
if !usb_active {
usb_activity = true; // Force initial report
idle_mode = false;
usb_activity_timeout_count = 0;
}
usb_active = true; usb_active = true;
} }
@ -329,6 +336,7 @@ fn main() -> ! {
let system_state = SystemState { let system_state = SystemState {
usb_active, usb_active,
idle_mode,
calibration_active: calibration_manager.is_active(), calibration_active: calibration_manager.is_active(),
throttle_hold_enable: axis_manager.throttle_hold_enable, throttle_hold_enable: axis_manager.throttle_hold_enable,
vt_enable, vt_enable,
@ -424,16 +432,22 @@ fn main() -> ! {
// Process gimbal axes through calibration, expo curves, and scaling // Process gimbal axes through calibration, expo curves, and scaling
if axis_manager.process_axis_values(&smoother, &expo_lut) { if axis_manager.process_axis_values(&smoother, &expo_lut) {
usb_activity = true; usb_activity = true;
usb_activity_timeout_count = 0; // Reset timeout on real input activity
idle_mode = false;
} }
// Update virtual axes based on front button states // Update virtual axes based on front button states
if axis_manager.update_virtual_axes(button_manager.buttons(), vt_enable) { if axis_manager.update_virtual_axes(button_manager.buttons(), vt_enable) {
usb_activity = true; usb_activity = true;
usb_activity_timeout_count = 0; // Reset timeout on real input activity
idle_mode = false;
} }
// Process button logic (press types, timing, USB mapping) // Process button logic (press types, timing, USB mapping)
if button_manager.process_button_logic_with_timer(&timer) { if button_manager.process_button_logic_with_timer(&timer) {
usb_activity = true; usb_activity = true;
usb_activity_timeout_count = 0; // Reset timeout on real input activity
idle_mode = false;
} }
} }
@ -450,25 +464,38 @@ fn main() -> ! {
// - Virtual throttle mode handling // - Virtual throttle mode handling
// Only transmit USB reports when input activity is detected // Only transmit USB reports when input activity is detected
if usb_update_count_down.wait().is_ok() && usb_activity { let usb_tick = usb_update_count_down.wait().is_ok();
let virtual_ry_value = axis_manager.get_virtual_ry_value(&expo_lut_virtual);
let virtual_rz_value = axis_manager.get_virtual_rz_value(&expo_lut_virtual);
match usb_hid_joystick.device().write_report(&get_joystick_report( if usb_tick && usb_activity {
button_manager.buttons_mut(), // Check if we've exceeded the activity timeout
&mut axis_manager.axes, usb_activity_timeout_count += timers::USB_UPDATE_INTERVAL_MS;
virtual_ry_value, if usb_activity_timeout_count >= timers::USB_ACTIVITY_TIMEOUT_MS {
virtual_rz_value, usb_activity = false; // Stop sending reports after timeout
&vt_enable, usb_activity_timeout_count = 0;
)) { idle_mode = true;
Err(UsbHidError::WouldBlock) => {} } else {
Ok(_) => {} let virtual_ry_value = axis_manager.get_virtual_ry_value(&expo_lut_virtual);
Err(e) => { let virtual_rz_value = axis_manager.get_virtual_rz_value(&expo_lut_virtual);
status_led.update(StatusMode::Error);
core::panic!("Failed to write joystick report: {:?}", e) match usb_hid_joystick.device().write_report(&get_joystick_report(
} button_manager.buttons_mut(),
}; &mut axis_manager.axes,
usb_activity = false; virtual_ry_value,
virtual_rz_value,
&vt_enable,
)) {
Err(UsbHidError::WouldBlock) => {}
Ok(_) => {}
Err(e) => {
status_led.update(StatusMode::Error);
core::panic!("Failed to write joystick report: {:?}", e)
}
};
}
} else if usb_tick && usb_active {
idle_mode = true;
} else if usb_tick {
idle_mode = false;
} }
} }
} }

View File

@ -91,7 +91,6 @@ pub const USB_HAT_RIGHT: usize = 34;
pub const USB_HAT_DOWN: usize = 35; pub const USB_HAT_DOWN: usize = 35;
pub const USB_HAT_LEFT: usize = 36; pub const USB_HAT_LEFT: usize = 36;
use crate::buttons::Button; use crate::buttons::Button;
/// Configure USB button mappings for all buttons. /// Configure USB button mappings for all buttons.

View File

@ -6,9 +6,9 @@
use rp2040_hal::{ use rp2040_hal::{
gpio::AnyPin, gpio::AnyPin,
pio::{PIO, PIOExt, StateMachineIndex, UninitStateMachine}, pio::{PIOExt, StateMachineIndex, UninitStateMachine, PIO},
}; };
use smart_leds::{RGB8, SmartLedsWrite}; use smart_leds::{SmartLedsWrite, RGB8};
use ws2812_pio::Ws2812Direct; use ws2812_pio::Ws2812Direct;
/// Status LED modes with clear semantics. /// Status LED modes with clear semantics.
@ -20,36 +20,57 @@ pub enum StatusMode {
NormalFlash = 2, NormalFlash = 2,
Activity = 3, Activity = 3,
ActivityFlash = 4, ActivityFlash = 4,
Other = 5, Idle = 5,
OtherFlash = 6, Other = 6,
Warning = 7, OtherFlash = 7,
Error = 8, Warning = 8,
Bootloader = 9, Error = 9,
Bootloader = 10,
} }
/// Aggregate system state for LED status indication. /// Aggregate system state for LED status indication.
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct SystemState { pub struct SystemState {
pub usb_active: bool, pub usb_active: bool,
pub idle_mode: bool,
pub calibration_active: bool, pub calibration_active: bool,
pub throttle_hold_enable: bool, pub throttle_hold_enable: bool,
pub vt_enable: bool, pub vt_enable: bool,
} }
/// Color definitions for different status modes. /// Color definitions for different status modes.
const LED_COLORS: [RGB8; 10] = [ const LED_COLORS: [RGB8; 11] = [
RGB8 { r: 0, g: 0, b: 0 }, // Off RGB8 { r: 0, g: 0, b: 0 }, // Off
RGB8 { r: 10, g: 7, b: 0 }, // Normal (Green) RGB8 { r: 10, g: 7, b: 0 }, // Normal (Green)
RGB8 { r: 10, g: 7, b: 0 }, // NormalFlash (Green) RGB8 { r: 10, g: 7, b: 0 }, // NormalFlash (Green)
RGB8 { r: 10, g: 4, b: 10 }, // Activity (Blue) RGB8 { r: 10, g: 4, b: 10 }, // Activity (Blue)
RGB8 { r: 10, g: 4, b: 10 }, // ActivityFlash (Blue) RGB8 { r: 10, g: 4, b: 10 }, // ActivityFlash (Blue)
RGB8 { r: 5, g: 10, b: 0 }, // Other (Orange) RGB8 { r: 10, g: 7, b: 0 }, // Idle (Green flash)
RGB8 { r: 5, g: 10, b: 0 }, // OtherFlash (Orange) RGB8 { r: 5, g: 10, b: 0 }, // Other (Orange)
RGB8 { r: 2, g: 20, b: 0 }, // Warning (Red) RGB8 { r: 5, g: 10, b: 0 }, // OtherFlash (Orange)
RGB8 { r: 2, g: 20, b: 0 }, // Error (Red) RGB8 { r: 2, g: 20, b: 0 }, // Warning (Red)
RGB8 { r: 0, g: 10, b: 10 }, // Bootloader (Purple) RGB8 { r: 2, g: 20, b: 0 }, // Error (Red)
RGB8 { r: 0, g: 10, b: 10 }, // Bootloader (Purple)
]; ];
fn determine_mode(system_state: SystemState) -> StatusMode {
if system_state.calibration_active {
StatusMode::ActivityFlash
} else if system_state.idle_mode {
StatusMode::Idle
} else if !system_state.usb_active {
StatusMode::NormalFlash
} else if system_state.vt_enable {
StatusMode::Activity
} else if system_state.throttle_hold_enable {
StatusMode::Other
} else if system_state.usb_active {
StatusMode::Normal
} else {
StatusMode::Off
}
}
/// Status LED driver with self-contained state management. /// Status LED driver with self-contained state management.
pub struct StatusLed<P, SM, I> pub struct StatusLed<P, SM, I>
where where
@ -93,20 +114,7 @@ where
/// Update LED based on system state and current time (ms). /// Update LED based on system state and current time (ms).
pub fn update_from_system_state(&mut self, system_state: SystemState, current_time: u32) { pub fn update_from_system_state(&mut self, system_state: SystemState, current_time: u32) {
let desired_mode = if system_state.calibration_active { let desired_mode = determine_mode(system_state);
StatusMode::ActivityFlash
} else if !system_state.usb_active {
StatusMode::NormalFlash
} else if system_state.usb_active && system_state.vt_enable {
StatusMode::Activity
} else if system_state.usb_active && system_state.throttle_hold_enable {
StatusMode::Other
} else if system_state.usb_active {
StatusMode::Normal
} else {
StatusMode::Off
};
self.set_mode(desired_mode, current_time); self.set_mode(desired_mode, current_time);
} }
@ -133,6 +141,7 @@ where
// Flashing modes - toggle between on and off // Flashing modes - toggle between on and off
StatusMode::NormalFlash StatusMode::NormalFlash
| StatusMode::ActivityFlash | StatusMode::ActivityFlash
| StatusMode::Idle
| StatusMode::OtherFlash | StatusMode::OtherFlash
| StatusMode::Warning => { | StatusMode::Warning => {
if self.flash_state { if self.flash_state {
@ -143,7 +152,7 @@ where
self.write_color(LED_COLORS[StatusMode::Off as usize]); self.write_color(LED_COLORS[StatusMode::Off as usize]);
} }
self.flash_state = !self.flash_state; self.flash_state = !self.flash_state;
}, }
// Solid modes - just show the color // Solid modes - just show the color
_ => { _ => {
self.write_color(LED_COLORS[self.current_mode as usize]); self.write_color(LED_COLORS[self.current_mode as usize]);
@ -167,11 +176,10 @@ where
match self.current_mode { match self.current_mode {
StatusMode::NormalFlash StatusMode::NormalFlash
| StatusMode::ActivityFlash | StatusMode::ActivityFlash
| StatusMode::Idle
| StatusMode::OtherFlash | StatusMode::OtherFlash
| StatusMode::Warning => { | StatusMode::Warning => current_time.saturating_sub(last_time) >= 500,
current_time.saturating_sub(last_time) >= 500 _ => false, // Non-flashing modes don't need periodic updates
},
_ => false // Non-flashing modes don't need periodic updates
} }
} }
} }
@ -195,3 +203,34 @@ where
self.set_mode(mode, 0); self.set_mode(mode, 0);
} }
} }
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
#[test]
fn idle_mode_selected_when_usb_idle() {
let state = SystemState {
usb_active: true,
idle_mode: true,
calibration_active: false,
throttle_hold_enable: false,
vt_enable: false,
};
assert_eq!(determine_mode(state), StatusMode::Idle);
}
#[test]
fn calibration_has_priority_over_idle() {
let state = SystemState {
usb_active: true,
idle_mode: true,
calibration_active: true,
throttle_hold_enable: false,
vt_enable: false,
};
assert_eq!(determine_mode(state), StatusMode::ActivityFlash);
}
}

View File

@ -28,7 +28,7 @@ pub enum StorageError {
/// Returns (min, max, center) values as u16. /// Returns (min, max, center) values as u16.
pub fn read_axis_calibration( pub fn read_axis_calibration(
read_byte_fn: &mut dyn FnMut(u32) -> Result<u8, ()>, read_byte_fn: &mut dyn FnMut(u32) -> Result<u8, ()>,
axis_index: usize axis_index: usize,
) -> Result<(u16, u16, u16), StorageError> { ) -> Result<(u16, u16, u16), StorageError> {
// Original format uses: base = axis_index * 6, addresses = base+1,2,3,4,5,6 // Original format uses: base = axis_index * 6, addresses = base+1,2,3,4,5,6
let base = axis_index as u32 * 6; let base = axis_index as u32 * 6;
@ -42,10 +42,9 @@ pub fn read_axis_calibration(
/// Read gimbal mode from EEPROM. /// Read gimbal mode from EEPROM.
pub fn read_gimbal_mode( pub fn read_gimbal_mode(
read_byte_fn: &mut dyn FnMut(u32) -> Result<u8, ()> read_byte_fn: &mut dyn FnMut(u32) -> Result<u8, ()>,
) -> Result<u8, StorageError> { ) -> Result<u8, StorageError> {
read_byte_fn(GIMBAL_MODE_OFFSET) read_byte_fn(GIMBAL_MODE_OFFSET).map_err(|_| StorageError::ReadError)
.map_err(|_| StorageError::ReadError)
} }
/// Write all calibration data and gimbal mode to EEPROM. /// Write all calibration data and gimbal mode to EEPROM.
@ -53,7 +52,7 @@ pub fn read_gimbal_mode(
pub fn write_calibration_data( pub fn write_calibration_data(
write_page_fn: &mut dyn FnMut(u32, &[u8]) -> Result<(), ()>, write_page_fn: &mut dyn FnMut(u32, &[u8]) -> Result<(), ()>,
axis_data: &[(u16, u16, u16)], axis_data: &[(u16, u16, u16)],
gimbal_mode: u8 gimbal_mode: u8,
) -> Result<(), StorageError> { ) -> Result<(), StorageError> {
let mut eeprom_data: [u8; EEPROM_DATA_LENGTH] = [0; EEPROM_DATA_LENGTH]; let mut eeprom_data: [u8; EEPROM_DATA_LENGTH] = [0; EEPROM_DATA_LENGTH];
@ -73,28 +72,23 @@ pub fn write_calibration_data(
eeprom_data[EEPROM_DATA_LENGTH - 1] = gimbal_mode; eeprom_data[EEPROM_DATA_LENGTH - 1] = gimbal_mode;
// Write entire page to EEPROM // Write entire page to EEPROM
write_page_fn(0x01, &eeprom_data) write_page_fn(0x01, &eeprom_data).map_err(|_| StorageError::WriteError)
.map_err(|_| StorageError::WriteError)
} }
// ==================== HELPER FUNCTIONS ==================== // ==================== HELPER FUNCTIONS ====================
/// Read a u16 value from EEPROM in littleendian (low then high byte) format. /// Read a u16 value from EEPROM in littleendian (low then high byte) format.
fn read_u16_with_closure( fn read_u16_with_closure(
read_byte_fn: &mut dyn FnMut(u32) -> Result<u8, ()>, read_byte_fn: &mut dyn FnMut(u32) -> Result<u8, ()>,
low_addr: u32, low_addr: u32,
high_addr: u32 high_addr: u32,
) -> Result<u16, StorageError> { ) -> Result<u16, StorageError> {
let low_byte = read_byte_fn(low_addr) let low_byte = read_byte_fn(low_addr).map_err(|_| StorageError::ReadError)? as u16;
.map_err(|_| StorageError::ReadError)? as u16; let high_byte = read_byte_fn(high_addr).map_err(|_| StorageError::ReadError)? as u16;
let high_byte = read_byte_fn(high_addr)
.map_err(|_| StorageError::ReadError)? as u16;
Ok(low_byte | (high_byte << 8)) Ok(low_byte | (high_byte << 8))
} }
// ==================== TESTS ==================== // ==================== TESTS ====================
#[cfg(all(test, feature = "std"))] #[cfg(all(test, feature = "std"))]
@ -120,14 +114,14 @@ mod tests {
let mut buffer = [0u8; 4]; let mut buffer = [0u8; 4];
let value = 0x1234u16; let value = 0x1234u16;
buffer[0] = value as u8; // 0x34 (low byte) buffer[0] = value as u8; // 0x34 (low byte)
buffer[1] = (value >> 8) as u8; // 0x12 (high byte) buffer[1] = (value >> 8) as u8; // 0x12 (high byte)
assert_eq!(buffer[0], 0x34); assert_eq!(buffer[0], 0x34);
assert_eq!(buffer[1], 0x12); assert_eq!(buffer[1], 0x12);
let value = 0xABCDu16; let value = 0xABCDu16;
buffer[2] = value as u8; // 0xCD (low byte) buffer[2] = value as u8; // 0xCD (low byte)
buffer[3] = (value >> 8) as u8; // 0xAB (high byte) buffer[3] = (value >> 8) as u8; // 0xAB (high byte)
assert_eq!(buffer[2], 0xCD); assert_eq!(buffer[2], 0xCD);
assert_eq!(buffer[3], 0xAB); assert_eq!(buffer[3], 0xAB);
} }
@ -147,7 +141,12 @@ mod tests {
#[test] #[test]
fn test_calibration_data_format() { fn test_calibration_data_format() {
// Test that calibration data format matches original (addresses base+1,2,3,4,5,6) // Test that calibration data format matches original (addresses base+1,2,3,4,5,6)
let test_data = [(100, 3900, 2000), (150, 3850, 2048), (200, 3800, 1900), (250, 3750, 2100)]; let test_data = [
(100, 3900, 2000),
(150, 3850, 2048),
(200, 3800, 1900),
(250, 3750, 2100),
];
let mut buffer = [0u8; EEPROM_DATA_LENGTH]; let mut buffer = [0u8; EEPROM_DATA_LENGTH];
// Pack data using original format // Pack data using original format
@ -162,8 +161,8 @@ mod tests {
} }
// Verify first axis data (addresses 1-6) // Verify first axis data (addresses 1-6)
assert_eq!(buffer[1], 100 as u8); // min low assert_eq!(buffer[1], 100 as u8); // min low
assert_eq!(buffer[2], 0); // min high assert_eq!(buffer[2], 0); // min high
assert_eq!(buffer[3], (3900 & 0xFF) as u8); // max low assert_eq!(buffer[3], (3900 & 0xFF) as u8); // max low
assert_eq!(buffer[4], (3900 >> 8) as u8); // max high assert_eq!(buffer[4], (3900 >> 8) as u8); // max high
assert_eq!(buffer[5], (2000 & 0xFF) as u8); // center low assert_eq!(buffer[5], (2000 & 0xFF) as u8); // center low
@ -194,12 +193,12 @@ mod tests {
// Mock EEPROM data: axis 0 with min=100, max=4000, center=2050 // Mock EEPROM data: axis 0 with min=100, max=4000, center=2050
// Original format: addresses 1,2,3,4,5,6 for axis 0 // Original format: addresses 1,2,3,4,5,6 for axis 0
let mut mock_data = [0u8; 7]; // Need indices 0-6 let mut mock_data = [0u8; 7]; // Need indices 0-6
mock_data[1] = 100; // min low byte mock_data[1] = 100; // min low byte
mock_data[2] = 0; // min high byte mock_data[2] = 0; // min high byte
mock_data[3] = 160; // max low byte (4000 = 0x0FA0) mock_data[3] = 160; // max low byte (4000 = 0x0FA0)
mock_data[4] = 15; // max high byte mock_data[4] = 15; // max high byte
mock_data[5] = 2; // center low byte (2050 = 0x0802) mock_data[5] = 2; // center low byte (2050 = 0x0802)
mock_data[6] = 8; // center high byte mock_data[6] = 8; // center high byte
let mut read_fn = |addr: u32| -> Result<u8, ()> { let mut read_fn = |addr: u32| -> Result<u8, ()> {
if addr < mock_data.len() as u32 { if addr < mock_data.len() as u32 {

View File

@ -182,12 +182,10 @@ pub struct JoystickConfig<'a> {
impl Default for JoystickConfig<'_> { impl Default for JoystickConfig<'_> {
fn default() -> Self { fn default() -> Self {
Self::new( Self::new(
unwrap!( unwrap!(unwrap!(InterfaceBuilder::new(JOYSTICK_DESCRIPTOR))
unwrap!(InterfaceBuilder::new(JOYSTICK_DESCRIPTOR)) .boot_device(InterfaceProtocol::None)
.boot_device(InterfaceProtocol::None) .description("Joystick")
.description("Joystick") .in_endpoint(10.millis()))
.in_endpoint(10.millis())
)
.without_out_endpoint() .without_out_endpoint()
.build(), .build(),
) )

View File

@ -4,10 +4,13 @@
//! that matches the HID descriptor defined in `usb_joystick_device.rs`. //! that matches the HID descriptor defined in `usb_joystick_device.rs`.
//! Also contains support for virtual throttle mode and HAT directions. //! Also contains support for virtual throttle mode and HAT directions.
use crate::axis::{GimbalAxis, remap, GIMBAL_AXIS_LEFT_X, GIMBAL_AXIS_LEFT_Y, GIMBAL_AXIS_RIGHT_X, GIMBAL_AXIS_RIGHT_Y}; 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::buttons::{Button, TOTAL_BUTTONS};
use crate::mapping::{USB_HAT_UP, USB_HAT_LEFT}; use crate::hardware::{ADC_MAX, ADC_MIN, AXIS_CENTER};
use crate::hardware::{ADC_MIN, ADC_MAX, AXIS_CENTER}; use crate::mapping::{USB_HAT_LEFT, USB_HAT_UP};
use crate::usb_joystick_device::JoystickReport; use crate::usb_joystick_device::JoystickReport;
// ==================== USB REPORT GENERATION ==================== // ==================== USB REPORT GENERATION ====================
@ -130,7 +133,6 @@ pub fn get_joystick_report(
} }
} }
// ==================== TESTS ==================== // ==================== TESTS ====================
#[cfg(all(test, feature = "std"))] #[cfg(all(test, feature = "std"))]
@ -176,7 +178,7 @@ mod tests {
// Test reverse remapping behavior: when out_min > out_max, constrain will // Test reverse remapping behavior: when out_min > out_max, constrain will
// clamp results to the "valid" range based on the constraint logic // 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(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 assert_eq!(remap(1000, 0, 1000, 2000, 0), 2000); // Calculated 0, constrained to 2000
} }
@ -195,7 +197,8 @@ mod tests {
let virtual_rz = 2000; let virtual_rz = 2000;
let vt_enable = false; let vt_enable = false;
let report = get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable); let report =
get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable);
// Verify axis conversions // Verify axis conversions
assert_eq!(report.x, axis_12bit_to_i16(2048)); assert_eq!(report.x, axis_12bit_to_i16(2048));
@ -221,7 +224,8 @@ mod tests {
let virtual_rz = 0; let virtual_rz = 0;
let vt_enable = true; let vt_enable = true;
let report = get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable); let report =
get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable);
// In VT mode, z should be 0 // In VT mode, z should be 0
assert_eq!(report.z, 0); assert_eq!(report.z, 0);
@ -243,7 +247,8 @@ mod tests {
let virtual_rz = 0; let virtual_rz = 0;
let vt_enable = true; let vt_enable = true;
let report = get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable); let report =
get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable);
// In VT mode, z should be 0 // In VT mode, z should be 0
assert_eq!(report.z, 0); assert_eq!(report.z, 0);
@ -269,7 +274,8 @@ mod tests {
let virtual_rz = 0; let virtual_rz = 0;
let vt_enable = false; let vt_enable = false;
let report = get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable); let report =
get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable);
// Check button bits are set correctly // Check button bits are set correctly
assert_eq!(report.buttons & (1 << 0), 1 << 0); // Button 1 assert_eq!(report.buttons & (1 << 0), 1 << 0); // Button 1
@ -290,7 +296,8 @@ mod tests {
let virtual_rz = 0; let virtual_rz = 0;
let vt_enable = false; let vt_enable = false;
let report = get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable); let report =
get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable);
// Check HAT direction is set correctly // Check HAT direction is set correctly
let expected_hat = (USB_HAT_UP as u8 - USB_HAT_UP as u8) * 2; // Should be 0 (up) let expected_hat = (USB_HAT_UP as u8 - USB_HAT_UP as u8) * 2; // Should be 0 (up)
@ -311,7 +318,8 @@ mod tests {
let virtual_rz = 0; let virtual_rz = 0;
let vt_enable = false; let vt_enable = false;
let report = get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable); let report =
get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable);
// Check long press button is handled // Check long press button is handled
assert_eq!(report.buttons & (1 << 2), 1 << 2); // Button 3 assert_eq!(report.buttons & (1 << 2), 1 << 2); // Button 3
@ -350,7 +358,8 @@ mod tests {
let virtual_rz = 0; let virtual_rz = 0;
let vt_enable = false; let vt_enable = false;
let report = get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable); let report =
get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable);
// Check HAT left direction // Check HAT left direction
let expected_hat = (USB_HAT_LEFT as u8 - USB_HAT_UP as u8) * 2; let expected_hat = (USB_HAT_LEFT as u8 - USB_HAT_UP as u8) * 2;
@ -374,7 +383,8 @@ mod tests {
let virtual_rz = 0; let virtual_rz = 0;
let vt_enable = false; let vt_enable = false;
let report = get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable); let report =
get_joystick_report(&mut buttons, &mut axes, virtual_ry, virtual_rz, &vt_enable);
// Check both regular buttons and HAT // Check both regular buttons and HAT
assert_eq!(report.buttons & (1 << 0), 1 << 0); // Button 1 assert_eq!(report.buttons & (1 << 0), 1 << 0); // Button 1