Updated documentation
This commit is contained in:
parent
c5204b172b
commit
1799f765dc
16
AGENTS.md
16
AGENTS.md
@ -1,13 +1,23 @@
|
|||||||
# Assistant Configuration
|
# Assistant Configuration
|
||||||
|
|
||||||
This file contains configuration and commands for the Claude assistant working on the CMtec CMDR Joystick 25.
|
|
||||||
|
|
||||||
## Global Rules
|
## Global Rules
|
||||||
|
|
||||||
|
- Rust emdedded
|
||||||
- Always describe what you thinking and your plan befor starting to change files.
|
- Always describe what you thinking and your plan befor starting to change files.
|
||||||
- Make sure code have max 5 indentation levels
|
- 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
|
- Make sure the codebase is manageable and easily readable
|
||||||
- Always check code (compile/check)
|
- Always check code (compile/check)
|
||||||
- Always fix compile warnings
|
- Always fix compile warnings
|
||||||
- Do not try to deploy project to hardware
|
- 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 {
|
impl Default for GimbalAxis {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
// Apply new calibration limits supplied by the calibration manager.
|
||||||
GimbalAxis {
|
GimbalAxis {
|
||||||
value: AXIS_CENTER,
|
value: AXIS_CENTER,
|
||||||
value_before_hold: AXIS_CENTER,
|
value_before_hold: AXIS_CENTER,
|
||||||
@ -143,6 +144,7 @@ pub struct VirtualAxis {
|
|||||||
|
|
||||||
impl Default for VirtualAxis {
|
impl Default for VirtualAxis {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
// Create a virtual axis starting at center with the supplied step size.
|
||||||
VirtualAxis {
|
VirtualAxis {
|
||||||
value: AXIS_CENTER,
|
value: AXIS_CENTER,
|
||||||
step: 5,
|
step: 5,
|
||||||
@ -206,6 +208,7 @@ pub struct AxisManager {
|
|||||||
|
|
||||||
impl Default for AxisManager {
|
impl Default for AxisManager {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
// Delegate to `new` so the default stays aligned with explicit constructor logic.
|
||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -462,6 +465,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_gimbal_axis_default() {
|
fn test_gimbal_axis_default() {
|
||||||
|
// Factory defaults should leave every axis field pointing at the center span.
|
||||||
let axis = GimbalAxis::default();
|
let axis = GimbalAxis::default();
|
||||||
assert_eq!(axis.value, AXIS_CENTER);
|
assert_eq!(axis.value, AXIS_CENTER);
|
||||||
assert_eq!(axis.min, ADC_MIN);
|
assert_eq!(axis.min, ADC_MIN);
|
||||||
@ -473,6 +477,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_gimbal_axis_new() {
|
fn test_gimbal_axis_new() {
|
||||||
|
// `new` should be a thin wrapper over `Default` without changing members.
|
||||||
let axis = GimbalAxis::new();
|
let axis = GimbalAxis::new();
|
||||||
assert_eq!(axis.value, AXIS_CENTER);
|
assert_eq!(axis.value, AXIS_CENTER);
|
||||||
assert_eq!(axis.min, ADC_MIN);
|
assert_eq!(axis.min, ADC_MIN);
|
||||||
@ -483,6 +488,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_gimbal_axis_with_calibration() {
|
fn test_gimbal_axis_with_calibration() {
|
||||||
|
// Axis constructed with explicit calibration should adopt all provided bounds.
|
||||||
let axis = GimbalAxis::with_calibration(100, 3900, 2000);
|
let axis = GimbalAxis::with_calibration(100, 3900, 2000);
|
||||||
assert_eq!(axis.min, 100);
|
assert_eq!(axis.min, 100);
|
||||||
assert_eq!(axis.max, 3900);
|
assert_eq!(axis.max, 3900);
|
||||||
@ -491,6 +497,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_gimbal_axis_activity_detection() {
|
fn test_gimbal_axis_activity_detection() {
|
||||||
|
// Activity flag should only flip when the axis value actually changes.
|
||||||
let mut axis = GimbalAxis::new();
|
let mut axis = GimbalAxis::new();
|
||||||
|
|
||||||
// Initially no activity (same as previous)
|
// Initially no activity (same as previous)
|
||||||
@ -506,6 +513,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_gimbal_axis_throttle_hold_processing() {
|
fn test_gimbal_axis_throttle_hold_processing() {
|
||||||
|
// Throttle hold remapping should cover remap, center latch, and pending hold cases.
|
||||||
let mut axis = GimbalAxis::new();
|
let mut axis = GimbalAxis::new();
|
||||||
axis.set_hold(1500); // Set hold value below center
|
axis.set_hold(1500); // Set hold value below center
|
||||||
|
|
||||||
@ -541,6 +549,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_virtual_axis_default() {
|
fn test_virtual_axis_default() {
|
||||||
|
// Virtual axis should boot at center with the configured step size.
|
||||||
let virtual_axis = VirtualAxis::default();
|
let virtual_axis = VirtualAxis::default();
|
||||||
assert_eq!(virtual_axis.value, AXIS_CENTER);
|
assert_eq!(virtual_axis.value, AXIS_CENTER);
|
||||||
assert_eq!(virtual_axis.step, 5);
|
assert_eq!(virtual_axis.step, 5);
|
||||||
@ -548,6 +557,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_virtual_axis_movement_up() {
|
fn test_virtual_axis_movement_up() {
|
||||||
|
// An upward press should increment the virtual axis and flag activity.
|
||||||
let mut virtual_axis = VirtualAxis::new(10);
|
let mut virtual_axis = VirtualAxis::new(10);
|
||||||
|
|
||||||
// Test upward movement
|
// Test upward movement
|
||||||
@ -558,6 +568,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_virtual_axis_movement_down() {
|
fn test_virtual_axis_movement_down() {
|
||||||
|
// A downward press should decrement the virtual axis and flag activity.
|
||||||
let mut virtual_axis = VirtualAxis::new(10);
|
let mut virtual_axis = VirtualAxis::new(10);
|
||||||
|
|
||||||
// Test downward movement
|
// Test downward movement
|
||||||
@ -568,6 +579,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_virtual_axis_return_to_center() {
|
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);
|
let mut virtual_axis = VirtualAxis::new(10);
|
||||||
virtual_axis.value = AXIS_CENTER + 20;
|
virtual_axis.value = AXIS_CENTER + 20;
|
||||||
|
|
||||||
@ -579,6 +591,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_virtual_axis_direction_compensation() {
|
fn test_virtual_axis_direction_compensation() {
|
||||||
|
// Reversing direction should recenter before stepping to avoid large jumps.
|
||||||
let mut virtual_axis = VirtualAxis::new(10);
|
let mut virtual_axis = VirtualAxis::new(10);
|
||||||
virtual_axis.value = AXIS_CENTER - 100;
|
virtual_axis.value = AXIS_CENTER - 100;
|
||||||
|
|
||||||
@ -591,6 +604,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_axis_manager_creation() {
|
fn test_axis_manager_creation() {
|
||||||
|
// Manager construction should initialize all axes and state to defaults.
|
||||||
let manager = AxisManager::new();
|
let manager = AxisManager::new();
|
||||||
assert_eq!(manager.axes.len(), NBR_OF_GIMBAL_AXIS);
|
assert_eq!(manager.axes.len(), NBR_OF_GIMBAL_AXIS);
|
||||||
assert_eq!(manager.gimbal_mode, GIMBAL_MODE_M10);
|
assert_eq!(manager.gimbal_mode, GIMBAL_MODE_M10);
|
||||||
@ -600,6 +614,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_gimbal_compensation_m10() {
|
fn test_gimbal_compensation_m10() {
|
||||||
|
// M10 gimbal compensation must invert the correct pair of axes.
|
||||||
let manager = AxisManager::new(); // Default is M10
|
let manager = AxisManager::new(); // Default is M10
|
||||||
let mut raw_values = [1000, 1500, 2000, 2500];
|
let mut raw_values = [1000, 1500, 2000, 2500];
|
||||||
|
|
||||||
@ -614,6 +629,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_gimbal_compensation_m7() {
|
fn test_gimbal_compensation_m7() {
|
||||||
|
// M7 gimbal compensation must invert the complementary axes.
|
||||||
let mut manager = AxisManager::new();
|
let mut manager = AxisManager::new();
|
||||||
manager.set_gimbal_mode(GIMBAL_MODE_M7);
|
manager.set_gimbal_mode(GIMBAL_MODE_M7);
|
||||||
let mut raw_values = [1000, 1500, 2000, 2500];
|
let mut raw_values = [1000, 1500, 2000, 2500];
|
||||||
@ -629,6 +645,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_axis_activity_detection() {
|
fn test_axis_activity_detection() {
|
||||||
|
// Manager activity flag should reflect when any axis value changes.
|
||||||
let mut manager = AxisManager::new();
|
let mut manager = AxisManager::new();
|
||||||
|
|
||||||
// No activity initially
|
// No activity initially
|
||||||
@ -644,6 +661,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_calculate_axis_value_boundaries() {
|
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
|
let expo_lut = ExpoLUT::new(0.0); // No expo for testing
|
||||||
|
|
||||||
// Test min boundary
|
// Test min boundary
|
||||||
@ -657,6 +675,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_calculate_axis_value_deadzone() {
|
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
|
let expo_lut = ExpoLUT::new(0.0); // No expo for testing
|
||||||
|
|
||||||
// Test center deadzone
|
// Test center deadzone
|
||||||
@ -666,6 +685,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_calculate_axis_value_degenerate_calibration() {
|
fn test_calculate_axis_value_degenerate_calibration() {
|
||||||
|
// Degenerate calibration inputs should return the provided center without panicking.
|
||||||
let expo_lut = ExpoLUT::new(0.0);
|
let expo_lut = ExpoLUT::new(0.0);
|
||||||
|
|
||||||
// When calibration collapses to a single point (min=max=center),
|
// 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 BoardI2c = I2C<pac::I2C1, (hardware::I2cSdaPin, hardware::I2cSclPin)>;
|
||||||
type BoardEeprom = Eeprom24x<BoardI2c, page_size::B32, addr_size::TwoBytes, unique_serial::No>;
|
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 struct AxisAnalogPins {
|
||||||
pub left_x: AdcPin<Pin<gpio::bank0::Gpio29, FunctionSioInput, PullNone>>,
|
pub left_x: AdcPin<Pin<gpio::bank0::Gpio29, FunctionSioInput, PullNone>>,
|
||||||
pub left_y: AdcPin<Pin<gpio::bank0::Gpio28, FunctionSioInput, PullNone>>,
|
pub left_y: AdcPin<Pin<gpio::bank0::Gpio28, FunctionSioInput, PullNone>>,
|
||||||
@ -38,6 +39,7 @@ pub struct AxisAnalogPins {
|
|||||||
|
|
||||||
impl AxisAnalogPins {
|
impl AxisAnalogPins {
|
||||||
fn new(inputs: hardware::AxisInputs) -> Self {
|
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_x = AdcPin::new(inputs.left_x).unwrap();
|
||||||
let left_y = AdcPin::new(inputs.left_y).unwrap();
|
let left_y = AdcPin::new(inputs.left_y).unwrap();
|
||||||
let right_x = AdcPin::new(inputs.right_x).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 {
|
pub struct Board {
|
||||||
button_matrix: JoystickMatrix,
|
button_matrix: JoystickMatrix,
|
||||||
status_led: JoystickStatusLed,
|
status_led: JoystickStatusLed,
|
||||||
@ -64,6 +67,7 @@ pub struct Board {
|
|||||||
usb_bus: &'static UsbBusAllocator<rp2040_hal::usb::UsbBus>,
|
usb_bus: &'static UsbBusAllocator<rp2040_hal::usb::UsbBus>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Board components handed off to the application after initialization.
|
||||||
pub struct BoardParts {
|
pub struct BoardParts {
|
||||||
pub button_matrix: JoystickMatrix,
|
pub button_matrix: JoystickMatrix,
|
||||||
pub status_led: JoystickStatusLed,
|
pub status_led: JoystickStatusLed,
|
||||||
@ -79,10 +83,12 @@ pub struct BoardParts {
|
|||||||
|
|
||||||
impl Board {
|
impl Board {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
// Acquire RP2040 peripheral handles before configuration begins.
|
||||||
let mut pac = pac::Peripherals::take().unwrap();
|
let mut pac = pac::Peripherals::take().unwrap();
|
||||||
let core = pac::CorePeripherals::take().unwrap();
|
let core = pac::CorePeripherals::take().unwrap();
|
||||||
|
|
||||||
let mut watchdog = Watchdog::new(pac.WATCHDOG);
|
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(
|
let clocks = init_clocks_and_plls(
|
||||||
hardware::XTAL_FREQ_HZ,
|
hardware::XTAL_FREQ_HZ,
|
||||||
pac.XOSC,
|
pac.XOSC,
|
||||||
@ -96,6 +102,7 @@ impl Board {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let sio = Sio::new(pac.SIO);
|
let sio = Sio::new(pac.SIO);
|
||||||
|
// Split GPIO banks and translate them into the strongly typed board pins.
|
||||||
let raw_pins = gpio::Pins::new(
|
let raw_pins = gpio::Pins::new(
|
||||||
pac.IO_BANK0,
|
pac.IO_BANK0,
|
||||||
pac.PADS_BANK0,
|
pac.PADS_BANK0,
|
||||||
@ -105,6 +112,7 @@ impl Board {
|
|||||||
let pins = BoardPins::new(raw_pins);
|
let pins = BoardPins::new(raw_pins);
|
||||||
|
|
||||||
let matrix_pins = MatrixPins::new(pins.matrix_rows, pins.matrix_cols);
|
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(
|
let mut button_matrix = ButtonMatrix::new(
|
||||||
matrix_pins,
|
matrix_pins,
|
||||||
hardware::MATRIX_DEBOUNCE_SCANS,
|
hardware::MATRIX_DEBOUNCE_SCANS,
|
||||||
@ -113,6 +121,7 @@ impl Board {
|
|||||||
button_matrix.init_pins();
|
button_matrix.init_pins();
|
||||||
|
|
||||||
let (mut pio, sm0, _, _, _) = pac.PIO0.split(&mut pac.RESETS);
|
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(
|
let status_led = StatusLed::new(
|
||||||
pins.status_led,
|
pins.status_led,
|
||||||
&mut pio,
|
&mut pio,
|
||||||
@ -120,9 +129,11 @@ impl Board {
|
|||||||
clocks.peripheral_clock.freq(),
|
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 timer = Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);
|
||||||
let delay = Delay::new(core.SYST, clocks.system_clock.freq().to_Hz());
|
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(
|
let i2c = I2C::i2c1(
|
||||||
pac.I2C1,
|
pac.I2C1,
|
||||||
pins.i2c_sda,
|
pins.i2c_sda,
|
||||||
@ -133,9 +144,11 @@ impl Board {
|
|||||||
);
|
);
|
||||||
let eeprom = Eeprom24x::new_24x32(i2c, hardware::i2c::EEPROM_ADDRESS);
|
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 adc = Adc::new(pac.ADC, &mut pac.RESETS);
|
||||||
let axis_pins = AxisAnalogPins::new(pins.axis_inputs);
|
let axis_pins = AxisAnalogPins::new(pins.axis_inputs);
|
||||||
|
|
||||||
|
// Prepare a global USB bus allocator for the joystick HID device.
|
||||||
let usb_bus = usb_allocator(
|
let usb_bus = usb_allocator(
|
||||||
pac.USBCTRL_REGS,
|
pac.USBCTRL_REGS,
|
||||||
pac.USBCTRL_DPRAM,
|
pac.USBCTRL_DPRAM,
|
||||||
@ -179,8 +192,11 @@ fn usb_allocator(
|
|||||||
usb_clock: rp2040_hal::clocks::UsbClock,
|
usb_clock: rp2040_hal::clocks::UsbClock,
|
||||||
resets: &mut pac::RESETS,
|
resets: &mut pac::RESETS,
|
||||||
) -> &'static UsbBusAllocator<rp2040_hal::usb::UsbBus> {
|
) -> &'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();
|
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(|_| {
|
interrupt::free(|_| {
|
||||||
USB_BUS.init_with(|| {
|
USB_BUS.init_with(|| {
|
||||||
UsbBusAllocator::new(rp2040_hal::usb::UsbBus::new(
|
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> {
|
impl<const ROWS: usize, const COLS: usize> MatrixPinAccess<ROWS, COLS> for MatrixPins<ROWS, COLS> {
|
||||||
fn init_columns(&mut self) {
|
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() {
|
for column in self.cols.iter_mut() {
|
||||||
let _ = column.set_high();
|
let _ = column.set_high();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_column_low(&mut self, column: usize) {
|
fn set_column_low(&mut self, column: usize) {
|
||||||
|
// Pull the active column low before scanning its rows.
|
||||||
let _ = self.cols[column].set_low();
|
let _ = self.cols[column].set_low();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_column_high(&mut self, column: usize) {
|
fn set_column_high(&mut self, column: usize) {
|
||||||
|
// Release the column after scanning so other columns remain idle.
|
||||||
let _ = self.cols[column].set_high();
|
let _ = self.cols[column].set_high();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_row(&mut self, row: usize) -> bool {
|
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)
|
self.rows[row].is_low().unwrap_or(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,6 +116,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn process_column(&mut self, column: usize) {
|
fn process_column(&mut self, column: usize) {
|
||||||
|
// Drive a single column scan to update button press history.
|
||||||
for row in 0..ROWS {
|
for row in 0..ROWS {
|
||||||
let index = column + (row * COLS);
|
let index = column + (row * COLS);
|
||||||
let current_state = self.pins.read_row(row);
|
let current_state = self.pins.read_row(row);
|
||||||
@ -138,6 +143,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn should_register_press(&mut self, index: usize) -> bool {
|
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 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;
|
let can_register = self.last_press_scan[index] == 0 || elapsed >= self.min_press_gap_scans;
|
||||||
|
|
||||||
@ -178,6 +184,7 @@ mod tests {
|
|||||||
|
|
||||||
impl MockPins {
|
impl MockPins {
|
||||||
fn new(row_state: Rc<Cell<bool>>, column_state: Rc<Cell<bool>>) -> Self {
|
fn new(row_state: Rc<Cell<bool>>, column_state: Rc<Cell<bool>>) -> Self {
|
||||||
|
// Build a button matrix scanner with default state tracking arrays.
|
||||||
Self {
|
Self {
|
||||||
row_state,
|
row_state,
|
||||||
column_state,
|
column_state,
|
||||||
@ -187,18 +194,22 @@ mod tests {
|
|||||||
|
|
||||||
impl MatrixPinAccess<1, 1> for MockPins {
|
impl MatrixPinAccess<1, 1> for MockPins {
|
||||||
fn init_columns(&mut self) {
|
fn init_columns(&mut self) {
|
||||||
|
// Simulate the hardware by driving the single column high by default.
|
||||||
self.column_state.set(true);
|
self.column_state.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_column_low(&mut self, _column: usize) {
|
fn set_column_low(&mut self, _column: usize) {
|
||||||
|
// Drop the mock column low to emulate scanning behaviour.
|
||||||
self.column_state.set(false);
|
self.column_state.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_column_high(&mut self, _column: usize) {
|
fn set_column_high(&mut self, _column: usize) {
|
||||||
|
// Release the mock column back to the idle high state.
|
||||||
self.column_state.set(true);
|
self.column_state.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_row(&mut self, _row: usize) -> bool {
|
fn read_row(&mut self, _row: usize) -> bool {
|
||||||
|
// Return the mocked row state so tests can control pressed/unpressed.
|
||||||
self.row_state.get()
|
self.row_state.get()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -217,6 +228,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn debounce_requires_consecutive_scans() {
|
fn debounce_requires_consecutive_scans() {
|
||||||
|
// Debounce logic should require two consecutive pressed scans before registering.
|
||||||
let (mut matrix, row, _column) = fixture();
|
let (mut matrix, row, _column) = fixture();
|
||||||
matrix.set_scan_counter(1);
|
matrix.set_scan_counter(1);
|
||||||
|
|
||||||
|
|||||||
@ -67,6 +67,7 @@ pub struct ButtonManager {
|
|||||||
|
|
||||||
impl Default for ButtonManager {
|
impl Default for ButtonManager {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
// Build a button manager with default-initialized buttons and state flags.
|
||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -121,6 +122,7 @@ impl ButtonManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn reconcile_hat(&mut self, directions: &[usize; 4], center: usize) {
|
fn reconcile_hat(&mut self, directions: &[usize; 4], center: usize) {
|
||||||
|
// Normalize hat inputs by clearing the center and conflicting directions.
|
||||||
let pressed_count = directions
|
let pressed_count = directions
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|&&index| self.buttons[index].pressed)
|
.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.
|
/// 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> {
|
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];
|
let button = &self.buttons[button_index];
|
||||||
if button.pressed != button.previous_pressed {
|
if button.pressed != button.previous_pressed {
|
||||||
Some(button.pressed)
|
Some(button.pressed)
|
||||||
@ -265,6 +268,7 @@ impl ButtonManager {
|
|||||||
/// - Short press: on release (and if long not activated), activate `usb_button`
|
/// - 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
|
/// - 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) {
|
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;
|
const LONG_PRESS_THRESHOLD: u32 = 200;
|
||||||
|
|
||||||
// Pressing button
|
// Pressing button
|
||||||
@ -334,12 +338,14 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_button_manager_creation() {
|
fn test_button_manager_creation() {
|
||||||
|
// Button manager should allocate an entry for every physical button.
|
||||||
let manager = ButtonManager::new();
|
let manager = ButtonManager::new();
|
||||||
assert_eq!(manager.buttons.len(), TOTAL_BUTTONS);
|
assert_eq!(manager.buttons.len(), TOTAL_BUTTONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_button_default_state() {
|
fn test_button_default_state() {
|
||||||
|
// Default button instances start unpressed with no lingering USB state.
|
||||||
let button = Button::default();
|
let button = Button::default();
|
||||||
assert!(!button.pressed);
|
assert!(!button.pressed);
|
||||||
assert!(!button.previous_pressed);
|
assert!(!button.previous_pressed);
|
||||||
@ -349,6 +355,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_special_action_combinations() {
|
fn test_special_action_combinations() {
|
||||||
|
// The bootloader combo should trigger when all required buttons are pressed.
|
||||||
let mut manager = ButtonManager::new();
|
let mut manager = ButtonManager::new();
|
||||||
|
|
||||||
// Test bootloader combination
|
// Test bootloader combination
|
||||||
@ -362,6 +369,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_calibration_combination() {
|
fn test_calibration_combination() {
|
||||||
|
// Calibration combo should generate the start-calibration action.
|
||||||
let mut manager = ButtonManager::new();
|
let mut manager = ButtonManager::new();
|
||||||
|
|
||||||
// Test calibration combination
|
// Test calibration combination
|
||||||
@ -375,6 +383,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_throttle_hold_center() {
|
fn test_throttle_hold_center() {
|
||||||
|
// Throttle hold combo should capture the centered axis value when centered.
|
||||||
let mut manager = ButtonManager::new();
|
let mut manager = ButtonManager::new();
|
||||||
manager.buttons[TH_BUTTON].pressed = true;
|
manager.buttons[TH_BUTTON].pressed = true;
|
||||||
manager.buttons[TH_BUTTON].previous_pressed = false;
|
manager.buttons[TH_BUTTON].previous_pressed = false;
|
||||||
@ -385,6 +394,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_throttle_hold_value() {
|
fn test_throttle_hold_value() {
|
||||||
|
// Off-center throttle hold should capture the live axis value for hold.
|
||||||
let mut manager = ButtonManager::new();
|
let mut manager = ButtonManager::new();
|
||||||
manager.buttons[TH_BUTTON].pressed = true;
|
manager.buttons[TH_BUTTON].pressed = true;
|
||||||
manager.buttons[TH_BUTTON].previous_pressed = false;
|
manager.buttons[TH_BUTTON].previous_pressed = false;
|
||||||
@ -396,6 +406,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_virtual_throttle_toggle() {
|
fn test_virtual_throttle_toggle() {
|
||||||
|
// Virtual throttle button should emit the toggle action when pressed.
|
||||||
let mut manager = ButtonManager::new();
|
let mut manager = ButtonManager::new();
|
||||||
manager.buttons[VT_BUTTON].pressed = true;
|
manager.buttons[VT_BUTTON].pressed = true;
|
||||||
manager.buttons[VT_BUTTON].previous_pressed = false;
|
manager.buttons[VT_BUTTON].previous_pressed = false;
|
||||||
@ -406,6 +417,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_hat_switch_filtering_left() {
|
fn test_hat_switch_filtering_left() {
|
||||||
|
// Left hat should clear center and conflicting directions when multiple inputs are active.
|
||||||
let mut manager = ButtonManager::new();
|
let mut manager = ButtonManager::new();
|
||||||
|
|
||||||
// Press multiple directional buttons on left hat
|
// Press multiple directional buttons on left hat
|
||||||
@ -424,6 +436,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_hat_switch_filtering_right() {
|
fn test_hat_switch_filtering_right() {
|
||||||
|
// Right hat should behave the same way, disabling conflicts and the center button.
|
||||||
let mut manager = ButtonManager::new();
|
let mut manager = ButtonManager::new();
|
||||||
|
|
||||||
// Press multiple directional buttons on right hat
|
// Press multiple directional buttons on right hat
|
||||||
@ -442,6 +455,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_hat_center_button_filtering() {
|
fn test_hat_center_button_filtering() {
|
||||||
|
// Pressing a direction should suppress the corresponding hat center button.
|
||||||
let mut manager = ButtonManager::new();
|
let mut manager = ButtonManager::new();
|
||||||
|
|
||||||
// Press directional button and center button
|
// Press directional button and center button
|
||||||
@ -458,6 +472,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_hat_switch_single_direction_allowed() {
|
fn test_hat_switch_single_direction_allowed() {
|
||||||
|
// A single direction press must remain active for both hats.
|
||||||
let mut manager = ButtonManager::new();
|
let mut manager = ButtonManager::new();
|
||||||
|
|
||||||
// Press only one directional button on left hat
|
// Press only one directional button on left hat
|
||||||
@ -480,6 +495,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_hat_center_button_works_alone() {
|
fn test_hat_center_button_works_alone() {
|
||||||
|
// When no direction is pressed, the center button should report as pressed.
|
||||||
let mut manager = ButtonManager::new();
|
let mut manager = ButtonManager::new();
|
||||||
|
|
||||||
// Press only center button (no directions)
|
// Press only center button (no directions)
|
||||||
@ -493,6 +509,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_button_press_type_short_press() {
|
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();
|
let mut button = Button::default();
|
||||||
button.usb_button = 1;
|
button.usb_button = 1;
|
||||||
button.enable_long_press = false;
|
button.enable_long_press = false;
|
||||||
@ -513,6 +530,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_button_press_type_long_press() {
|
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();
|
let mut button = Button::default();
|
||||||
button.usb_button = 1;
|
button.usb_button = 1;
|
||||||
button.usb_button_long = 2;
|
button.usb_button_long = 2;
|
||||||
@ -533,6 +551,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_button_press_type_long_press_auto_release_once() {
|
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();
|
let mut button = Button::default();
|
||||||
button.usb_button_long = 2;
|
button.usb_button_long = 2;
|
||||||
button.enable_long_press = true;
|
button.enable_long_press = true;
|
||||||
@ -564,6 +583,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_timer_integration_method_exists() {
|
fn test_timer_integration_method_exists() {
|
||||||
|
// Document that the timer-backed helper stays callable without hardware wiring.
|
||||||
let manager = ButtonManager::new();
|
let manager = ButtonManager::new();
|
||||||
|
|
||||||
// This test verifies the timer integration method signature and basic functionality
|
// 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.
|
/// Reset each axis calibration to its current smoothed center.
|
||||||
fn reset_axis_calibration(
|
fn reset_axis_calibration(
|
||||||
|
// Recenter all axis calibration values using current smoother readings.
|
||||||
&self,
|
&self,
|
||||||
axes: &mut [GimbalAxis; NBR_OF_GIMBAL_AXIS],
|
axes: &mut [GimbalAxis; NBR_OF_GIMBAL_AXIS],
|
||||||
smoothers: &[DynamicSmootherEcoI32; NBR_OF_GIMBAL_AXIS],
|
smoothers: &[DynamicSmootherEcoI32; NBR_OF_GIMBAL_AXIS],
|
||||||
@ -191,6 +192,7 @@ impl CalibrationManager {
|
|||||||
|
|
||||||
impl Default for CalibrationManager {
|
impl Default for CalibrationManager {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
// Construct a calibration manager using the Default implementation.
|
||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -205,6 +207,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_calibration_manager_creation() {
|
fn test_calibration_manager_creation() {
|
||||||
|
// Report whether calibration is currently active.
|
||||||
let manager = CalibrationManager::new();
|
let manager = CalibrationManager::new();
|
||||||
assert!(!manager.is_active());
|
assert!(!manager.is_active());
|
||||||
assert_eq!(manager.get_gimbal_mode(), GIMBAL_MODE_M10);
|
assert_eq!(manager.get_gimbal_mode(), GIMBAL_MODE_M10);
|
||||||
@ -212,6 +215,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_calibration_state_management() {
|
fn test_calibration_state_management() {
|
||||||
|
// Start and stop transitions should flip the active flag accordingly.
|
||||||
let mut manager = CalibrationManager::new();
|
let mut manager = CalibrationManager::new();
|
||||||
|
|
||||||
// Initially inactive
|
// Initially inactive
|
||||||
@ -228,6 +232,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_gimbal_mode_management() {
|
fn test_gimbal_mode_management() {
|
||||||
|
// Manual mode setter should swap between M10 and M7 without side effects.
|
||||||
let mut manager = CalibrationManager::new();
|
let mut manager = CalibrationManager::new();
|
||||||
|
|
||||||
// Default mode
|
// Default mode
|
||||||
@ -244,6 +249,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_dynamic_calibration_inactive() {
|
fn test_dynamic_calibration_inactive() {
|
||||||
|
// Inactive calibration should ignore updates and leave min/max untouched.
|
||||||
let manager = CalibrationManager::new();
|
let manager = CalibrationManager::new();
|
||||||
let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
|
let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
|
||||||
|
|
||||||
@ -271,6 +277,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_dynamic_calibration_active() {
|
fn test_dynamic_calibration_active() {
|
||||||
|
// Active calibration should track new lows/highs as smoothed values change.
|
||||||
let mut manager = CalibrationManager::new();
|
let mut manager = CalibrationManager::new();
|
||||||
manager.start_calibration();
|
manager.start_calibration();
|
||||||
|
|
||||||
@ -319,6 +326,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_mode_selection_inactive() {
|
fn test_mode_selection_inactive() {
|
||||||
|
// Mode change commands should fail when calibration mode is not active.
|
||||||
let mut manager = CalibrationManager::new();
|
let mut manager = CalibrationManager::new();
|
||||||
let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
|
let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
|
||||||
|
|
||||||
@ -341,6 +349,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_load_axis_calibration_success() {
|
fn test_load_axis_calibration_success() {
|
||||||
|
// Successful EEPROM reads should populate the axis calibration tuple.
|
||||||
let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
|
let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
|
||||||
|
|
||||||
// Mock EEPROM data simulating successful calibration read
|
// Mock EEPROM data simulating successful calibration read
|
||||||
@ -373,6 +382,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_load_axis_calibration_failure() {
|
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 mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
|
||||||
let original_axes = axes;
|
let original_axes = axes;
|
||||||
|
|
||||||
@ -391,6 +401,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_load_gimbal_mode_success() {
|
fn test_load_gimbal_mode_success() {
|
||||||
|
// Reading the stored gimbal mode should return the persisted value.
|
||||||
// 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 {
|
||||||
@ -405,6 +416,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_load_gimbal_mode_failure() {
|
fn test_load_gimbal_mode_failure() {
|
||||||
|
// Read failures should fall back to the default M10 mode.
|
||||||
// Mock EEPROM read failure
|
// Mock EEPROM read failure
|
||||||
let mut read_fn = |_addr: u32| Err(());
|
let mut read_fn = |_addr: u32| Err(());
|
||||||
|
|
||||||
@ -414,6 +426,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_update_calibration_inactive() {
|
fn test_update_calibration_inactive() {
|
||||||
|
// When calibration is inactive, mode/set/save helpers should be no-ops.
|
||||||
let mut manager = CalibrationManager::new();
|
let mut manager = CalibrationManager::new();
|
||||||
let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
|
let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
|
||||||
|
|
||||||
@ -442,6 +455,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_process_mode_selection_m10_command() {
|
fn test_process_mode_selection_m10_command() {
|
||||||
|
// Active M10 command should set mode and recenter axes using smoother values.
|
||||||
let mut manager = CalibrationManager::new();
|
let mut manager = CalibrationManager::new();
|
||||||
manager.start_calibration();
|
manager.start_calibration();
|
||||||
let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
|
let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
|
||||||
@ -469,6 +483,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_process_mode_selection_m7_command() {
|
fn test_process_mode_selection_m7_command() {
|
||||||
|
// Active M7 command should likewise set mode and recenter axes.
|
||||||
let mut manager = CalibrationManager::new();
|
let mut manager = CalibrationManager::new();
|
||||||
manager.start_calibration();
|
manager.start_calibration();
|
||||||
let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
|
let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
|
||||||
@ -496,6 +511,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_save_calibration_command() {
|
fn test_save_calibration_command() {
|
||||||
|
// Successful saves should write EEPROM data and exit calibration mode.
|
||||||
let mut manager = CalibrationManager::new();
|
let mut manager = CalibrationManager::new();
|
||||||
manager.start_calibration();
|
manager.start_calibration();
|
||||||
let axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
|
let axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
|
||||||
@ -515,6 +531,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_save_calibration_failure_keeps_active() {
|
fn test_save_calibration_failure_keeps_active() {
|
||||||
|
// Write failures should keep calibration active for retrials.
|
||||||
let mut manager = CalibrationManager::new();
|
let mut manager = CalibrationManager::new();
|
||||||
manager.start_calibration();
|
manager.start_calibration();
|
||||||
let axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
|
let axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
|
||||||
@ -529,6 +546,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_save_calibration_inactive() {
|
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 mut manager = CalibrationManager::new(); // Note: not starting calibration
|
||||||
let axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
|
let axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
|
||||||
|
|
||||||
|
|||||||
@ -79,6 +79,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_generate_expo_lut_boundaries() {
|
fn test_generate_expo_lut_boundaries() {
|
||||||
|
// Ensure expo LUT boundary entries clamp to ADC range.
|
||||||
let lut = generate_expo_lut(0.5);
|
let lut = generate_expo_lut(0.5);
|
||||||
assert_eq!(lut[0], ADC_MIN);
|
assert_eq!(lut[0], ADC_MIN);
|
||||||
assert_eq!(lut[ADC_MAX as usize], ADC_MAX);
|
assert_eq!(lut[ADC_MAX as usize], ADC_MAX);
|
||||||
@ -86,6 +87,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_generate_expo_lut_center_point() {
|
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 lut = generate_expo_lut(0.5);
|
||||||
let center_index = (ADC_MAX / 2) as usize;
|
let center_index = (ADC_MAX / 2) as usize;
|
||||||
let center_value = lut[center_index];
|
let center_value = lut[center_index];
|
||||||
@ -94,6 +96,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_generate_expo_lut_different_factors() {
|
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_linear = generate_expo_lut(0.0);
|
||||||
let lut_expo = generate_expo_lut(1.0);
|
let lut_expo = generate_expo_lut(1.0);
|
||||||
let quarter_point = (ADC_MAX / 4) as usize;
|
let quarter_point = (ADC_MAX / 4) as usize;
|
||||||
@ -102,6 +105,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_apply_expo_curve_no_expo() {
|
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 lut = generate_expo_lut(0.0);
|
||||||
let input_value = 1000u16;
|
let input_value = 1000u16;
|
||||||
let result = apply_expo_curve(input_value, &lut);
|
let result = apply_expo_curve(input_value, &lut);
|
||||||
@ -111,6 +115,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_apply_expo_curve_with_expo() {
|
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_linear = generate_expo_lut(0.0);
|
||||||
let lut_expo = generate_expo_lut(0.5);
|
let lut_expo = generate_expo_lut(0.5);
|
||||||
let test_value = 1000u16;
|
let test_value = 1000u16;
|
||||||
@ -123,6 +128,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_apply_expo_curve_center_unchanged() {
|
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 lut = generate_expo_lut(0.5);
|
||||||
let result = apply_expo_curve(AXIS_CENTER, &lut);
|
let result = apply_expo_curve(AXIS_CENTER, &lut);
|
||||||
// Center point should remain close to center
|
// Center point should remain close to center
|
||||||
@ -131,21 +137,25 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_constrain_within_range() {
|
fn test_constrain_within_range() {
|
||||||
|
// Values inside limits should be returned unchanged by constrain.
|
||||||
assert_eq!(constrain(50u16, 0u16, 100u16), 50u16);
|
assert_eq!(constrain(50u16, 0u16, 100u16), 50u16);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_constrain_above_range() {
|
fn test_constrain_above_range() {
|
||||||
|
// Values above the upper bound should clamp to the max.
|
||||||
assert_eq!(constrain(150u16, 0u16, 100u16), 100u16);
|
assert_eq!(constrain(150u16, 0u16, 100u16), 100u16);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_constrain_below_range() {
|
fn test_constrain_below_range() {
|
||||||
|
// Values below the lower bound should clamp to the min.
|
||||||
assert_eq!(constrain(0u16, 50u16, 100u16), 50u16);
|
assert_eq!(constrain(0u16, 50u16, 100u16), 50u16);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_expo_integration_real_world_values() {
|
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 lut = generate_expo_lut(0.3);
|
||||||
let test_values = [500u16, 1000u16, 2000u16, 3000u16];
|
let test_values = [500u16, 1000u16, 2000u16, 3000u16];
|
||||||
|
|
||||||
|
|||||||
@ -43,6 +43,7 @@ impl UsbState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_poll(&mut self) {
|
pub fn on_poll(&mut self) {
|
||||||
|
// Called on every USB poll; mark device active and wake from idle.
|
||||||
if !self.initialized {
|
if !self.initialized {
|
||||||
self.initialized = true;
|
self.initialized = true;
|
||||||
}
|
}
|
||||||
@ -53,6 +54,7 @@ impl UsbState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn mark_activity(&mut self) {
|
pub fn mark_activity(&mut self) {
|
||||||
|
// Flag that input activity occurred and reset idle counters.
|
||||||
self.activity = true;
|
self.activity = true;
|
||||||
self.activity_elapsed_ms = 0;
|
self.activity_elapsed_ms = 0;
|
||||||
self.idle_mode = false;
|
self.idle_mode = false;
|
||||||
@ -60,6 +62,7 @@ impl UsbState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_input_activity(&mut self) {
|
pub fn handle_input_activity(&mut self) {
|
||||||
|
// Treat input changes as activity and request wake from suspend.
|
||||||
self.mark_activity();
|
self.mark_activity();
|
||||||
if self.suspended && self.wake_on_input {
|
if self.suspended && self.wake_on_input {
|
||||||
self.wake_on_input = false;
|
self.wake_on_input = false;
|
||||||
@ -67,6 +70,7 @@ impl UsbState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_suspend_change(&mut self, state: UsbDeviceState) {
|
pub fn on_suspend_change(&mut self, state: UsbDeviceState) {
|
||||||
|
// Update suspend bookkeeping when USB state changes.
|
||||||
let was_suspended = self.suspended;
|
let was_suspended = self.suspended;
|
||||||
self.suspended = state == UsbDeviceState::Suspend;
|
self.suspended = state == UsbDeviceState::Suspend;
|
||||||
|
|
||||||
@ -86,6 +90,7 @@ impl UsbState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn advance_idle_timer(&mut self, interval_ms: u32) {
|
pub fn advance_idle_timer(&mut self, interval_ms: u32) {
|
||||||
|
// Age the activity timer, transitioning to idle after the timeout.
|
||||||
if !self.activity {
|
if !self.activity {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -99,6 +104,7 @@ impl UsbState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn acknowledge_report(&mut self) {
|
pub fn acknowledge_report(&mut self) {
|
||||||
|
// Reset pending state once the host accepts a report.
|
||||||
self.send_pending = false;
|
self.send_pending = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -116,6 +122,7 @@ pub struct JoystickState {
|
|||||||
|
|
||||||
impl JoystickState {
|
impl JoystickState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
// Initialize managers, smoothers, expo curves, and USB state.
|
||||||
Self {
|
Self {
|
||||||
axis_manager: AxisManager::new(),
|
axis_manager: AxisManager::new(),
|
||||||
button_manager: ButtonManager::new(),
|
button_manager: ButtonManager::new(),
|
||||||
@ -132,6 +139,7 @@ impl JoystickState {
|
|||||||
where
|
where
|
||||||
R: FnMut(u32) -> Result<u8, ()>,
|
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);
|
CalibrationManager::load_axis_calibration(&mut self.axis_manager.axes, &mut read_fn);
|
||||||
let gimbal_mode = CalibrationManager::load_gimbal_mode(&mut read_fn);
|
let gimbal_mode = CalibrationManager::load_gimbal_mode(&mut read_fn);
|
||||||
self.axis_manager.set_gimbal_mode(gimbal_mode);
|
self.axis_manager.set_gimbal_mode(gimbal_mode);
|
||||||
@ -149,6 +157,7 @@ impl JoystickState {
|
|||||||
L::Error: Debug,
|
L::Error: Debug,
|
||||||
R::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_from_matrix(matrix);
|
||||||
self.button_manager
|
self.button_manager
|
||||||
.update_extra_buttons(left_button, right_button);
|
.update_extra_buttons(left_button, right_button);
|
||||||
@ -156,10 +165,12 @@ impl JoystickState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn finalize_button_logic(&mut self, timer: &Timer) -> bool {
|
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)
|
self.button_manager.process_button_logic_with_timer(timer)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_special_action(&self) -> SpecialAction {
|
pub fn check_special_action(&self) -> SpecialAction {
|
||||||
|
// Inspect button state for bootloader/calibration/hold command combinations.
|
||||||
self.button_manager.check_special_combinations(
|
self.button_manager.check_special_combinations(
|
||||||
self.axis_manager.get_value_before_hold(),
|
self.axis_manager.get_value_before_hold(),
|
||||||
self.calibration_manager.is_active(),
|
self.calibration_manager.is_active(),
|
||||||
@ -170,6 +181,7 @@ impl JoystickState {
|
|||||||
where
|
where
|
||||||
W: FnMut(u32, &[u8]) -> Result<(), ()>,
|
W: FnMut(u32, &[u8]) -> Result<(), ()>,
|
||||||
{
|
{
|
||||||
|
// Execute the requested special action, updating calibration/throttle state as needed.
|
||||||
match action {
|
match action {
|
||||||
SpecialAction::Bootloader => {}
|
SpecialAction::Bootloader => {}
|
||||||
SpecialAction::StartCalibration => {
|
SpecialAction::StartCalibration => {
|
||||||
|
|||||||
@ -20,6 +20,7 @@ pub static BOOT2_FIRMWARE: [u8; 256] = rp2040_boot2::BOOT_LOADER_W25Q080;
|
|||||||
|
|
||||||
#[rp2040_hal::entry]
|
#[rp2040_hal::entry]
|
||||||
fn main() -> ! {
|
fn main() -> ! {
|
||||||
|
// Firmware entry point initializes hardware before handing off to RTIC.
|
||||||
let BoardParts {
|
let BoardParts {
|
||||||
mut button_matrix,
|
mut button_matrix,
|
||||||
mut status_led,
|
mut status_led,
|
||||||
@ -33,6 +34,7 @@ fn main() -> ! {
|
|||||||
usb_bus,
|
usb_bus,
|
||||||
} = Board::new().into_parts();
|
} = Board::new().into_parts();
|
||||||
|
|
||||||
|
// Build the HID joystick class on the shared USB bus.
|
||||||
let mut usb_hid_joystick = UsbHidClassBuilder::new()
|
let mut usb_hid_joystick = UsbHidClassBuilder::new()
|
||||||
.add_device(JoystickConfig::default())
|
.add_device(JoystickConfig::default())
|
||||||
.build(usb_bus);
|
.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(|_| ());
|
let mut read_fn = |addr: u32| eeprom.read_byte(addr).map_err(|_| ());
|
||||||
state.load_calibration(&mut read_fn);
|
state.load_calibration(&mut read_fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up periodic timers for scanning, status updates, and USB activity.
|
||||||
let mut scan_tick = timer.count_down();
|
let mut scan_tick = timer.count_down();
|
||||||
scan_tick.start(timers::SCAN_INTERVAL_US.micros());
|
scan_tick.start(timers::SCAN_INTERVAL_US.micros());
|
||||||
|
|
||||||
@ -71,7 +75,9 @@ fn main() -> ! {
|
|||||||
|
|
||||||
let mut status_time_ms: u32 = 0;
|
let mut status_time_ms: u32 = 0;
|
||||||
|
|
||||||
|
// Main control loop: service USB, process inputs, and emit reports.
|
||||||
loop {
|
loop {
|
||||||
|
// Service the USB stack and HID class when data is pending.
|
||||||
if usb_dev.poll(&mut [&mut usb_hid_joystick]) {
|
if usb_dev.poll(&mut [&mut usb_hid_joystick]) {
|
||||||
state.usb_state().on_poll();
|
state.usb_state().on_poll();
|
||||||
}
|
}
|
||||||
@ -79,11 +85,13 @@ fn main() -> ! {
|
|||||||
let usb_state = usb_dev.state();
|
let usb_state = usb_dev.state();
|
||||||
state.usb_state().on_suspend_change(usb_state);
|
state.usb_state().on_suspend_change(usb_state);
|
||||||
|
|
||||||
|
// Periodically refresh the status LED animation.
|
||||||
if status_tick.wait().is_ok() {
|
if status_tick.wait().is_ok() {
|
||||||
status_time_ms = status_time_ms.saturating_add(timers::STATUS_LED_INTERVAL_MS);
|
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);
|
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 {
|
let should_scan = if state.usb_state().suspended {
|
||||||
static mut SUSPENDED_SCAN_COUNTER: u8 = 0;
|
static mut SUSPENDED_SCAN_COUNTER: u8 = 0;
|
||||||
unsafe {
|
unsafe {
|
||||||
@ -95,6 +103,7 @@ fn main() -> ! {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if should_scan && scan_tick.wait().is_ok() {
|
if should_scan && scan_tick.wait().is_ok() {
|
||||||
|
// Scan buttons, read analog axes, and update state machines.
|
||||||
button_matrix.scan_matrix(&mut delay);
|
button_matrix.scan_matrix(&mut delay);
|
||||||
|
|
||||||
let mut raw_values = [
|
let mut raw_values = [
|
||||||
@ -111,6 +120,7 @@ fn main() -> ! {
|
|||||||
&mut right_extra_button,
|
&mut right_extra_button,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Evaluate special button combinations (bootloader, calibration, etc.).
|
||||||
let action = state.check_special_action();
|
let action = state.check_special_action();
|
||||||
if matches!(action, SpecialAction::Bootloader) {
|
if matches!(action, SpecialAction::Bootloader) {
|
||||||
if !state.usb_state().suspended {
|
if !state.usb_state().suspended {
|
||||||
@ -132,6 +142,7 @@ fn main() -> ! {
|
|||||||
state.handle_special_action(action, &mut write_page);
|
state.handle_special_action(action, &mut write_page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track calibration extrema and process axis/virtual/button logic.
|
||||||
state.update_calibration_tracking();
|
state.update_calibration_tracking();
|
||||||
|
|
||||||
if state.process_axes() {
|
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();
|
let usb_tick_elapsed = usb_tick.wait().is_ok();
|
||||||
if usb_tick_elapsed {
|
if usb_tick_elapsed {
|
||||||
state
|
state
|
||||||
@ -154,6 +166,7 @@ fn main() -> ! {
|
|||||||
.advance_idle_timer(timers::USB_UPDATE_INTERVAL_MS);
|
.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
|
if state.usb_state().activity
|
||||||
&& (usb_tick_elapsed || state.usb_state().send_pending)
|
&& (usb_tick_elapsed || state.usb_state().send_pending)
|
||||||
&& !state.usb_state().suspended
|
&& !state.usb_state().suspended
|
||||||
|
|||||||
@ -171,6 +171,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn front_buttons_have_expected_mappings() {
|
fn front_buttons_have_expected_mappings() {
|
||||||
|
// Front panel buttons map to the expected USB button ids.
|
||||||
let mut buttons = [Button::default(); TOTAL_BUTTONS];
|
let mut buttons = [Button::default(); TOTAL_BUTTONS];
|
||||||
configure_button_mappings(&mut buttons);
|
configure_button_mappings(&mut buttons);
|
||||||
|
|
||||||
@ -181,6 +182,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn long_press_flags_set_correctly() {
|
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];
|
let mut buttons = [Button::default(); TOTAL_BUTTONS];
|
||||||
configure_button_mappings(&mut buttons);
|
configure_button_mappings(&mut buttons);
|
||||||
|
|
||||||
@ -192,6 +194,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hat_buttons_map_to_expected_ids() {
|
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];
|
let mut buttons = [Button::default(); TOTAL_BUTTONS];
|
||||||
configure_button_mappings(&mut buttons);
|
configure_button_mappings(&mut buttons);
|
||||||
|
|
||||||
|
|||||||
@ -67,6 +67,7 @@ const HEARTBEAT_IDLE_MS: u32 = 3200;
|
|||||||
|
|
||||||
impl LedEffect {
|
impl LedEffect {
|
||||||
fn update_interval_ms(self) -> u32 {
|
fn update_interval_ms(self) -> u32 {
|
||||||
|
// Resolve the LED descriptor for the requested status mode.
|
||||||
match self {
|
match self {
|
||||||
LedEffect::Solid => 0,
|
LedEffect::Solid => 0,
|
||||||
LedEffect::Blink { period_ms } => period_ms / 2,
|
LedEffect::Blink { period_ms } => period_ms / 2,
|
||||||
@ -75,6 +76,7 @@ impl LedEffect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn color_for(self, base: RGB8, elapsed_ms: u32) -> RGB8 {
|
fn color_for(self, base: RGB8, elapsed_ms: u32) -> RGB8 {
|
||||||
|
// Compute the base status mode given system state flags.
|
||||||
match self {
|
match self {
|
||||||
LedEffect::Solid => base,
|
LedEffect::Solid => base,
|
||||||
LedEffect::Blink { period_ms } => {
|
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 {
|
fn scale_color(base: RGB8, brightness: u8) -> RGB8 {
|
||||||
|
// Scale each RGB component proportionally to the requested brightness factor.
|
||||||
let scale = brightness as u16;
|
let scale = brightness as u16;
|
||||||
RGB8 {
|
RGB8 {
|
||||||
r: ((base.r as u16 * scale) / 255) as u8,
|
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 {
|
fn determine_base_mode(system_state: SystemState) -> StatusMode {
|
||||||
|
// Compute the base status mode based on USB/calibration/idle state.
|
||||||
if system_state.usb_suspended {
|
if system_state.usb_suspended {
|
||||||
StatusMode::Suspended
|
StatusMode::Suspended
|
||||||
} else if system_state.calibration_active {
|
} else if system_state.calibration_active {
|
||||||
@ -331,6 +335,7 @@ where
|
|||||||
|
|
||||||
/// Write a single color to the LED.
|
/// Write a single color to the LED.
|
||||||
fn write_color(&mut self, color: RGB8) {
|
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());
|
let _ = self.ws2812_direct.write([color].iter().copied());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -354,6 +359,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn idle_mode_uses_base_color_with_heartbeat() {
|
fn idle_mode_uses_base_color_with_heartbeat() {
|
||||||
|
// Idle state should inherit the base color while enforcing a heartbeat pattern.
|
||||||
let state = SystemState {
|
let state = SystemState {
|
||||||
usb_active: false,
|
usb_active: false,
|
||||||
usb_initialized: true,
|
usb_initialized: true,
|
||||||
@ -376,6 +382,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn power_mode_uses_fast_heartbeat() {
|
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);
|
let descriptor = descriptor_for(StatusMode::Power, StatusMode::Normal);
|
||||||
if let LedEffect::Heartbeat { period_ms } = descriptor.effect {
|
if let LedEffect::Heartbeat { period_ms } = descriptor.effect {
|
||||||
assert_eq!(period_ms, HEARTBEAT_POWER_MS);
|
assert_eq!(period_ms, HEARTBEAT_POWER_MS);
|
||||||
@ -387,6 +394,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn calibration_has_priority_over_idle() {
|
fn calibration_has_priority_over_idle() {
|
||||||
|
// Calibration activity should override idle when both flags are set.
|
||||||
let state = SystemState {
|
let state = SystemState {
|
||||||
usb_active: true,
|
usb_active: true,
|
||||||
usb_initialized: true,
|
usb_initialized: true,
|
||||||
@ -403,6 +411,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn heartbeat_effect_fades() {
|
fn heartbeat_effect_fades() {
|
||||||
|
// Heartbeat should ramp up and down around the target color.
|
||||||
let base = StatusMode::Normal;
|
let base = StatusMode::Normal;
|
||||||
let descriptor = descriptor_for(StatusMode::Idle, base);
|
let descriptor = descriptor_for(StatusMode::Idle, base);
|
||||||
let LedEffect::Heartbeat { period_ms } = descriptor.effect else {
|
let LedEffect::Heartbeat { period_ms } = descriptor.effect else {
|
||||||
@ -422,6 +431,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn blink_effect_toggles() {
|
fn blink_effect_toggles() {
|
||||||
|
// Blink descriptor should alternate between the color and off state.
|
||||||
let descriptor = descriptor_for(StatusMode::NormalFlash, StatusMode::NormalFlash);
|
let descriptor = descriptor_for(StatusMode::NormalFlash, StatusMode::NormalFlash);
|
||||||
let LedEffect::Blink { period_ms } = descriptor.effect else {
|
let LedEffect::Blink { period_ms } = descriptor.effect else {
|
||||||
panic!("NormalFlash should use blink effect");
|
panic!("NormalFlash should use blink effect");
|
||||||
@ -435,6 +445,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn determine_base_mode_before_usb() {
|
fn determine_base_mode_before_usb() {
|
||||||
|
// Before USB comes up the controller should stay in Power mode.
|
||||||
let state = SystemState {
|
let state = SystemState {
|
||||||
usb_active: false,
|
usb_active: false,
|
||||||
usb_initialized: false,
|
usb_initialized: false,
|
||||||
@ -450,6 +461,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn usb_suspend_takes_priority() {
|
fn usb_suspend_takes_priority() {
|
||||||
|
// USB suspend should trump other status priorities.
|
||||||
let state = SystemState {
|
let state = SystemState {
|
||||||
usb_active: true,
|
usb_active: true,
|
||||||
usb_initialized: 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.
|
/// Read a u16 value from EEPROM in little‑endian (low then high byte) format.
|
||||||
fn read_u16_with_closure(
|
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, ()>,
|
read_byte_fn: &mut dyn FnMut(u32) -> Result<u8, ()>,
|
||||||
low_addr: u32,
|
low_addr: u32,
|
||||||
high_addr: u32,
|
high_addr: u32,
|
||||||
@ -171,6 +172,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_boundary_values() {
|
fn test_boundary_values() {
|
||||||
|
// Pack axis tuples into EEPROM layout and write the gimbal mode byte.
|
||||||
let mut buffer = [0u8; 4];
|
let mut buffer = [0u8; 4];
|
||||||
|
|
||||||
// Test minimum value (manual packing)
|
// Test minimum value (manual packing)
|
||||||
|
|||||||
@ -37,6 +37,7 @@ pub trait Try {
|
|||||||
type Ok;
|
type Ok;
|
||||||
type Error;
|
type Error;
|
||||||
fn into_result(self) -> Result<Self::Ok, Self::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> {
|
impl<T> Try for Option<T> {
|
||||||
@ -45,6 +46,7 @@ impl<T> Try for Option<T> {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn into_result(self) -> Result<T, NoneError> {
|
fn into_result(self) -> Result<T, NoneError> {
|
||||||
|
// Convert an optional value into a result with a unit error.
|
||||||
self.ok_or(NoneError)
|
self.ok_or(NoneError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -55,6 +57,7 @@ impl<T, E> Try for Result<T, E> {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn into_result(self) -> Self {
|
fn into_result(self) -> Self {
|
||||||
|
// `Result` already matches the desired signature; return it untouched.
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -165,12 +168,16 @@ impl<'a, B: UsbBus> DeviceClass<'a> for Joystick<'a, B> {
|
|||||||
type I = Interface<'a, B, InBytes32, OutNone, ReportSingle>;
|
type I = Interface<'a, B, InBytes32, OutNone, ReportSingle>;
|
||||||
|
|
||||||
fn interface(&mut self) -> &mut Self::I {
|
fn interface(&mut self) -> &mut Self::I {
|
||||||
|
// Expose the HID interface so the USB stack can enqueue reports.
|
||||||
&mut self.interface
|
&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> {
|
fn tick(&mut self) -> Result<(), UsbHidError> {
|
||||||
|
// Flush pending HID data and poll the USB stack for new requests.
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -181,6 +188,7 @@ pub struct JoystickConfig<'a> {
|
|||||||
|
|
||||||
impl Default for JoystickConfig<'_> {
|
impl Default for JoystickConfig<'_> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
// Construct the HID interface with the default joystick descriptor and endpoints.
|
||||||
Self::new(
|
Self::new(
|
||||||
unwrap!(unwrap!(InterfaceBuilder::new(JOYSTICK_DESCRIPTOR))
|
unwrap!(unwrap!(InterfaceBuilder::new(JOYSTICK_DESCRIPTOR))
|
||||||
.boot_device(InterfaceProtocol::None)
|
.boot_device(InterfaceProtocol::None)
|
||||||
@ -203,6 +211,7 @@ impl<'a, B: UsbBus + 'a> UsbAllocatable<'a, B> for JoystickConfig<'a> {
|
|||||||
type Allocated = Joystick<'a, B>;
|
type Allocated = Joystick<'a, B>;
|
||||||
|
|
||||||
fn allocate(self, usb_alloc: &'a UsbBusAllocator<B>) -> Self::Allocated {
|
fn allocate(self, usb_alloc: &'a UsbBusAllocator<B>) -> Self::Allocated {
|
||||||
|
// Allocate the HID interface using the provided USB bus allocator.
|
||||||
Self::Allocated {
|
Self::Allocated {
|
||||||
interface: Interface::new(usb_alloc, self.interface),
|
interface: Interface::new(usb_alloc, self.interface),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -184,6 +184,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_joystick_report_basic_axes() {
|
fn test_joystick_report_basic_axes() {
|
||||||
|
// Remap helper scales values between integer ranges with clamping.
|
||||||
let mut buttons = [Button::default(); TOTAL_BUTTONS];
|
let mut buttons = [Button::default(); TOTAL_BUTTONS];
|
||||||
let mut axes = [GimbalAxis::new(); 4];
|
let mut axes = [GimbalAxis::new(); 4];
|
||||||
|
|
||||||
@ -214,6 +215,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_virtual_throttle_mode() {
|
fn test_virtual_throttle_mode() {
|
||||||
|
// Slider combines throttle hold and virtual throttle for report serialization.
|
||||||
let mut buttons = [Button::default(); TOTAL_BUTTONS];
|
let mut buttons = [Button::default(); TOTAL_BUTTONS];
|
||||||
let mut axes = [GimbalAxis::new(); 4];
|
let mut axes = [GimbalAxis::new(); 4];
|
||||||
|
|
||||||
@ -237,6 +239,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_virtual_throttle_below_center() {
|
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 buttons = [Button::default(); TOTAL_BUTTONS];
|
||||||
let mut axes = [GimbalAxis::new(); 4];
|
let mut axes = [GimbalAxis::new(); 4];
|
||||||
|
|
||||||
@ -261,6 +264,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_button_mapping_regular_buttons() {
|
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 buttons = [Button::default(); TOTAL_BUTTONS];
|
||||||
let mut axes = [GimbalAxis::new(); 4];
|
let mut axes = [GimbalAxis::new(); 4];
|
||||||
|
|
||||||
@ -285,6 +289,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_hat_switch_mapping() {
|
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 buttons = [Button::default(); TOTAL_BUTTONS];
|
||||||
let mut axes = [GimbalAxis::new(); 4];
|
let mut axes = [GimbalAxis::new(); 4];
|
||||||
|
|
||||||
@ -307,6 +312,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_long_press_button_handling() {
|
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 buttons = [Button::default(); TOTAL_BUTTONS];
|
||||||
let mut axes = [GimbalAxis::new(); 4];
|
let mut axes = [GimbalAxis::new(); 4];
|
||||||
|
|
||||||
@ -327,6 +333,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_usb_changed_flag_reset() {
|
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 buttons = [Button::default(); TOTAL_BUTTONS];
|
||||||
let mut axes = [GimbalAxis::new(); 4];
|
let mut axes = [GimbalAxis::new(); 4];
|
||||||
|
|
||||||
@ -347,6 +354,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_edge_case_hat_values() {
|
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 buttons = [Button::default(); TOTAL_BUTTONS];
|
||||||
let mut axes = [GimbalAxis::new(); 4];
|
let mut axes = [GimbalAxis::new(); 4];
|
||||||
|
|
||||||
@ -368,6 +376,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_multiple_buttons_and_hat() {
|
fn test_multiple_buttons_and_hat() {
|
||||||
|
// Report should accommodate simultaneous button presses and hat direction.
|
||||||
let mut buttons = [Button::default(); TOTAL_BUTTONS];
|
let mut buttons = [Button::default(); TOTAL_BUTTONS];
|
||||||
let mut axes = [GimbalAxis::new(); 4];
|
let mut axes = [GimbalAxis::new(); 4];
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user