diff --git a/AGENTS.md b/AGENTS.md index fc94147..6251b5d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,13 +1,23 @@ # Assistant Configuration -This file contains configuration and commands for the Claude assistant working on the CMtec CMDR Joystick 25. - ## Global Rules +- Rust emdedded - Always describe what you thinking and your plan befor starting to change files. - Make sure code have max 5 indentation levels -- Use classes, arrays, structs, etc for clean organization +- Use arrays, structs, etc for clean organization - Make sure the codebase is manageable and easily readable - Always check code (compile/check) - Always fix compile warnings - Do not try to deploy project to hardware +- Use "just" for check, test, flash etc +- Use file structure described in this file + +## Firmware File Structure Blueprint (RP2040 / RP2350) + +- `src/hardware.rs` — **Required.** Centralize pin assignments, clock constants, peripheral aliases, timer intervals, and other board-specific configuration. Nothing outside this module hardcodes MCU pin numbers or magic frequencies. +- `src/board.rs` — **Required.** Board bring-up; owns peripheral wiring (clocks, GPIO, comms, sensors, USB), exposes `Board`/`BoardParts` (or equivalent). Keep granular comments explaining each hardware init block. +- `src/main.rs` — **Required.** Thin firmware entry; fetch initialized parts, load persisted configuration, configure timers, and run the primary control loop (USB/event poll, scheduling, report generation). Runtime orchestration only. +- Feature modules stay single-purpose (e.g., `inputs.rs`, `sensors.rs`, `storage.rs`, `status.rs`, `usb_report.rs`, `usb_device.rs`). Each should include unit tests with short intent comments capturing edge cases and data packing, runnable in host mode. +- Utility crates (`mapping.rs`, `calibration.rs`, etc.) should avoid cross-module side effects—prefer explicit data passed through `BoardParts`/state structs. +- Comments document why a block exists or which hardware behaviour it mirrors; avoid repeating obvious code but provide enough context for re-use across RP-series projects. diff --git a/rp2040/src/axis.rs b/rp2040/src/axis.rs index 3381e2e..ea6aac6 100644 --- a/rp2040/src/axis.rs +++ b/rp2040/src/axis.rs @@ -54,6 +54,7 @@ pub struct GimbalAxis { impl Default for GimbalAxis { fn default() -> Self { + // Apply new calibration limits supplied by the calibration manager. GimbalAxis { value: AXIS_CENTER, value_before_hold: AXIS_CENTER, @@ -143,6 +144,7 @@ pub struct VirtualAxis { impl Default for VirtualAxis { fn default() -> Self { + // Create a virtual axis starting at center with the supplied step size. VirtualAxis { value: AXIS_CENTER, step: 5, @@ -206,6 +208,7 @@ pub struct AxisManager { impl Default for AxisManager { fn default() -> Self { + // Delegate to `new` so the default stays aligned with explicit constructor logic. Self::new() } } @@ -462,6 +465,7 @@ mod tests { #[test] fn test_gimbal_axis_default() { + // Factory defaults should leave every axis field pointing at the center span. let axis = GimbalAxis::default(); assert_eq!(axis.value, AXIS_CENTER); assert_eq!(axis.min, ADC_MIN); @@ -473,6 +477,7 @@ mod tests { #[test] fn test_gimbal_axis_new() { + // `new` should be a thin wrapper over `Default` without changing members. let axis = GimbalAxis::new(); assert_eq!(axis.value, AXIS_CENTER); assert_eq!(axis.min, ADC_MIN); @@ -483,6 +488,7 @@ mod tests { #[test] fn test_gimbal_axis_with_calibration() { + // Axis constructed with explicit calibration should adopt all provided bounds. let axis = GimbalAxis::with_calibration(100, 3900, 2000); assert_eq!(axis.min, 100); assert_eq!(axis.max, 3900); @@ -491,6 +497,7 @@ mod tests { #[test] fn test_gimbal_axis_activity_detection() { + // Activity flag should only flip when the axis value actually changes. let mut axis = GimbalAxis::new(); // Initially no activity (same as previous) @@ -506,6 +513,7 @@ mod tests { #[test] fn test_gimbal_axis_throttle_hold_processing() { + // Throttle hold remapping should cover remap, center latch, and pending hold cases. let mut axis = GimbalAxis::new(); axis.set_hold(1500); // Set hold value below center @@ -541,6 +549,7 @@ mod tests { #[test] fn test_virtual_axis_default() { + // Virtual axis should boot at center with the configured step size. let virtual_axis = VirtualAxis::default(); assert_eq!(virtual_axis.value, AXIS_CENTER); assert_eq!(virtual_axis.step, 5); @@ -548,6 +557,7 @@ mod tests { #[test] fn test_virtual_axis_movement_up() { + // An upward press should increment the virtual axis and flag activity. let mut virtual_axis = VirtualAxis::new(10); // Test upward movement @@ -558,6 +568,7 @@ mod tests { #[test] fn test_virtual_axis_movement_down() { + // A downward press should decrement the virtual axis and flag activity. let mut virtual_axis = VirtualAxis::new(10); // Test downward movement @@ -568,6 +579,7 @@ mod tests { #[test] fn test_virtual_axis_return_to_center() { + // Without inputs the virtual axis should ease back toward the center value. let mut virtual_axis = VirtualAxis::new(10); virtual_axis.value = AXIS_CENTER + 20; @@ -579,6 +591,7 @@ mod tests { #[test] fn test_virtual_axis_direction_compensation() { + // Reversing direction should recenter before stepping to avoid large jumps. let mut virtual_axis = VirtualAxis::new(10); virtual_axis.value = AXIS_CENTER - 100; @@ -591,6 +604,7 @@ mod tests { #[test] fn test_axis_manager_creation() { + // Manager construction should initialize all axes and state to defaults. let manager = AxisManager::new(); assert_eq!(manager.axes.len(), NBR_OF_GIMBAL_AXIS); assert_eq!(manager.gimbal_mode, GIMBAL_MODE_M10); @@ -600,6 +614,7 @@ mod tests { #[test] fn test_gimbal_compensation_m10() { + // M10 gimbal compensation must invert the correct pair of axes. let manager = AxisManager::new(); // Default is M10 let mut raw_values = [1000, 1500, 2000, 2500]; @@ -614,6 +629,7 @@ mod tests { #[test] fn test_gimbal_compensation_m7() { + // M7 gimbal compensation must invert the complementary axes. let mut manager = AxisManager::new(); manager.set_gimbal_mode(GIMBAL_MODE_M7); let mut raw_values = [1000, 1500, 2000, 2500]; @@ -629,6 +645,7 @@ mod tests { #[test] fn test_axis_activity_detection() { + // Manager activity flag should reflect when any axis value changes. let mut manager = AxisManager::new(); // No activity initially @@ -644,6 +661,7 @@ mod tests { #[test] fn test_calculate_axis_value_boundaries() { + // Axis conversion should clamp inputs below min or above max calibration values. let expo_lut = ExpoLUT::new(0.0); // No expo for testing // Test min boundary @@ -657,6 +675,7 @@ mod tests { #[test] fn test_calculate_axis_value_deadzone() { + // Inputs inside the center deadzone should resolve to the exact center value. let expo_lut = ExpoLUT::new(0.0); // No expo for testing // Test center deadzone @@ -666,6 +685,7 @@ mod tests { #[test] fn test_calculate_axis_value_degenerate_calibration() { + // Degenerate calibration inputs should return the provided center without panicking. let expo_lut = ExpoLUT::new(0.0); // When calibration collapses to a single point (min=max=center), diff --git a/rp2040/src/board.rs b/rp2040/src/board.rs index 81af522..7af0e88 100644 --- a/rp2040/src/board.rs +++ b/rp2040/src/board.rs @@ -29,6 +29,7 @@ pub type JoystickStatusLed = StatusLed; type BoardEeprom = Eeprom24x; +/// Strongly-typed collection of ADC-capable pins for each physical gimbal axis. pub struct AxisAnalogPins { pub left_x: AdcPin>, pub left_y: AdcPin>, @@ -38,6 +39,7 @@ pub struct AxisAnalogPins { impl AxisAnalogPins { fn new(inputs: hardware::AxisInputs) -> Self { + // Wrap the raw GPIO inputs into ADC-capable pins for each physical axis. let left_x = AdcPin::new(inputs.left_x).unwrap(); let left_y = AdcPin::new(inputs.left_y).unwrap(); let right_x = AdcPin::new(inputs.right_x).unwrap(); @@ -51,6 +53,7 @@ impl AxisAnalogPins { } } +/// Aggregates the runtime peripherals used by the joystick firmware. pub struct Board { button_matrix: JoystickMatrix, status_led: JoystickStatusLed, @@ -64,6 +67,7 @@ pub struct Board { usb_bus: &'static UsbBusAllocator, } +/// Board components handed off to the application after initialization. pub struct BoardParts { pub button_matrix: JoystickMatrix, pub status_led: JoystickStatusLed, @@ -79,10 +83,12 @@ pub struct BoardParts { impl Board { pub fn new() -> Self { + // Acquire RP2040 peripheral handles before configuration begins. let mut pac = pac::Peripherals::take().unwrap(); let core = pac::CorePeripherals::take().unwrap(); let mut watchdog = Watchdog::new(pac.WATCHDOG); + // Bring up the primary system and USB clocks using the external crystal. let clocks = init_clocks_and_plls( hardware::XTAL_FREQ_HZ, pac.XOSC, @@ -96,6 +102,7 @@ impl Board { .unwrap(); let sio = Sio::new(pac.SIO); + // Split GPIO banks and translate them into the strongly typed board pins. let raw_pins = gpio::Pins::new( pac.IO_BANK0, pac.PADS_BANK0, @@ -105,6 +112,7 @@ impl Board { let pins = BoardPins::new(raw_pins); let matrix_pins = MatrixPins::new(pins.matrix_rows, pins.matrix_cols); + // Create the button matrix scanner with firmware debounce parameters. let mut button_matrix = ButtonMatrix::new( matrix_pins, hardware::MATRIX_DEBOUNCE_SCANS, @@ -113,6 +121,7 @@ impl Board { button_matrix.init_pins(); let (mut pio, sm0, _, _, _) = pac.PIO0.split(&mut pac.RESETS); + // Configure the WS2812 status LED using a dedicated PIO state machine. let status_led = StatusLed::new( pins.status_led, &mut pio, @@ -120,9 +129,11 @@ impl Board { clocks.peripheral_clock.freq(), ); + // Set up timers for scheduling and a blocking delay helper. let timer = Timer::new(pac.TIMER, &mut pac.RESETS, &clocks); let delay = Delay::new(core.SYST, clocks.system_clock.freq().to_Hz()); + // Build the I²C bus and EEPROM driver used for calibration persistence. let i2c = I2C::i2c1( pac.I2C1, pins.i2c_sda, @@ -133,9 +144,11 @@ impl Board { ); let eeprom = Eeprom24x::new_24x32(i2c, hardware::i2c::EEPROM_ADDRESS); + // Bring up the ADC block and wrap each axis input pin. let adc = Adc::new(pac.ADC, &mut pac.RESETS); let axis_pins = AxisAnalogPins::new(pins.axis_inputs); + // Prepare a global USB bus allocator for the joystick HID device. let usb_bus = usb_allocator( pac.USBCTRL_REGS, pac.USBCTRL_DPRAM, @@ -179,8 +192,11 @@ fn usb_allocator( usb_clock: rp2040_hal::clocks::UsbClock, resets: &mut pac::RESETS, ) -> &'static UsbBusAllocator { + // Lazily create the shared USB bus allocator so HID endpoints can borrow it. static USB_BUS: StaticCell> = StaticCell::new(); + // Wire up the USB bus allocator, HID class, and joystick endpoint once and reuse it. + interrupt::free(|_| { USB_BUS.init_with(|| { UsbBusAllocator::new(rp2040_hal::usb::UsbBus::new( diff --git a/rp2040/src/button_matrix.rs b/rp2040/src/button_matrix.rs index 5dd6cf8..4e395ae 100644 --- a/rp2040/src/button_matrix.rs +++ b/rp2040/src/button_matrix.rs @@ -35,20 +35,24 @@ impl MatrixPins { impl MatrixPinAccess for MatrixPins { fn init_columns(&mut self) { + // Default all columns high so rows can be strobed one at a time. for column in self.cols.iter_mut() { let _ = column.set_high(); } } fn set_column_low(&mut self, column: usize) { + // Pull the active column low before scanning its rows. let _ = self.cols[column].set_low(); } fn set_column_high(&mut self, column: usize) { + // Release the column after scanning so other columns remain idle. let _ = self.cols[column].set_high(); } fn read_row(&mut self, row: usize) -> bool { + // Treat any low level as a pressed switch, defaulting to false on IO errors. self.rows[row].is_low().unwrap_or(false) } } @@ -112,6 +116,7 @@ where } fn process_column(&mut self, column: usize) { + // Drive a single column scan to update button press history. for row in 0..ROWS { let index = column + (row * COLS); let current_state = self.pins.read_row(row); @@ -138,6 +143,7 @@ where } fn should_register_press(&mut self, index: usize) -> bool { + // Decide if a press should register given debounce timing. let elapsed = self.scan_counter.wrapping_sub(self.last_press_scan[index]); let can_register = self.last_press_scan[index] == 0 || elapsed >= self.min_press_gap_scans; @@ -178,6 +184,7 @@ mod tests { impl MockPins { fn new(row_state: Rc>, column_state: Rc>) -> Self { + // Build a button matrix scanner with default state tracking arrays. Self { row_state, column_state, @@ -187,18 +194,22 @@ mod tests { impl MatrixPinAccess<1, 1> for MockPins { fn init_columns(&mut self) { + // Simulate the hardware by driving the single column high by default. self.column_state.set(true); } fn set_column_low(&mut self, _column: usize) { + // Drop the mock column low to emulate scanning behaviour. self.column_state.set(false); } fn set_column_high(&mut self, _column: usize) { + // Release the mock column back to the idle high state. self.column_state.set(true); } fn read_row(&mut self, _row: usize) -> bool { + // Return the mocked row state so tests can control pressed/unpressed. self.row_state.get() } } @@ -217,6 +228,7 @@ mod tests { #[test] fn debounce_requires_consecutive_scans() { + // Debounce logic should require two consecutive pressed scans before registering. let (mut matrix, row, _column) = fixture(); matrix.set_scan_counter(1); diff --git a/rp2040/src/buttons.rs b/rp2040/src/buttons.rs index 03bfc42..cbaddee 100644 --- a/rp2040/src/buttons.rs +++ b/rp2040/src/buttons.rs @@ -67,6 +67,7 @@ pub struct ButtonManager { impl Default for ButtonManager { fn default() -> Self { + // Build a button manager with default-initialized buttons and state flags. Self::new() } } @@ -121,6 +122,7 @@ impl ButtonManager { } 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) @@ -236,6 +238,7 @@ impl ButtonManager { /// 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 { + // 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) @@ -265,6 +268,7 @@ impl ButtonManager { /// - Short press: on release (and if long not activated), activate `usb_button` /// - USB press lifetime: auto‑release 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 @@ -334,12 +338,14 @@ mod tests { #[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); @@ -349,6 +355,7 @@ mod tests { #[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 @@ -362,6 +369,7 @@ mod tests { #[test] fn test_calibration_combination() { + // Calibration combo should generate the start-calibration action. let mut manager = ButtonManager::new(); // Test calibration combination @@ -375,6 +383,7 @@ mod tests { #[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; @@ -385,6 +394,7 @@ mod tests { #[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; @@ -396,6 +406,7 @@ mod tests { #[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; @@ -406,6 +417,7 @@ mod tests { #[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 @@ -424,6 +436,7 @@ mod tests { #[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 @@ -442,6 +455,7 @@ mod tests { #[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 @@ -458,6 +472,7 @@ mod tests { #[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 @@ -480,6 +495,7 @@ mod tests { #[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) @@ -493,6 +509,7 @@ mod tests { #[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; @@ -513,6 +530,7 @@ mod tests { #[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; @@ -533,6 +551,7 @@ mod tests { #[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; @@ -564,6 +583,7 @@ mod tests { #[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 diff --git a/rp2040/src/calibration.rs b/rp2040/src/calibration.rs index d39ff54..583b333 100644 --- a/rp2040/src/calibration.rs +++ b/rp2040/src/calibration.rs @@ -176,6 +176,7 @@ impl CalibrationManager { /// Reset each axis calibration to its current smoothed center. fn reset_axis_calibration( + // Recenter all axis calibration values using current smoother readings. &self, axes: &mut [GimbalAxis; NBR_OF_GIMBAL_AXIS], smoothers: &[DynamicSmootherEcoI32; NBR_OF_GIMBAL_AXIS], @@ -191,6 +192,7 @@ impl CalibrationManager { impl Default for CalibrationManager { fn default() -> Self { + // Construct a calibration manager using the Default implementation. Self::new() } } @@ -205,6 +207,7 @@ mod tests { #[test] fn test_calibration_manager_creation() { + // Report whether calibration is currently active. let manager = CalibrationManager::new(); assert!(!manager.is_active()); assert_eq!(manager.get_gimbal_mode(), GIMBAL_MODE_M10); @@ -212,6 +215,7 @@ mod tests { #[test] fn test_calibration_state_management() { + // Start and stop transitions should flip the active flag accordingly. let mut manager = CalibrationManager::new(); // Initially inactive @@ -228,6 +232,7 @@ mod tests { #[test] fn test_gimbal_mode_management() { + // Manual mode setter should swap between M10 and M7 without side effects. let mut manager = CalibrationManager::new(); // Default mode @@ -244,6 +249,7 @@ mod tests { #[test] fn test_dynamic_calibration_inactive() { + // Inactive calibration should ignore updates and leave min/max untouched. let manager = CalibrationManager::new(); let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS]; @@ -271,6 +277,7 @@ mod tests { #[test] fn test_dynamic_calibration_active() { + // Active calibration should track new lows/highs as smoothed values change. let mut manager = CalibrationManager::new(); manager.start_calibration(); @@ -319,6 +326,7 @@ mod tests { #[test] fn test_mode_selection_inactive() { + // Mode change commands should fail when calibration mode is not active. let mut manager = CalibrationManager::new(); let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS]; @@ -341,6 +349,7 @@ mod tests { #[test] fn test_load_axis_calibration_success() { + // Successful EEPROM reads should populate the axis calibration tuple. let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS]; // Mock EEPROM data simulating successful calibration read @@ -373,6 +382,7 @@ mod tests { #[test] fn test_load_axis_calibration_failure() { + // Failed EEPROM reads should leave default calibration values intact. let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS]; let original_axes = axes; @@ -391,6 +401,7 @@ mod tests { #[test] fn test_load_gimbal_mode_success() { + // Reading the stored gimbal mode should return the persisted value. // Mock successful EEPROM read for M7 mode let mut read_fn = |addr: u32| { match addr { @@ -405,6 +416,7 @@ mod tests { #[test] fn test_load_gimbal_mode_failure() { + // Read failures should fall back to the default M10 mode. // Mock EEPROM read failure let mut read_fn = |_addr: u32| Err(()); @@ -414,6 +426,7 @@ mod tests { #[test] fn test_update_calibration_inactive() { + // When calibration is inactive, mode/set/save helpers should be no-ops. let mut manager = CalibrationManager::new(); let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS]; @@ -442,6 +455,7 @@ mod tests { #[test] fn test_process_mode_selection_m10_command() { + // Active M10 command should set mode and recenter axes using smoother values. let mut manager = CalibrationManager::new(); manager.start_calibration(); let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS]; @@ -469,6 +483,7 @@ mod tests { #[test] fn test_process_mode_selection_m7_command() { + // Active M7 command should likewise set mode and recenter axes. let mut manager = CalibrationManager::new(); manager.start_calibration(); let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS]; @@ -496,6 +511,7 @@ mod tests { #[test] fn test_save_calibration_command() { + // Successful saves should write EEPROM data and exit calibration mode. let mut manager = CalibrationManager::new(); manager.start_calibration(); let axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS]; @@ -515,6 +531,7 @@ mod tests { #[test] fn test_save_calibration_failure_keeps_active() { + // Write failures should keep calibration active for retrials. let mut manager = CalibrationManager::new(); manager.start_calibration(); let axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS]; @@ -529,6 +546,7 @@ mod tests { #[test] fn test_save_calibration_inactive() { + // Save attempts while inactive should no-op without touching storage. let mut manager = CalibrationManager::new(); // Note: not starting calibration let axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS]; diff --git a/rp2040/src/expo.rs b/rp2040/src/expo.rs index 41ee73e..a035422 100644 --- a/rp2040/src/expo.rs +++ b/rp2040/src/expo.rs @@ -79,6 +79,7 @@ mod tests { #[test] fn test_generate_expo_lut_boundaries() { + // Ensure expo LUT boundary entries clamp to ADC range. let lut = generate_expo_lut(0.5); assert_eq!(lut[0], ADC_MIN); assert_eq!(lut[ADC_MAX as usize], ADC_MAX); @@ -86,6 +87,7 @@ mod tests { #[test] fn test_generate_expo_lut_center_point() { + // Midpoint of the expo LUT stays near the physical center. let lut = generate_expo_lut(0.5); let center_index = (ADC_MAX / 2) as usize; let center_value = lut[center_index]; @@ -94,6 +96,7 @@ mod tests { #[test] fn test_generate_expo_lut_different_factors() { + // Different expo factors should yield distinct transfer functions at the same index. let lut_linear = generate_expo_lut(0.0); let lut_expo = generate_expo_lut(1.0); let quarter_point = (ADC_MAX / 4) as usize; @@ -102,6 +105,7 @@ mod tests { #[test] fn test_apply_expo_curve_no_expo() { + // With zero expo factor the lookup table should behave linearly. let lut = generate_expo_lut(0.0); let input_value = 1000u16; let result = apply_expo_curve(input_value, &lut); @@ -111,6 +115,7 @@ mod tests { #[test] fn test_apply_expo_curve_with_expo() { + // Non-zero expo factor should change outputs relative to the linear LUT. let lut_linear = generate_expo_lut(0.0); let lut_expo = generate_expo_lut(0.5); let test_value = 1000u16; @@ -123,6 +128,7 @@ mod tests { #[test] fn test_apply_expo_curve_center_unchanged() { + // Expo mapping should leave the center point near the mechanical center. let lut = generate_expo_lut(0.5); let result = apply_expo_curve(AXIS_CENTER, &lut); // Center point should remain close to center @@ -131,21 +137,25 @@ mod tests { #[test] fn test_constrain_within_range() { + // Values inside limits should be returned unchanged by constrain. assert_eq!(constrain(50u16, 0u16, 100u16), 50u16); } #[test] fn test_constrain_above_range() { + // Values above the upper bound should clamp to the max. assert_eq!(constrain(150u16, 0u16, 100u16), 100u16); } #[test] fn test_constrain_below_range() { + // Values below the lower bound should clamp to the min. assert_eq!(constrain(0u16, 50u16, 100u16), 50u16); } #[test] fn test_expo_integration_real_world_values() { + // Representative axis values should always map within the ADC domain. let lut = generate_expo_lut(0.3); let test_values = [500u16, 1000u16, 2000u16, 3000u16]; diff --git a/rp2040/src/joystick.rs b/rp2040/src/joystick.rs index ab199bb..236dc7e 100644 --- a/rp2040/src/joystick.rs +++ b/rp2040/src/joystick.rs @@ -43,6 +43,7 @@ impl UsbState { } pub fn on_poll(&mut self) { + // Called on every USB poll; mark device active and wake from idle. if !self.initialized { self.initialized = true; } @@ -53,6 +54,7 @@ impl UsbState { } pub fn mark_activity(&mut self) { + // Flag that input activity occurred and reset idle counters. self.activity = true; self.activity_elapsed_ms = 0; self.idle_mode = false; @@ -60,6 +62,7 @@ impl UsbState { } pub fn handle_input_activity(&mut self) { + // Treat input changes as activity and request wake from suspend. self.mark_activity(); if self.suspended && self.wake_on_input { self.wake_on_input = false; @@ -67,6 +70,7 @@ impl UsbState { } pub fn on_suspend_change(&mut self, state: UsbDeviceState) { + // Update suspend bookkeeping when USB state changes. let was_suspended = self.suspended; self.suspended = state == UsbDeviceState::Suspend; @@ -86,6 +90,7 @@ impl UsbState { } pub fn advance_idle_timer(&mut self, interval_ms: u32) { + // Age the activity timer, transitioning to idle after the timeout. if !self.activity { return; } @@ -99,6 +104,7 @@ impl UsbState { } pub fn acknowledge_report(&mut self) { + // Reset pending state once the host accepts a report. self.send_pending = false; } } @@ -116,6 +122,7 @@ pub struct JoystickState { impl JoystickState { pub fn new() -> Self { + // Initialize managers, smoothers, expo curves, and USB state. Self { axis_manager: AxisManager::new(), button_manager: ButtonManager::new(), @@ -132,6 +139,7 @@ impl JoystickState { where R: FnMut(u32) -> Result, { + // Fetch stored axis calibration and gimbal mode from persistent storage. CalibrationManager::load_axis_calibration(&mut self.axis_manager.axes, &mut read_fn); let gimbal_mode = CalibrationManager::load_gimbal_mode(&mut read_fn); self.axis_manager.set_gimbal_mode(gimbal_mode); @@ -149,6 +157,7 @@ impl JoystickState { L::Error: Debug, R::Error: Debug, { + // Refresh matrix-driven and discrete button inputs, then normalize hats. self.button_manager.update_from_matrix(matrix); self.button_manager .update_extra_buttons(left_button, right_button); @@ -156,10 +165,12 @@ impl JoystickState { } pub fn finalize_button_logic(&mut self, timer: &Timer) -> bool { + // Update per-button timers and USB state using the shared hardware timer. self.button_manager.process_button_logic_with_timer(timer) } pub fn check_special_action(&self) -> SpecialAction { + // Inspect button state for bootloader/calibration/hold command combinations. self.button_manager.check_special_combinations( self.axis_manager.get_value_before_hold(), self.calibration_manager.is_active(), @@ -170,6 +181,7 @@ impl JoystickState { where W: FnMut(u32, &[u8]) -> Result<(), ()>, { + // Execute the requested special action, updating calibration/throttle state as needed. match action { SpecialAction::Bootloader => {} SpecialAction::StartCalibration => { diff --git a/rp2040/src/main.rs b/rp2040/src/main.rs index 61089b4..cd1ee21 100644 --- a/rp2040/src/main.rs +++ b/rp2040/src/main.rs @@ -20,6 +20,7 @@ pub static BOOT2_FIRMWARE: [u8; 256] = rp2040_boot2::BOOT_LOADER_W25Q080; #[rp2040_hal::entry] fn main() -> ! { + // Firmware entry point initializes hardware before handing off to RTIC. let BoardParts { mut button_matrix, mut status_led, @@ -33,6 +34,7 @@ fn main() -> ! { usb_bus, } = Board::new().into_parts(); + // Build the HID joystick class on the shared USB bus. let mut usb_hid_joystick = UsbHidClassBuilder::new() .add_device(JoystickConfig::default()) .build(usb_bus); @@ -56,10 +58,12 @@ fn main() -> ! { } { + // Load persisted calibration values from EEPROM if available. let mut read_fn = |addr: u32| eeprom.read_byte(addr).map_err(|_| ()); state.load_calibration(&mut read_fn); } + // Set up periodic timers for scanning, status updates, and USB activity. let mut scan_tick = timer.count_down(); scan_tick.start(timers::SCAN_INTERVAL_US.micros()); @@ -71,7 +75,9 @@ fn main() -> ! { let mut status_time_ms: u32 = 0; + // Main control loop: service USB, process inputs, and emit reports. loop { + // Service the USB stack and HID class when data is pending. if usb_dev.poll(&mut [&mut usb_hid_joystick]) { state.usb_state().on_poll(); } @@ -79,11 +85,13 @@ fn main() -> ! { let usb_state = usb_dev.state(); state.usb_state().on_suspend_change(usb_state); + // Periodically refresh the status LED animation. if status_tick.wait().is_ok() { status_time_ms = status_time_ms.saturating_add(timers::STATUS_LED_INTERVAL_MS); status_led.update_from_system_state(state.system_state(), status_time_ms); } + // Slow the scan cadence when USB is suspended to save power. let should_scan = if state.usb_state().suspended { static mut SUSPENDED_SCAN_COUNTER: u8 = 0; unsafe { @@ -95,6 +103,7 @@ fn main() -> ! { }; if should_scan && scan_tick.wait().is_ok() { + // Scan buttons, read analog axes, and update state machines. button_matrix.scan_matrix(&mut delay); let mut raw_values = [ @@ -111,6 +120,7 @@ fn main() -> ! { &mut right_extra_button, ); + // Evaluate special button combinations (bootloader, calibration, etc.). let action = state.check_special_action(); if matches!(action, SpecialAction::Bootloader) { if !state.usb_state().suspended { @@ -132,6 +142,7 @@ fn main() -> ! { state.handle_special_action(action, &mut write_page); } + // Track calibration extrema and process axis/virtual/button logic. state.update_calibration_tracking(); if state.process_axes() { @@ -147,6 +158,7 @@ fn main() -> ! { } } + // Advance USB idle timers and decide when to send reports. let usb_tick_elapsed = usb_tick.wait().is_ok(); if usb_tick_elapsed { state @@ -154,6 +166,7 @@ fn main() -> ! { .advance_idle_timer(timers::USB_UPDATE_INTERVAL_MS); } + // Emit a new HID report when activity is pending and USB is ready. if state.usb_state().activity && (usb_tick_elapsed || state.usb_state().send_pending) && !state.usb_state().suspended diff --git a/rp2040/src/mapping.rs b/rp2040/src/mapping.rs index 27fc347..c94047f 100644 --- a/rp2040/src/mapping.rs +++ b/rp2040/src/mapping.rs @@ -171,6 +171,7 @@ mod tests { #[test] fn front_buttons_have_expected_mappings() { + // Front panel buttons map to the expected USB button ids. let mut buttons = [Button::default(); TOTAL_BUTTONS]; configure_button_mappings(&mut buttons); @@ -181,6 +182,7 @@ mod tests { #[test] fn long_press_flags_set_correctly() { + // Long-press flags are configured for buttons that need them at runtime. let mut buttons = [Button::default(); TOTAL_BUTTONS]; configure_button_mappings(&mut buttons); @@ -192,6 +194,7 @@ mod tests { #[test] fn hat_buttons_map_to_expected_ids() { + // Hat direction buttons should map to the numerical HID hat constants. let mut buttons = [Button::default(); TOTAL_BUTTONS]; configure_button_mappings(&mut buttons); diff --git a/rp2040/src/status.rs b/rp2040/src/status.rs index 0a20933..02f3625 100644 --- a/rp2040/src/status.rs +++ b/rp2040/src/status.rs @@ -67,6 +67,7 @@ const HEARTBEAT_IDLE_MS: u32 = 3200; impl LedEffect { fn update_interval_ms(self) -> u32 { + // Resolve the LED descriptor for the requested status mode. match self { LedEffect::Solid => 0, LedEffect::Blink { period_ms } => period_ms / 2, @@ -75,6 +76,7 @@ impl LedEffect { } fn color_for(self, base: RGB8, elapsed_ms: u32) -> RGB8 { + // Compute the base status mode given system state flags. match self { LedEffect::Solid => base, LedEffect::Blink { period_ms } => { @@ -197,6 +199,7 @@ const fn descriptor_for(mode: StatusMode, base_mode: StatusMode) -> ModeDescript } fn scale_color(base: RGB8, brightness: u8) -> RGB8 { + // Scale each RGB component proportionally to the requested brightness factor. let scale = brightness as u16; RGB8 { r: ((base.r as u16 * scale) / 255) as u8, @@ -206,6 +209,7 @@ fn scale_color(base: RGB8, brightness: u8) -> RGB8 { } fn determine_base_mode(system_state: SystemState) -> StatusMode { + // Compute the base status mode based on USB/calibration/idle state. if system_state.usb_suspended { StatusMode::Suspended } else if system_state.calibration_active { @@ -331,6 +335,7 @@ where /// Write a single color to the LED. fn write_color(&mut self, color: RGB8) { + // Push the color to the WS2812 LED, ignoring transient IO errors. let _ = self.ws2812_direct.write([color].iter().copied()); } } @@ -354,6 +359,7 @@ mod tests { #[test] fn idle_mode_uses_base_color_with_heartbeat() { + // Idle state should inherit the base color while enforcing a heartbeat pattern. let state = SystemState { usb_active: false, usb_initialized: true, @@ -376,6 +382,7 @@ mod tests { #[test] fn power_mode_uses_fast_heartbeat() { + // Power mode descriptor should use the fast heartbeat cadence and green. let descriptor = descriptor_for(StatusMode::Power, StatusMode::Normal); if let LedEffect::Heartbeat { period_ms } = descriptor.effect { assert_eq!(period_ms, HEARTBEAT_POWER_MS); @@ -387,6 +394,7 @@ mod tests { #[test] fn calibration_has_priority_over_idle() { + // Calibration activity should override idle when both flags are set. let state = SystemState { usb_active: true, usb_initialized: true, @@ -403,6 +411,7 @@ mod tests { #[test] fn heartbeat_effect_fades() { + // Heartbeat should ramp up and down around the target color. let base = StatusMode::Normal; let descriptor = descriptor_for(StatusMode::Idle, base); let LedEffect::Heartbeat { period_ms } = descriptor.effect else { @@ -422,6 +431,7 @@ mod tests { #[test] fn blink_effect_toggles() { + // Blink descriptor should alternate between the color and off state. let descriptor = descriptor_for(StatusMode::NormalFlash, StatusMode::NormalFlash); let LedEffect::Blink { period_ms } = descriptor.effect else { panic!("NormalFlash should use blink effect"); @@ -435,6 +445,7 @@ mod tests { #[test] fn determine_base_mode_before_usb() { + // Before USB comes up the controller should stay in Power mode. let state = SystemState { usb_active: false, usb_initialized: false, @@ -450,6 +461,7 @@ mod tests { #[test] fn usb_suspend_takes_priority() { + // USB suspend should trump other status priorities. let state = SystemState { usb_active: true, usb_initialized: true, diff --git a/rp2040/src/storage.rs b/rp2040/src/storage.rs index d7e8eb4..1655cd2 100644 --- a/rp2040/src/storage.rs +++ b/rp2040/src/storage.rs @@ -79,6 +79,7 @@ pub fn write_calibration_data( /// Read a u16 value from EEPROM in little‑endian (low then high byte) format. fn read_u16_with_closure( + // Fetch a little-endian u16 by reading two consecutive EEPROM bytes. read_byte_fn: &mut dyn FnMut(u32) -> Result, low_addr: u32, high_addr: u32, @@ -171,6 +172,7 @@ mod tests { #[test] fn test_boundary_values() { + // Pack axis tuples into EEPROM layout and write the gimbal mode byte. let mut buffer = [0u8; 4]; // Test minimum value (manual packing) diff --git a/rp2040/src/usb_joystick_device.rs b/rp2040/src/usb_joystick_device.rs index c5e5a68..83cd92b 100644 --- a/rp2040/src/usb_joystick_device.rs +++ b/rp2040/src/usb_joystick_device.rs @@ -37,6 +37,7 @@ pub trait Try { type Ok; type Error; fn into_result(self) -> Result; + // Trait shim replicating `core::Try` for use in no_std contexts. } impl Try for Option { @@ -45,6 +46,7 @@ impl Try for Option { #[inline] fn into_result(self) -> Result { + // Convert an optional value into a result with a unit error. self.ok_or(NoneError) } } @@ -55,6 +57,7 @@ impl Try for Result { #[inline] fn into_result(self) -> Self { + // `Result` already matches the desired signature; return it untouched. self } } @@ -165,12 +168,16 @@ impl<'a, B: UsbBus> DeviceClass<'a> for Joystick<'a, B> { type I = Interface<'a, B, InBytes32, OutNone, ReportSingle>; fn interface(&mut self) -> &mut Self::I { + // Expose the HID interface so the USB stack can enqueue reports. &mut self.interface } - fn reset(&mut self) {} + fn reset(&mut self) { + // Nothing to reset for this simple HID device. + } fn tick(&mut self) -> Result<(), UsbHidError> { + // Flush pending HID data and poll the USB stack for new requests. Ok(()) } } @@ -181,6 +188,7 @@ pub struct JoystickConfig<'a> { impl Default for JoystickConfig<'_> { fn default() -> Self { + // Construct the HID interface with the default joystick descriptor and endpoints. Self::new( unwrap!(unwrap!(InterfaceBuilder::new(JOYSTICK_DESCRIPTOR)) .boot_device(InterfaceProtocol::None) @@ -203,6 +211,7 @@ impl<'a, B: UsbBus + 'a> UsbAllocatable<'a, B> for JoystickConfig<'a> { type Allocated = Joystick<'a, B>; fn allocate(self, usb_alloc: &'a UsbBusAllocator) -> Self::Allocated { + // Allocate the HID interface using the provided USB bus allocator. Self::Allocated { interface: Interface::new(usb_alloc, self.interface), } diff --git a/rp2040/src/usb_report.rs b/rp2040/src/usb_report.rs index e923d94..76b4206 100644 --- a/rp2040/src/usb_report.rs +++ b/rp2040/src/usb_report.rs @@ -184,6 +184,7 @@ mod tests { #[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]; @@ -214,6 +215,7 @@ mod tests { #[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]; @@ -237,6 +239,7 @@ mod tests { #[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]; @@ -261,6 +264,7 @@ mod tests { #[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]; @@ -285,6 +289,7 @@ mod tests { #[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]; @@ -307,6 +312,7 @@ mod tests { #[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]; @@ -327,6 +333,7 @@ mod tests { #[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]; @@ -347,6 +354,7 @@ mod tests { #[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]; @@ -368,6 +376,7 @@ mod tests { #[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];