Updated documentation
This commit is contained in:
parent
c5204b172b
commit
1799f765dc
16
AGENTS.md
16
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.
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -29,6 +29,7 @@ pub type JoystickStatusLed = StatusLed<pac::PIO0, rp2040_hal::pio::SM0, hardware
|
||||
type BoardI2c = I2C<pac::I2C1, (hardware::I2cSdaPin, hardware::I2cSclPin)>;
|
||||
type BoardEeprom = Eeprom24x<BoardI2c, page_size::B32, addr_size::TwoBytes, unique_serial::No>;
|
||||
|
||||
/// Strongly-typed collection of ADC-capable pins for each physical gimbal axis.
|
||||
pub struct AxisAnalogPins {
|
||||
pub left_x: AdcPin<Pin<gpio::bank0::Gpio29, FunctionSioInput, PullNone>>,
|
||||
pub left_y: AdcPin<Pin<gpio::bank0::Gpio28, FunctionSioInput, PullNone>>,
|
||||
@ -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<rp2040_hal::usb::UsbBus>,
|
||||
}
|
||||
|
||||
/// 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<rp2040_hal::usb::UsbBus> {
|
||||
// Lazily create the shared USB bus allocator so HID endpoints can borrow it.
|
||||
static USB_BUS: StaticCell<UsbBusAllocator<rp2040_hal::usb::UsbBus>> = 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(
|
||||
|
||||
@ -35,20 +35,24 @@ impl<const ROWS: usize, const COLS: usize> MatrixPins<ROWS, COLS> {
|
||||
|
||||
impl<const ROWS: usize, const COLS: usize> MatrixPinAccess<ROWS, COLS> for MatrixPins<ROWS, COLS> {
|
||||
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<Cell<bool>>, column_state: Rc<Cell<bool>>) -> 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);
|
||||
|
||||
|
||||
@ -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<bool> {
|
||||
// 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
|
||||
|
||||
@ -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];
|
||||
|
||||
|
||||
@ -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];
|
||||
|
||||
|
||||
@ -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<u8, ()>,
|
||||
{
|
||||
// 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 => {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<u8, ()>,
|
||||
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)
|
||||
|
||||
@ -37,6 +37,7 @@ pub trait Try {
|
||||
type Ok;
|
||||
type Error;
|
||||
fn into_result(self) -> Result<Self::Ok, Self::Error>;
|
||||
// Trait shim replicating `core::Try` for use in no_std contexts.
|
||||
}
|
||||
|
||||
impl<T> Try for Option<T> {
|
||||
@ -45,6 +46,7 @@ impl<T> Try for Option<T> {
|
||||
|
||||
#[inline]
|
||||
fn into_result(self) -> Result<T, NoneError> {
|
||||
// Convert an optional value into a result with a unit error.
|
||||
self.ok_or(NoneError)
|
||||
}
|
||||
}
|
||||
@ -55,6 +57,7 @@ impl<T, E> Try for Result<T, E> {
|
||||
|
||||
#[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<B>) -> Self::Allocated {
|
||||
// Allocate the HID interface using the provided USB bus allocator.
|
||||
Self::Allocated {
|
||||
interface: Interface::new(usb_alloc, self.interface),
|
||||
}
|
||||
|
||||
@ -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];
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user