diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..530cc4e --- /dev/null +++ b/Justfile @@ -0,0 +1,30 @@ +set export := true + +default: check + +check: + cd rp2040 && cargo check --target thumbv6m-none-eabi + +test: + cd rp2040 && cargo test --lib --target x86_64-unknown-linux-gnu --features std + +build-uf2: + cd rp2040 && cargo build --release --target thumbv6m-none-eabi + cd rp2040 && cargo objcopy --release --target thumbv6m-none-eabi -- -O binary target/thumbv6m-none-eabi/release/cmdr-joystick-25.bin + cd rp2040 && python3 uf2conv.py target/thumbv6m-none-eabi/release/cmdr-joystick-25.bin --base 0x10000000 --family 0xe48bff56 --convert --output target/firmware.uf2 + +clean: + cargo clean --manifest-path rp2040/Cargo.toml + +flash mount="" timeout="10": + @just build-uf2 + MOUNT="{{mount}}" python3 tools/copy_uf2.py --source rp2040/target/firmware.uf2 --timeout {{timeout}} + +flash-ssh target mount="/Volumes/RPI-RP2" key="" port="22": + @just build-uf2 + target="{{target}}" + mount="{{mount}}" + key_arg="" + if [ -n "{{key}}" ]; then key_arg="-i {{key}}"; fi + ssh $key_arg -p {{port}} "$target" "mkdir -p \"$mount\"" + scp $key_arg -P {{port}} rp2040/target/firmware.uf2 "$target:$mount/" diff --git a/rp2040/src/board.rs b/rp2040/src/board.rs new file mode 100644 index 0000000..81af522 --- /dev/null +++ b/rp2040/src/board.rs @@ -0,0 +1,195 @@ +use crate::button_matrix::{ButtonMatrix, MatrixPins}; +use crate::hardware::{self, BoardPins}; +use crate::status::StatusLed; +use cortex_m::delay::Delay; +use cortex_m::interrupt; +use eeprom24x::{addr_size, page_size, unique_serial, Eeprom24x}; +use rp2040_hal::adc::{Adc, AdcPin}; +use rp2040_hal::clocks::Clock; +use rp2040_hal::gpio::{self, Pin, PullNone}; +use rp2040_hal::i2c::I2C; +use rp2040_hal::pac; +use rp2040_hal::pio::PIOExt; +use rp2040_hal::sio::Sio; +use rp2040_hal::timer::Timer; +use rp2040_hal::watchdog::Watchdog; +use rp2040_hal::{clocks::init_clocks_and_plls, gpio::FunctionSioInput}; +use static_cell::StaticCell; +use usb_device::class_prelude::UsbBusAllocator; + +pub type JoystickMatrix = ButtonMatrix< + MatrixPins<{ hardware::BUTTON_ROWS }, { hardware::BUTTON_COLS }>, + { hardware::BUTTON_ROWS }, + { hardware::BUTTON_COLS }, + { hardware::NUMBER_OF_BUTTONS }, +>; + +pub type JoystickStatusLed = StatusLed; + +type BoardI2c = I2C; +type BoardEeprom = Eeprom24x; + +pub struct AxisAnalogPins { + pub left_x: AdcPin>, + pub left_y: AdcPin>, + pub right_x: AdcPin>, + pub right_y: AdcPin>, +} + +impl AxisAnalogPins { + fn new(inputs: hardware::AxisInputs) -> Self { + 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(); + let right_y = AdcPin::new(inputs.right_y).unwrap(); + Self { + left_x, + left_y, + right_x, + right_y, + } + } +} + +pub struct Board { + button_matrix: JoystickMatrix, + status_led: JoystickStatusLed, + delay: Delay, + timer: Timer, + adc: Adc, + axis_pins: AxisAnalogPins, + left_extra_button: hardware::ExtraButtonPin, + right_extra_button: hardware::ExtraButtonPin, + eeprom: BoardEeprom, + usb_bus: &'static UsbBusAllocator, +} + +pub struct BoardParts { + pub button_matrix: JoystickMatrix, + pub status_led: JoystickStatusLed, + pub delay: Delay, + pub timer: Timer, + pub adc: Adc, + pub axis_pins: AxisAnalogPins, + pub left_extra_button: hardware::ExtraButtonPin, + pub right_extra_button: hardware::ExtraButtonPin, + pub eeprom: BoardEeprom, + pub usb_bus: &'static UsbBusAllocator, +} + +impl Board { + pub fn new() -> Self { + let mut pac = pac::Peripherals::take().unwrap(); + let core = pac::CorePeripherals::take().unwrap(); + + let mut watchdog = Watchdog::new(pac.WATCHDOG); + let clocks = init_clocks_and_plls( + hardware::XTAL_FREQ_HZ, + pac.XOSC, + pac.CLOCKS, + pac.PLL_SYS, + pac.PLL_USB, + &mut pac.RESETS, + &mut watchdog, + ) + .ok() + .unwrap(); + + let sio = Sio::new(pac.SIO); + let raw_pins = gpio::Pins::new( + pac.IO_BANK0, + pac.PADS_BANK0, + sio.gpio_bank0, + &mut pac.RESETS, + ); + let pins = BoardPins::new(raw_pins); + + let matrix_pins = MatrixPins::new(pins.matrix_rows, pins.matrix_cols); + let mut button_matrix = ButtonMatrix::new( + matrix_pins, + hardware::MATRIX_DEBOUNCE_SCANS, + hardware::MIN_PRESS_SPACING_SCANS, + ); + button_matrix.init_pins(); + + let (mut pio, sm0, _, _, _) = pac.PIO0.split(&mut pac.RESETS); + let status_led = StatusLed::new( + pins.status_led, + &mut pio, + sm0, + clocks.peripheral_clock.freq(), + ); + + let timer = Timer::new(pac.TIMER, &mut pac.RESETS, &clocks); + let delay = Delay::new(core.SYST, clocks.system_clock.freq().to_Hz()); + + let i2c = I2C::i2c1( + pac.I2C1, + pins.i2c_sda, + pins.i2c_scl, + hardware::i2c::frequency(), + &mut pac.RESETS, + hardware::i2c::system_clock(), + ); + let eeprom = Eeprom24x::new_24x32(i2c, hardware::i2c::EEPROM_ADDRESS); + + let adc = Adc::new(pac.ADC, &mut pac.RESETS); + let axis_pins = AxisAnalogPins::new(pins.axis_inputs); + + let usb_bus = usb_allocator( + pac.USBCTRL_REGS, + pac.USBCTRL_DPRAM, + clocks.usb_clock, + &mut pac.RESETS, + ); + + Self { + button_matrix, + status_led, + delay, + timer, + adc, + axis_pins, + left_extra_button: pins.left_extra_button, + right_extra_button: pins.right_extra_button, + eeprom, + usb_bus, + } + } + + pub fn into_parts(self) -> BoardParts { + BoardParts { + button_matrix: self.button_matrix, + status_led: self.status_led, + delay: self.delay, + timer: self.timer, + adc: self.adc, + axis_pins: self.axis_pins, + left_extra_button: self.left_extra_button, + right_extra_button: self.right_extra_button, + eeprom: self.eeprom, + usb_bus: self.usb_bus, + } + } +} + +fn usb_allocator( + usbctrl_regs: pac::USBCTRL_REGS, + usbctrl_dpram: pac::USBCTRL_DPRAM, + usb_clock: rp2040_hal::clocks::UsbClock, + resets: &mut pac::RESETS, +) -> &'static UsbBusAllocator { + static USB_BUS: StaticCell> = StaticCell::new(); + + interrupt::free(|_| { + USB_BUS.init_with(|| { + UsbBusAllocator::new(rp2040_hal::usb::UsbBus::new( + usbctrl_regs, + usbctrl_dpram, + usb_clock, + true, + resets, + )) + }) + }) +} diff --git a/rp2040/src/bootloader.rs b/rp2040/src/bootloader.rs new file mode 100644 index 0000000..3aa12b5 --- /dev/null +++ b/rp2040/src/bootloader.rs @@ -0,0 +1,30 @@ +//! Bootloader helpers shared between power-on checks and runtime button chords. + +use crate::status::{StatusLed, StatusMode}; +use cortex_m::asm; +use rp2040_hal::gpio::AnyPin; +use rp2040_hal::pio::{PIOExt, StateMachineIndex}; + +/// Returns `true` when the power-on matrix snapshot requests bootloader entry. +/// +/// The original firmware required the front-left-lower button to be held during +/// power-up to jump straight into ROM boot. +pub fn startup_requested(buttons: &[bool; crate::hardware::NUMBER_OF_BUTTONS]) -> bool { + buttons[crate::mapping::BUTTON_FRONT_LEFT_LOWER] +} + +/// Puts the RP2040 into the ROM bootloader after updating the status LED. +pub fn enter(status_led: &mut StatusLed) -> ! +where + P: PIOExt, + SM: StateMachineIndex, + I: AnyPin, +{ + status_led.update(StatusMode::Bootloader); + let gpio_activity_pin_mask: u32 = 0; + let disable_interface_mask: u32 = 0; + rp2040_hal::rom_data::reset_to_usb_boot(gpio_activity_pin_mask, disable_interface_mask); + loop { + asm::nop(); + } +} diff --git a/rp2040/src/button_matrix.rs b/rp2040/src/button_matrix.rs index c2db171..5dd6cf8 100644 --- a/rp2040/src/button_matrix.rs +++ b/rp2040/src/button_matrix.rs @@ -1,140 +1,166 @@ -//! Button matrix scanner for CMDR Joystick 25 +//! Button matrix scanner for CMDR Joystick 25. //! -//! Scans a row/column matrix and produces a debounced boolean state for each -//! button. Designed for small matrices on microcontrollers where timing is -//! deterministic and GPIO is plentiful. -//! -//! - Rows are configured as inputs with pull‑ups -//! - Columns are configured as push‑pull outputs -//! - Debounce is handled per‑button using a simple counter -//! - A tiny inter‑column delay is inserted to allow signals to settle +//! Mirrors the refactor performed for the keyboard firmware: the matrix owns +//! concrete pins, exposes a small `MatrixPinAccess` trait, and keeps the +//! debouncing + minimum press spacing behaviour identical to the original +//! joystick implementation. -use core::convert::Infallible; use cortex_m::delay::Delay; use embedded_hal::digital::{InputPin, OutputPin}; +use rp2040_hal::gpio::{DynPinId, FunctionSioInput, FunctionSioOutput, Pin, PullNone, PullUp}; -/// Button matrix driver -/// -/// Generics -/// - `R`: number of rows -/// - `C`: number of columns -/// - `N`: total number of buttons (usually `R * C`) -/// -/// Example -/// ```ignore -/// // 4 rows, 6 columns, 24 buttons, 5-scan debounce -/// let mut matrix: ButtonMatrix<4, 6, 24> = ButtonMatrix::new(row_pins, col_pins, 5); -/// matrix.init_pins(); -/// loop { -/// matrix.scan_matrix(&mut delay); -/// let states = matrix.buttons_pressed(); -/// // use `states` -/// } -/// ``` -pub struct ButtonMatrix<'a, const R: usize, const C: usize, const N: usize> { - rows: &'a mut [&'a mut dyn InputPin; R], - cols: &'a mut [&'a mut dyn OutputPin; C], - pressed: [bool; N], - debounce: u8, - debounce_counter: [u8; N], - // Anti-bounce protection: minimum time between same-button presses - last_press_scan: [u32; N], +/// Abstraction over the matrix pins so the scanner can work with either the +/// concrete RP2040 pins or test doubles. +pub trait MatrixPinAccess { + fn init_columns(&mut self); + fn set_column_low(&mut self, column: usize); + fn set_column_high(&mut self, column: usize); + fn read_row(&mut self, row: usize) -> bool; +} + +/// Concrete matrix pins backed by RP2040 GPIO using dynamic pin IDs. +type RowPin = Pin; +type ColPin = Pin; + +pub struct MatrixPins { + rows: [RowPin; ROWS], + cols: [ColPin; COLS], +} + +impl MatrixPins { + pub fn new(rows: [RowPin; ROWS], cols: [ColPin; COLS]) -> Self { + Self { rows, cols } + } +} + +impl MatrixPinAccess for MatrixPins { + fn init_columns(&mut self) { + for column in self.cols.iter_mut() { + let _ = column.set_high(); + } + } + + fn set_column_low(&mut self, column: usize) { + let _ = self.cols[column].set_low(); + } + + fn set_column_high(&mut self, column: usize) { + let _ = self.cols[column].set_high(); + } + + fn read_row(&mut self, row: usize) -> bool { + self.rows[row].is_low().unwrap_or(false) + } +} + +/// Row/column scanned button matrix driver with debounce counters and minimum +/// spacing between subsequent presses of the same key. +pub struct ButtonMatrix +where + P: MatrixPinAccess, +{ + pins: P, + pressed: [bool; BUTTONS], + debounce_threshold: u8, + debounce_counter: [u8; BUTTONS], + last_press_scan: [u32; BUTTONS], + min_press_gap_scans: u32, scan_counter: u32, } -impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C, N> { - /// Creates a new button matrix. - /// - /// Arguments - /// - `rows`: array of row pins (inputs with pull‑ups) - /// - `cols`: array of column pins (push‑pull outputs) - /// - `debounce`: number of consecutive scans a change must persist before it is accepted - pub fn new( - rows: &'a mut [&'a mut dyn InputPin; R], - cols: &'a mut [&'a mut dyn OutputPin; C], - debounce: u8, - ) -> Self { +impl + ButtonMatrix +where + P: MatrixPinAccess, +{ + pub fn new(pins: P, debounce_threshold: u8, min_press_gap_scans: u32) -> Self { + debug_assert_eq!(BUTTONS, ROWS * COLS); Self { - rows, - cols, - pressed: [false; N], - debounce, - debounce_counter: [0; N], - last_press_scan: [0; N], + pins, + pressed: [false; BUTTONS], + debounce_threshold, + debounce_counter: [0; BUTTONS], + last_press_scan: [0; BUTTONS], + min_press_gap_scans, scan_counter: 0, } } - /// Initialize the matrix GPIOs (set all columns high). - /// - /// Call once before the first scan. pub fn init_pins(&mut self) { - for col in self.cols.iter_mut() { - col.set_high().unwrap(); + self.pins.init_columns(); + } + + pub fn prime(&mut self, delay: &mut Delay, passes: usize) { + for _ in 0..passes { + self.scan_matrix(delay); } } - /// Scan the matrix and update each button's debounced state. - /// - /// Call at a fixed cadence. The simple debounce uses a per‑button counter: only - /// when a changed level is observed for `debounce` consecutive scans is the - /// new state committed. - /// - /// Arguments - /// - `delay`: short delay implementation used to let signals settle between columns pub fn scan_matrix(&mut self, delay: &mut Delay) { self.scan_counter = self.scan_counter.wrapping_add(1); - for col_index in 0..self.cols.len() { - self.cols[col_index].set_low().unwrap(); + for column in 0..COLS { + self.pins.set_column_low(column); delay.delay_us(1); - self.process_column(col_index); - self.cols[col_index].set_high().unwrap(); + self.process_column(column); + self.pins.set_column_high(column); delay.delay_us(1); } } - /// Process a single column: drive low, sample rows, update debounce state, then release high. - /// - /// Arguments - /// - `col_index`: index of the column being scanned - fn process_column(&mut self, col_index: usize) { - for row_index in 0..self.rows.len() { - let button_index: usize = col_index + (row_index * C); - let current_state = self.rows[row_index].is_low().unwrap(); + pub fn buttons_pressed(&self) -> [bool; BUTTONS] { + self.pressed + } - if current_state == self.pressed[button_index] { - self.debounce_counter[button_index] = 0; + fn process_column(&mut self, column: usize) { + for row in 0..ROWS { + let index = column + (row * COLS); + let current_state = self.pins.read_row(row); + + if current_state == self.pressed[index] { + self.debounce_counter[index] = 0; continue; } - self.debounce_counter[button_index] += 1; - if self.debounce_counter[button_index] >= self.debounce { - // Anti-bounce protection for press events: minimum 25 scans (5ms) between presses - if current_state { - // Pressing - let scans_since_last = self - .scan_counter - .wrapping_sub(self.last_press_scan[button_index]); - if scans_since_last >= 25 { - // 5ms at 200μs scan rate - self.pressed[button_index] = current_state; - self.last_press_scan[button_index] = self.scan_counter; - } - } else { - // Releasing - self.pressed[button_index] = current_state; + self.debounce_counter[index] = self.debounce_counter[index].saturating_add(1); + if self.debounce_counter[index] < self.debounce_threshold { + continue; + } + + self.debounce_counter[index] = 0; + if current_state { + if self.should_register_press(index) { + self.pressed[index] = true; } - self.debounce_counter[button_index] = 0; + } else { + self.pressed[index] = false; } } } - /// Return a copy of the debounced pressed state for all buttons. - /// - /// For small `N` this copy is cheap. If needed, the API could be extended to - /// return a reference in the future. - pub fn buttons_pressed(&mut self) -> [bool; N] { - self.pressed + fn should_register_press(&mut self, index: usize) -> bool { + 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; + + if can_register { + self.last_press_scan[index] = self.scan_counter; + } + + can_register + } + + #[cfg(all(test, feature = "std"))] + pub(crate) fn process_column_for_test(&mut self, column: usize) { + self.process_column(column); + } + + #[cfg(all(test, feature = "std"))] + pub(crate) fn set_scan_counter(&mut self, value: u32) { + self.scan_counter = value; + } + + #[cfg(all(test, feature = "std"))] + pub(crate) fn bump_scan_counter(&mut self) { + self.scan_counter = self.scan_counter.wrapping_add(1); } } @@ -142,123 +168,65 @@ impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C, mod tests { use super::*; use core::cell::Cell; - use embedded_hal::digital::ErrorType; use std::rc::Rc; - struct MockInputPin { - state: Rc>, + #[derive(Clone)] + struct MockPins { + row_state: Rc>, + column_state: Rc>, } - impl MockInputPin { - fn new(state: Rc>) -> Self { - Self { state } + impl MockPins { + fn new(row_state: Rc>, column_state: Rc>) -> Self { + Self { + row_state, + column_state, + } } } - impl ErrorType for MockInputPin { - type Error = Infallible; - } - - impl InputPin for MockInputPin { - fn is_high(&mut self) -> Result { - Ok(!self.state.get()) + impl MatrixPinAccess<1, 1> for MockPins { + fn init_columns(&mut self) { + self.column_state.set(true); } - fn is_low(&mut self) -> Result { - Ok(self.state.get()) + fn set_column_low(&mut self, _column: usize) { + self.column_state.set(false); + } + + fn set_column_high(&mut self, _column: usize) { + self.column_state.set(true); + } + + fn read_row(&mut self, _row: usize) -> bool { + self.row_state.get() } } - struct MockOutputPin { - state: Rc>, - } - - impl MockOutputPin { - fn new(state: Rc>) -> Self { - Self { state } - } - } - - impl ErrorType for MockOutputPin { - type Error = Infallible; - } - - impl OutputPin for MockOutputPin { - fn set_high(&mut self) -> Result<(), Self::Error> { - self.state.set(true); - Ok(()) - } - - fn set_low(&mut self) -> Result<(), Self::Error> { - self.state.set(false); - Ok(()) - } - } - - fn matrix_fixture() -> ( - ButtonMatrix<'static, 1, 1, 1>, + fn fixture() -> ( + ButtonMatrix, Rc>, Rc>, ) { - let row_state = Rc::new(Cell::new(false)); - let col_state = Rc::new(Cell::new(false)); - - let row_pin: &'static mut dyn InputPin = - Box::leak(Box::new(MockInputPin::new(row_state.clone()))); - let col_pin: &'static mut dyn OutputPin = - Box::leak(Box::new(MockOutputPin::new(col_state.clone()))); - - let rows: &'static mut [&'static mut dyn InputPin; 1] = - Box::leak(Box::new([row_pin])); - let cols: &'static mut [&'static mut dyn OutputPin; 1] = - Box::leak(Box::new([col_pin])); - - let matrix = ButtonMatrix::new(rows, cols, 15); - - (matrix, row_state, col_state) + let row = Rc::new(Cell::new(false)); + let column = Rc::new(Cell::new(true)); + let pins = MockPins::new(row.clone(), column.clone()); + let matrix = ButtonMatrix::new(pins, 2, 3); + (matrix, row, column) } #[test] - fn init_pins_sets_columns_high() { - let (mut matrix, _row_state, col_state) = matrix_fixture(); - assert!(!col_state.get()); - matrix.init_pins(); - assert!(col_state.get()); - } + fn debounce_requires_consecutive_scans() { + let (mut matrix, row, _column) = fixture(); + matrix.set_scan_counter(1); - #[test] - fn process_column_obeys_debounce() { - let (mut matrix, row_state, _col_state) = matrix_fixture(); - let mut states = matrix.buttons_pressed(); - assert!(!states[0]); + row.set(true); + matrix.bump_scan_counter(); + matrix.process_column_for_test(0); + assert!(!matrix.buttons_pressed()[0]); - // Set scan counter to start with enough history - matrix.scan_counter = 100; - - row_state.set(true); - // Need 15 scans to register press - for _ in 0..14 { - matrix.scan_counter = matrix.scan_counter.wrapping_add(1); - matrix.process_column(0); - states = matrix.buttons_pressed(); - assert!(!states[0]); // Still not pressed - } - matrix.scan_counter = matrix.scan_counter.wrapping_add(1); - matrix.process_column(0); // 15th scan - states = matrix.buttons_pressed(); - assert!(states[0]); // Now pressed - - row_state.set(false); - // Need 15 scans to register release - for _ in 0..14 { - matrix.scan_counter = matrix.scan_counter.wrapping_add(1); - matrix.process_column(0); - states = matrix.buttons_pressed(); - assert!(states[0]); // Still pressed - } - matrix.scan_counter = matrix.scan_counter.wrapping_add(1); - matrix.process_column(0); // 15th scan - states = matrix.buttons_pressed(); - assert!(!states[0]); // Now released + matrix.bump_scan_counter(); + matrix.process_column_for_test(0); + assert!(matrix.buttons_pressed()[0]); } } diff --git a/rp2040/src/buttons.rs b/rp2040/src/buttons.rs index 0c79683..03bfc42 100644 --- a/rp2040/src/buttons.rs +++ b/rp2040/src/buttons.rs @@ -8,7 +8,7 @@ //! - Evaluate special combinations (bootloader, calibration, etc.) //! - Expose a compact state consumed by USB report generation -use crate::button_matrix::ButtonMatrix; +use crate::button_matrix::{ButtonMatrix, MatrixPins}; use crate::hardware::{AXIS_CENTER, BUTTON_COLS, BUTTON_ROWS, NUMBER_OF_BUTTONS}; use crate::mapping::*; use embedded_hal::digital::InputPin; @@ -17,6 +17,13 @@ use rp2040_hal::timer::Timer; // Total buttons including the two extra (non‑matrix) buttons pub const TOTAL_BUTTONS: usize = NUMBER_OF_BUTTONS + 2; +pub type JoystickButtonMatrix = ButtonMatrix< + MatrixPins<{ BUTTON_ROWS }, { BUTTON_COLS }>, + { BUTTON_ROWS }, + { BUTTON_COLS }, + { NUMBER_OF_BUTTONS }, +>; + // ==================== BUTTON STRUCT ==================== #[derive(Copy, Clone, Default)] @@ -75,10 +82,7 @@ impl ButtonManager { } /// Update button states from the button matrix snapshot. - pub fn update_from_matrix( - &mut self, - matrix: &mut ButtonMatrix, - ) { + pub fn update_from_matrix(&mut self, matrix: &mut JoystickButtonMatrix) { for (index, key) in matrix.buttons_pressed().iter().enumerate() { self.buttons[index].pressed = *key; } diff --git a/rp2040/src/hardware.rs b/rp2040/src/hardware.rs index 5b6e755..b3ab261 100644 --- a/rp2040/src/hardware.rs +++ b/rp2040/src/hardware.rs @@ -1,105 +1,39 @@ //! Hardware configuration for CMDR Joystick 25 (RP2040) //! -//! Centralizes board constants, GPIO mappings, timing cadences and helper -//! macros to keep hardware details out of business logic. +//! Mirrors the structure introduced for the CMDR Keyboard firmware so that +//! bring-up, pin management, and timing constants follow the same layout. + +use rp2040_hal::gpio::Pins; +use rp2040_hal::gpio::{ + self, DynPinId, FunctionI2C, FunctionPio0, FunctionSioInput, FunctionSioOutput, Pin, PullNone, + PullUp, +}; -// ==================== CRYSTAL AND USB CONSTANTS ==================== /// External crystal frequency (Hz). -pub const XTAL_FREQ_HZ: u32 = 12_000_000u32; -/// USB Vendor ID. +pub const XTAL_FREQ_HZ: u32 = 12_000_000; + +/// USB Vendor ID/Product ID. pub const USB_VID: u16 = 0x1209; -/// USB Product ID. pub const USB_PID: u16 = 0x0002; -// ==================== JOYSTICK CONSTANTS ==================== -/// Button matrix geometry (rows). +/// Button matrix geometry (rows/cols) and count. pub const BUTTON_ROWS: usize = 5; -/// Button matrix geometry (columns). pub const BUTTON_COLS: usize = 5; -/// Total number of matrix buttons. pub const NUMBER_OF_BUTTONS: usize = BUTTON_ROWS * BUTTON_COLS; -/// ADC raw minimum (12‑bit). + +/// ADC characteristics. pub const ADC_MIN: u16 = 0; -/// ADC raw maximum (12‑bit). pub const ADC_MAX: u16 = 4095; -/// Logical axis center. pub const AXIS_CENTER: u16 = (ADC_MIN + ADC_MAX) / 2; -/// Number of physical gimbal axes. pub const NBR_OF_GIMBAL_AXIS: usize = 4; -/// Debounce threshold (in scans) for the matrix. -/// Increased from 10 to 15 scans to prevent double button presses from bounce. -/// At 200μs scan rate: 15 scans = 3ms debounce time. -pub const DEBOUNCE: u8 = 15; -/// Bytes reserved in EEPROM for calibration data + gimbal mode. + +/// Debounce thresholds. +pub const MATRIX_DEBOUNCE_SCANS: u8 = 15; +pub const MIN_PRESS_SPACING_SCANS: u32 = 25; // ~5ms @ 200µs cadence + +/// EEPROM storage length (calibration data + gimbal mode). pub const EEPROM_DATA_LENGTH: usize = 25; -// ==================== GPIO PIN DEFINITIONS ==================== -/// Logical mapping between board functions and GPIO numbers. -pub mod pins { - /// Extra buttons (TX/RX pins) - pub const LEFT_EXTRA_BUTTON_PIN: u8 = 1; - pub const RIGHT_EXTRA_BUTTON_PIN: u8 = 0; - - /// Button matrix row pins - pub const BUTTON_ROW_PIN_0: u8 = 6; - pub const BUTTON_ROW_PIN_1: u8 = 8; - pub const BUTTON_ROW_PIN_2: u8 = 4; - pub const BUTTON_ROW_PIN_3: u8 = 7; - pub const BUTTON_ROW_PIN_4: u8 = 5; - - /// Button matrix column pins - pub const BUTTON_COL_PIN_0: u8 = 9; - pub const BUTTON_COL_PIN_1: u8 = 10; - pub const BUTTON_COL_PIN_2: u8 = 11; - pub const BUTTON_COL_PIN_3: u8 = 12; - pub const BUTTON_COL_PIN_4: u8 = 13; - - /// ADC pins for gimbal axes - pub const ADC_LEFT_X_PIN: u8 = 29; - pub const ADC_LEFT_Y_PIN: u8 = 28; - pub const ADC_RIGHT_X_PIN: u8 = 27; - pub const ADC_RIGHT_Y_PIN: u8 = 26; - - /// Status LED pin - pub const STATUS_LED_PIN: u8 = 16; - - /// I2C pins for EEPROM - pub const I2C_SDA_PIN: u8 = 14; - pub const I2C_SCL_PIN: u8 = 15; -} - -// ==================== I2C CONFIGURATION ==================== -/// I2C frequency and system clock helpers for the EEPROM bus. -pub mod i2c { - use fugit::{Rate, RateExtU32}; - - pub const I2C_FREQUENCY_HZ: u32 = 400_000; - pub fn i2c_frequency() -> Rate { - I2C_FREQUENCY_HZ.Hz() - } - pub const SYSTEM_CLOCK_HZ: u32 = 125_000_000; - pub fn system_clock() -> Rate { - SYSTEM_CLOCK_HZ.Hz() - } -} - -// ==================== TIMER INTERVALS ==================== -/// Cadences for periodic firmware tasks. -pub mod timers { - /// Status LED update interval (ms). - pub const STATUS_LED_INTERVAL_MS: u32 = 40; - - /// Button matrix scan interval (µs). - pub const SCAN_INTERVAL_US: u32 = 200; - - /// USB HID report interval (ms). - pub const USB_UPDATE_INTERVAL_MS: u32 = 1; - - /// USB activity timeout (ms) - stop sending reports after this period of inactivity. - pub const USB_ACTIVITY_TIMEOUT_MS: u32 = 5_000; // 5 seconds -} - -// ==================== USB DEVICE CONFIGURATION ==================== /// USB string descriptors. pub mod usb { pub const MANUFACTURER: &str = "CMtec"; @@ -107,86 +41,155 @@ pub mod usb { pub const SERIAL_NUMBER: &str = "0001"; } -// ==================== PIN ACCESS MACROS ==================== - -/// Macro to access typed GPIO pins using board constants. -/// Avoids scattering raw GPIO numbers; each arm references the constant it maps. -#[macro_export] -macro_rules! get_pin { - ($pins:expr, left_extra_button) => {{ - const _: u8 = $crate::hardware::pins::LEFT_EXTRA_BUTTON_PIN; - $pins.gpio1 - }}; - ($pins:expr, right_extra_button) => {{ - const _: u8 = $crate::hardware::pins::RIGHT_EXTRA_BUTTON_PIN; - $pins.gpio0 - }}; - ($pins:expr, button_row_0) => {{ - const _: u8 = $crate::hardware::pins::BUTTON_ROW_PIN_0; - $pins.gpio6 - }}; - ($pins:expr, button_row_1) => {{ - const _: u8 = $crate::hardware::pins::BUTTON_ROW_PIN_1; - $pins.gpio8 - }}; - ($pins:expr, button_row_2) => {{ - const _: u8 = $crate::hardware::pins::BUTTON_ROW_PIN_2; - $pins.gpio4 - }}; - ($pins:expr, button_row_3) => {{ - const _: u8 = $crate::hardware::pins::BUTTON_ROW_PIN_3; - $pins.gpio7 - }}; - ($pins:expr, button_row_4) => {{ - const _: u8 = $crate::hardware::pins::BUTTON_ROW_PIN_4; - $pins.gpio5 - }}; - ($pins:expr, button_col_0) => {{ - const _: u8 = $crate::hardware::pins::BUTTON_COL_PIN_0; - $pins.gpio9 - }}; - ($pins:expr, button_col_1) => {{ - const _: u8 = $crate::hardware::pins::BUTTON_COL_PIN_1; - $pins.gpio10 - }}; - ($pins:expr, button_col_2) => {{ - const _: u8 = $crate::hardware::pins::BUTTON_COL_PIN_2; - $pins.gpio11 - }}; - ($pins:expr, button_col_3) => {{ - const _: u8 = $crate::hardware::pins::BUTTON_COL_PIN_3; - $pins.gpio12 - }}; - ($pins:expr, button_col_4) => {{ - const _: u8 = $crate::hardware::pins::BUTTON_COL_PIN_4; - $pins.gpio13 - }}; - ($pins:expr, adc_left_x) => {{ - const _: u8 = $crate::hardware::pins::ADC_LEFT_X_PIN; - $pins.gpio29 - }}; - ($pins:expr, adc_left_y) => {{ - const _: u8 = $crate::hardware::pins::ADC_LEFT_Y_PIN; - $pins.gpio28 - }}; - ($pins:expr, adc_right_x) => {{ - const _: u8 = $crate::hardware::pins::ADC_RIGHT_X_PIN; - $pins.gpio27 - }}; - ($pins:expr, adc_right_y) => {{ - const _: u8 = $crate::hardware::pins::ADC_RIGHT_Y_PIN; - $pins.gpio26 - }}; - ($pins:expr, status_led) => {{ - const _: u8 = $crate::hardware::pins::STATUS_LED_PIN; - $pins.gpio16 - }}; - ($pins:expr, i2c_sda) => {{ - const _: u8 = $crate::hardware::pins::I2C_SDA_PIN; - $pins.gpio14 - }}; - ($pins:expr, i2c_scl) => {{ - const _: u8 = $crate::hardware::pins::I2C_SCL_PIN; - $pins.gpio15 - }}; +/// Timing cadences. +pub mod timers { + pub const STATUS_LED_INTERVAL_MS: u32 = 40; + pub const SCAN_INTERVAL_US: u32 = 200; + pub const USB_UPDATE_INTERVAL_MS: u32 = 1; + pub const USB_ACTIVITY_TIMEOUT_MS: u32 = 5_000; +} + +/// I2C helpers. +pub mod i2c { + use eeprom24x::SlaveAddr; + use fugit::{Rate, RateExtU32}; + + pub const FREQUENCY_HZ: u32 = 400_000; + pub const SYSTEM_CLOCK_HZ: u32 = 125_000_000; + + pub fn frequency() -> Rate { + FREQUENCY_HZ.Hz() + } + + pub fn system_clock() -> Rate { + SYSTEM_CLOCK_HZ.Hz() + } + + pub const EEPROM_ADDRESS: SlaveAddr = SlaveAddr::Alternative(false, false, false); +} + +/// Raw GPIO constants retained for documentation/reference. +pub mod pins { + pub const LEFT_EXTRA_BUTTON: u8 = 1; + pub const RIGHT_EXTRA_BUTTON: u8 = 0; + pub const BUTTON_ROW_0: u8 = 6; + pub const BUTTON_ROW_1: u8 = 8; + pub const BUTTON_ROW_2: u8 = 4; + pub const BUTTON_ROW_3: u8 = 7; + pub const BUTTON_ROW_4: u8 = 5; + pub const BUTTON_COL_0: u8 = 9; + pub const BUTTON_COL_1: u8 = 10; + pub const BUTTON_COL_2: u8 = 11; + pub const BUTTON_COL_3: u8 = 12; + pub const BUTTON_COL_4: u8 = 13; + pub const ADC_LEFT_X: u8 = 29; + pub const ADC_LEFT_Y: u8 = 28; + pub const ADC_RIGHT_X: u8 = 27; + pub const ADC_RIGHT_Y: u8 = 26; + pub const STATUS_LED: u8 = 16; + pub const I2C_SDA: u8 = 14; + pub const I2C_SCL: u8 = 15; +} + +/// Matrix row pins (dynamic to simplify scanning code). +pub type MatrixRowPin = Pin; +/// Matrix column pins (dynamic push-pull outputs). +pub type MatrixColPin = Pin; +/// Extra buttons (pull-up inputs). +pub type ExtraButtonPin = Pin; +/// Status LED pin configured for PIO output. +pub type StatusLedPin = Pin; +/// I2C SDA/SCL pins after reconfiguration. +pub type I2cSdaPin = Pin; +pub type I2cSclPin = Pin; + +/// Analog axis input pins (remain as SIO inputs until wrapped by `AdcPin`). +pub struct AxisInputs { + pub left_x: Pin, + pub left_y: Pin, + pub right_x: Pin, + pub right_y: Pin, +} + +/// Bundle returned by `split_board_pins`. +pub struct BoardPins { + pub matrix_rows: [MatrixRowPin; BUTTON_ROWS], + pub matrix_cols: [MatrixColPin; BUTTON_COLS], + pub left_extra_button: ExtraButtonPin, + pub right_extra_button: ExtraButtonPin, + pub axis_inputs: AxisInputs, + pub status_led: StatusLedPin, + pub i2c_sda: I2cSdaPin, + pub i2c_scl: I2cSclPin, +} + +impl BoardPins { + pub fn new(pins: Pins) -> Self { + let row0 = pins.gpio6.into_pull_up_input().into_dyn_pin(); + let row1 = pins.gpio8.into_pull_up_input().into_dyn_pin(); + let row2 = pins.gpio4.into_pull_up_input().into_dyn_pin(); + let row3 = pins.gpio7.into_pull_up_input().into_dyn_pin(); + let row4 = pins.gpio5.into_pull_up_input().into_dyn_pin(); + + let col0 = pins + .gpio9 + .into_push_pull_output() + .into_pull_type::() + .into_dyn_pin(); + let col1 = pins + .gpio10 + .into_push_pull_output() + .into_pull_type::() + .into_dyn_pin(); + let col2 = pins + .gpio11 + .into_push_pull_output() + .into_pull_type::() + .into_dyn_pin(); + let col3 = pins + .gpio12 + .into_push_pull_output() + .into_pull_type::() + .into_dyn_pin(); + let col4 = pins + .gpio13 + .into_push_pull_output() + .into_pull_type::() + .into_dyn_pin(); + + let left_extra = pins.gpio1.into_pull_up_input().into_dyn_pin(); + let right_extra = pins.gpio0.into_pull_up_input().into_dyn_pin(); + + let axis_inputs = AxisInputs { + left_x: pins.gpio29.into_floating_input(), + left_y: pins.gpio28.into_floating_input(), + right_x: pins.gpio27.into_floating_input(), + right_y: pins.gpio26.into_floating_input(), + }; + + let status_led = pins + .gpio16 + .into_function::() + .into_pull_type::(); + + let i2c_sda = pins + .gpio14 + .into_function::() + .into_pull_type::(); + let i2c_scl = pins + .gpio15 + .into_function::() + .into_pull_type::(); + + Self { + matrix_rows: [row0, row1, row2, row3, row4], + matrix_cols: [col0, col1, col2, col3, col4], + left_extra_button: left_extra, + right_extra_button: right_extra, + axis_inputs, + status_led, + i2c_sda, + i2c_scl, + } + } } diff --git a/rp2040/src/joystick.rs b/rp2040/src/joystick.rs new file mode 100644 index 0000000..ab199bb --- /dev/null +++ b/rp2040/src/joystick.rs @@ -0,0 +1,309 @@ +//! Runtime state for the CMDR Joystick 25. +//! +//! This mirrors the `KeyboardState` abstraction from the keyboard refactor and +//! concentrates axis/button/calibration logic alongside USB bookkeeping. + +use crate::axis::AxisManager; +use crate::buttons::{ButtonManager, SpecialAction}; +use crate::calibration::CalibrationManager; +use crate::expo::ExpoLUT; +use crate::hardware; +use crate::status::SystemState; +use crate::usb_joystick_device::JoystickReport; +use crate::usb_report::get_joystick_report; +use core::fmt::Debug; +use dyn_smooth::DynamicSmootherEcoI32; +use embedded_hal::digital::InputPin; +use rp2040_hal::timer::Timer; +use usb_device::device::UsbDeviceState; + +pub struct UsbState { + pub initialized: bool, + pub active: bool, + pub suspended: bool, + pub send_pending: bool, + pub wake_on_input: bool, + pub idle_mode: bool, + pub activity: bool, + activity_elapsed_ms: u32, +} + +impl UsbState { + pub const fn new() -> Self { + Self { + initialized: false, + active: false, + suspended: false, + send_pending: false, + wake_on_input: false, + idle_mode: false, + activity: false, + activity_elapsed_ms: 0, + } + } + + pub fn on_poll(&mut self) { + if !self.initialized { + self.initialized = true; + } + if !self.active { + self.mark_activity(); + } + self.active = true; + } + + pub fn mark_activity(&mut self) { + self.activity = true; + self.activity_elapsed_ms = 0; + self.idle_mode = false; + self.send_pending = true; + } + + pub fn handle_input_activity(&mut self) { + self.mark_activity(); + if self.suspended && self.wake_on_input { + self.wake_on_input = false; + } + } + + pub fn on_suspend_change(&mut self, state: UsbDeviceState) { + let was_suspended = self.suspended; + self.suspended = state == UsbDeviceState::Suspend; + + match (was_suspended, self.suspended) { + (true, false) => { + self.mark_activity(); + self.wake_on_input = false; + } + (false, true) => { + self.idle_mode = true; + self.activity = false; + self.send_pending = false; + self.wake_on_input = true; + } + _ => {} + } + } + + pub fn advance_idle_timer(&mut self, interval_ms: u32) { + if !self.activity { + return; + } + self.activity_elapsed_ms = self.activity_elapsed_ms.saturating_add(interval_ms); + if self.activity_elapsed_ms >= hardware::timers::USB_ACTIVITY_TIMEOUT_MS { + self.activity = false; + self.activity_elapsed_ms = 0; + self.idle_mode = true; + self.send_pending = false; + } + } + + pub fn acknowledge_report(&mut self) { + self.send_pending = false; + } +} + +pub struct JoystickState { + axis_manager: AxisManager, + button_manager: ButtonManager, + calibration_manager: CalibrationManager, + smoother: [DynamicSmootherEcoI32; hardware::NBR_OF_GIMBAL_AXIS], + expo_primary: ExpoLUT, + expo_virtual: ExpoLUT, + vt_enable: bool, + usb: UsbState, +} + +impl JoystickState { + pub fn new() -> Self { + Self { + axis_manager: AxisManager::new(), + button_manager: ButtonManager::new(), + calibration_manager: CalibrationManager::new(), + smoother: AxisManager::create_smoothers(), + expo_primary: ExpoLUT::new(0.3), + expo_virtual: ExpoLUT::new(0.6), + vt_enable: false, + usb: UsbState::new(), + } + } + + pub fn load_calibration(&mut self, mut read_fn: R) + where + R: FnMut(u32) -> Result, + { + 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); + self.calibration_manager.set_gimbal_mode(gimbal_mode); + } + + pub fn update_button_states( + &mut self, + matrix: &mut crate::board::JoystickMatrix, + left_button: &mut L, + right_button: &mut R, + ) where + L: InputPin, + R: InputPin, + L::Error: Debug, + R::Error: Debug, + { + self.button_manager.update_from_matrix(matrix); + self.button_manager + .update_extra_buttons(left_button, right_button); + self.button_manager.filter_hat_switches(); + } + + pub fn finalize_button_logic(&mut self, timer: &Timer) -> bool { + self.button_manager.process_button_logic_with_timer(timer) + } + + pub fn check_special_action(&self) -> SpecialAction { + self.button_manager.check_special_combinations( + self.axis_manager.get_value_before_hold(), + self.calibration_manager.is_active(), + ) + } + + pub fn handle_special_action(&mut self, action: SpecialAction, mut write_page: W) + where + W: FnMut(u32, &[u8]) -> Result<(), ()>, + { + match action { + SpecialAction::Bootloader => {} + SpecialAction::StartCalibration => { + for (index, axis) in self.axis_manager.axes.iter_mut().enumerate() { + let centered = self.smoother[index].value() as u16; + axis.center = centered; + axis.min = centered; + axis.max = centered; + } + self.axis_manager.clear_throttle_hold(); + self.calibration_manager.start_calibration(); + } + SpecialAction::CancelCalibration => { + self.calibration_manager.stop_calibration(); + } + SpecialAction::ThrottleHold(value) => { + self.axis_manager.set_throttle_hold(value); + } + SpecialAction::VirtualThrottleToggle => { + self.vt_enable = !self.vt_enable; + } + SpecialAction::CalibrationSetModeM10 => { + if self + .calibration_manager + .set_gimbal_mode_m10(&mut self.axis_manager.axes, &self.smoother) + { + self.axis_manager + .set_gimbal_mode(self.calibration_manager.get_gimbal_mode()); + self.axis_manager.clear_throttle_hold(); + } + } + SpecialAction::CalibrationSetModeM7 => { + if self + .calibration_manager + .set_gimbal_mode_m7(&mut self.axis_manager.axes, &self.smoother) + { + self.axis_manager + .set_gimbal_mode(self.calibration_manager.get_gimbal_mode()); + self.axis_manager.clear_throttle_hold(); + } + } + SpecialAction::CalibrationSave => { + self.calibration_manager + .save_calibration(&self.axis_manager.axes, &mut |page, data| { + write_page(page, data) + }); + } + SpecialAction::None => {} + } + } + + pub fn update_calibration_tracking(&mut self) { + self.calibration_manager + .update_dynamic_calibration(&mut self.axis_manager.axes, &self.smoother); + } + + pub fn tick_smoothers(&mut self, raw: &mut [u16; hardware::NBR_OF_GIMBAL_AXIS]) { + self.axis_manager.apply_gimbal_compensation(raw); + self.axis_manager.update_smoothers(&mut self.smoother, raw); + } + + pub fn process_axes(&mut self) -> bool { + self.axis_manager + .process_axis_values(&self.smoother, &self.expo_primary) + } + + pub fn update_virtual_axes(&mut self) -> bool { + self.axis_manager + .update_virtual_axes(self.button_manager.buttons(), self.vt_enable) + } + + pub fn vt_enable(&self) -> bool { + self.vt_enable + } + + pub fn usb_state(&mut self) -> &mut UsbState { + &mut self.usb + } + + pub fn axis_manager(&mut self) -> &mut AxisManager { + &mut self.axis_manager + } + + pub fn button_manager(&mut self) -> &mut ButtonManager { + &mut self.button_manager + } + + pub fn expo_virtual(&self) -> &ExpoLUT { + &self.expo_virtual + } + + pub fn smoother(&self) -> &[DynamicSmootherEcoI32; hardware::NBR_OF_GIMBAL_AXIS] { + &self.smoother + } + + pub fn calibration_manager(&self) -> &CalibrationManager { + &self.calibration_manager + } + + pub fn system_state(&self) -> SystemState { + SystemState { + usb_active: self.usb.active, + usb_initialized: self.usb.initialized, + usb_suspended: self.usb.suspended, + idle_mode: self.usb.idle_mode, + calibration_active: self.calibration_manager.is_active(), + throttle_hold_enable: self.axis_manager.throttle_hold_enable, + vt_enable: self.vt_enable, + } + } + + pub fn build_report(&mut self) -> JoystickReport { + let virtual_ry = self.axis_manager.get_virtual_ry_value(&self.expo_virtual); + let virtual_rz = self.axis_manager.get_virtual_rz_value(&self.expo_virtual); + get_joystick_report( + self.button_manager.buttons_mut(), + &mut self.axis_manager.axes, + virtual_ry, + virtual_rz, + &self.vt_enable, + ) + } + + pub fn empty_report() -> JoystickReport { + JoystickReport { + x: 0, + y: 0, + z: 0, + rx: 0, + ry: 0, + rz: 0, + slider: 0, + hat: 8, + buttons: 0, + } + } +} diff --git a/rp2040/src/lib.rs b/rp2040/src/lib.rs index a5164a9..45efd5f 100644 --- a/rp2040/src/lib.rs +++ b/rp2040/src/lib.rs @@ -6,6 +6,13 @@ //! firmware: axis processing, button handling, calibration and storage, USB //! HID reporting, and hardware/status abstractions. +#[cfg(not(feature = "std"))] +pub mod board; +#[cfg(not(feature = "std"))] +pub mod bootloader; +#[cfg(not(feature = "std"))] +pub mod joystick; + /// Axis processing for gimbal and virtual axes (smoothing, expo, holds). pub mod axis; /// Row/column scanned button matrix driver with debouncing. @@ -29,6 +36,11 @@ pub mod usb_joystick_device; /// Convert runtime state into USB HID joystick reports. pub mod usb_report; +#[cfg(not(feature = "std"))] +pub use board::{AxisAnalogPins, Board, BoardParts, JoystickMatrix, JoystickStatusLed}; +#[cfg(not(feature = "std"))] +pub use joystick::{JoystickState, UsbState}; + /// Re-exports for convenient access in `main` and downstream consumers. pub use axis::{AxisManager, GimbalAxis, VirtualAxis}; pub use calibration::CalibrationManager; diff --git a/rp2040/src/main.rs b/rp2040/src/main.rs index fd2a1da..61089b4 100644 --- a/rp2040/src/main.rs +++ b/rp2040/src/main.rs @@ -1,273 +1,44 @@ -//! CMDR Joystick 25 – RP2040 main firmware -//! -//! Overview -//! - 4 gimbal axes (LX, LY, RX, RY) with smoothing, calibration and expo -//! - 2 virtual axes (RY/RZ) driven by buttons with direction compensation -//! - 5x5 button matrix + 2 extra buttons, with debounce and short/long press -//! - USB HID joystick: 7 axes, 32 buttons, 8‑way HAT -//! - EEPROM‑backed calibration and gimbal mode (M10/M7) -//! - WS2812 status LED for state indication -//! -//! Modules -//! - hardware.rs: pins, clocks, timers, helpers -//! - axis.rs: gimbal/virtual axis processing and throttle hold -//! - button_matrix.rs + buttons.rs: scanning, debouncing, press types, special actions -//! - calibration.rs + storage.rs: runtime calibration and persistence -//! - usb_report.rs + usb_joystick_device.rs: HID descriptor and report generation -//! - status.rs: WS2812 driver and status model -//! -//! Modes: Normal, Calibration, Throttle Hold, Virtual Throttle, Bootloader -//! -//! Timing: scan 200 µs, process 1200 µs, USB 10 ms, LED 250 ms #![no_std] #![no_main] -mod axis; -mod button_matrix; -mod buttons; -mod calibration; -mod expo; -mod hardware; -mod mapping; -mod status; -mod storage; -mod usb_joystick_device; -mod usb_report; - -use axis::AxisManager; -use button_matrix::ButtonMatrix; -use buttons::{ButtonManager, SpecialAction}; -use calibration::CalibrationManager; -use core::convert::Infallible; -use core::panic::PanicInfo; -use cortex_m::delay::Delay; -use eeprom24x::{Eeprom24x, SlaveAddr}; -use embedded_hal::digital::{InputPin, OutputPin}; +use cmdr_joystick_25::buttons::SpecialAction; +use cmdr_joystick_25::hardware::{self, timers}; +use cmdr_joystick_25::status::StatusMode; +use cmdr_joystick_25::usb_joystick_device::JoystickConfig; +use cmdr_joystick_25::{bootloader, Board, BoardParts, JoystickState}; use embedded_hal_0_2::adc::OneShot; use embedded_hal_0_2::timer::CountDown; use fugit::ExtU32; -use hardware::timers; -use mapping::*; -use rp2040_hal::{ - adc::Adc, - adc::AdcPin, - clocks::{init_clocks_and_plls, Clock}, - gpio::Pins, - i2c::I2C, - pac, - pio::PIOExt, - timer::Timer, - watchdog::Watchdog, - Sio, -}; -use status::{StatusLed, StatusMode, SystemState}; -use usb_device::class_prelude::*; -use usb_device::device::UsbDeviceState; +use panic_halt as _; use usb_device::prelude::*; -use usb_joystick_device::JoystickConfig; -use usb_report::get_joystick_report; -use usbd_human_interface_device::prelude::*; +use usbd_human_interface_device::prelude::{UsbHidClassBuilder, UsbHidError}; -#[panic_handler] -fn panic(_info: &PanicInfo) -> ! { - loop {} -} - -/// Boot loader configuration for RP2040 ROM. -/// -/// The linker places this boot block at the start of our program image to help the ROM -/// bootloader initialize our code. This specific boot loader supports W25Q080 flash memory. -#[link_section = ".boot2"] -#[no_mangle] +#[unsafe(link_section = ".boot2")] +#[unsafe(no_mangle)] #[used] pub static BOOT2_FIRMWARE: [u8; 256] = rp2040_boot2::BOOT_LOADER_W25Q080; -use expo::ExpoLUT; - -/// Hardware configuration imports from the hardware abstraction layer. -use hardware::{ADC_MAX, ADC_MIN}; -use hardware::{BUTTON_COLS, BUTTON_ROWS, NUMBER_OF_BUTTONS}; - -/// Additional hardware constants for button debouncing. -use hardware::DEBOUNCE; - -#[cfg(not(test))] #[rp2040_hal::entry] fn main() -> ! { - // Hardware initialization and peripheral setup for joystick operation - - // Acquire exclusive access to RP2040 peripherals - let mut pac = pac::Peripherals::take().unwrap(); - - // Initialize watchdog timer (required for clock configuration) - let mut watchdog = Watchdog::new(pac.WATCHDOG); - - // Configure system clocks and phase-locked loops for stable operation - let clocks = init_clocks_and_plls( - hardware::XTAL_FREQ_HZ, - pac.XOSC, - pac.CLOCKS, - pac.PLL_SYS, - pac.PLL_USB, - &mut pac.RESETS, - &mut watchdog, - ) - .ok() - .unwrap(); - - let core = pac::CorePeripherals::take().unwrap(); - - // Initialize SIO (Single-cycle I/O) for high-performance GPIO operations - let sio = Sio::new(pac.SIO); - - // Configure GPIO pins to their default operational state - let pins = Pins::new( - pac.IO_BANK0, - pac.PADS_BANK0, - sio.gpio_bank0, - &mut pac.RESETS, - ); - - let i2c = I2C::i2c1( - pac.I2C1, - get_pin!(pins, i2c_sda).reconfigure(), // sda - get_pin!(pins, i2c_scl).reconfigure(), // scl - hardware::i2c::i2c_frequency(), - &mut pac.RESETS, - hardware::i2c::system_clock(), - ); - - let i2c_address = SlaveAddr::Alternative(false, false, false); - let mut eeprom = Eeprom24x::new_24x32(i2c, i2c_address); - - // ADC configuration: prepare 12-bit ADC channels for all four gimbal axes - - // Initialize 12-bit ADC with 4 channels for gimbal axes - let mut adc = Adc::new(pac.ADC, &mut pac.RESETS); - - // Configure ADC input pins for 4-axis gimbal (Left X/Y, Right X/Y) - let mut adc_pin_left_x = AdcPin::new(get_pin!(pins, adc_left_x).into_floating_input()).unwrap(); - let mut adc_pin_left_y = AdcPin::new(get_pin!(pins, adc_left_y).into_floating_input()).unwrap(); - let mut adc_pin_right_x = - AdcPin::new(get_pin!(pins, adc_right_x).into_floating_input()).unwrap(); - let mut adc_pin_right_y = - AdcPin::new(get_pin!(pins, adc_right_y).into_floating_input()).unwrap(); - - // # Button Matrix Configuration\n //\n // Configure the 5x5 button matrix using row/column scanning technique.\n // Rows are configured as pull-up inputs, columns as push-pull outputs.\n // This allows scanning 25 buttons with only 10 GPIO pins.\n\n // Configure button matrix row pins (inputs with pull-up resistors) - let button_matrix_row_pins: &mut [&mut dyn InputPin; BUTTON_ROWS] = &mut [ - &mut get_pin!(pins, button_row_0).into_pull_up_input(), - &mut get_pin!(pins, button_row_1).into_pull_up_input(), - &mut get_pin!(pins, button_row_2).into_pull_up_input(), - &mut get_pin!(pins, button_row_3).into_pull_up_input(), - &mut get_pin!(pins, button_row_4).into_pull_up_input(), - ]; - - // Configure button matrix column pins (push-pull outputs for scanning) - let button_matrix_col_pins: &mut [&mut dyn OutputPin; BUTTON_COLS] = &mut [ - &mut get_pin!(pins, button_col_0).into_push_pull_output(), - &mut get_pin!(pins, button_col_1).into_push_pull_output(), - &mut get_pin!(pins, button_col_2).into_push_pull_output(), - &mut get_pin!(pins, button_col_3).into_push_pull_output(), - &mut get_pin!(pins, button_col_4).into_push_pull_output(), - ]; - - // Initialize button matrix scanner with debouncing - let mut button_matrix: ButtonMatrix = - ButtonMatrix::new(button_matrix_row_pins, button_matrix_col_pins, DEBOUNCE); - - // Configure matrix pins for scanning operation - button_matrix.init_pins(); - - // Configure additional buttons outside the matrix (total: 27 buttons) - let mut left_extra_button = get_pin!(pins, left_extra_button).into_pull_up_input(); - let mut right_extra_button = get_pin!(pins, right_extra_button).into_pull_up_input(); - - // Status LED initialization: WS2812 via PIO for runtime status indication - - // Initialize WS2812 status LED using PIO state machine - let (mut pio, sm0, _, _, _) = pac.PIO0.split(&mut pac.RESETS); - let mut status_led = StatusLed::new( - get_pin!(pins, status_led).into_function(), - &mut pio, - sm0, - clocks.peripheral_clock.freq(), - ); - - // Initial LED state (red) indicates system initialization - status_led.update(StatusMode::Error); - - let mut delay = Delay::new(core.SYST, clocks.system_clock.freq().to_Hz()); - - // Bootloader entry check: early matrix scan; hold front‑left‑lower to enter USB mass‑storage bootloader - - // Scan button matrix multiple times to ensure stable debounced readings - for _ in 0..10 { - // Multiple scans ensure debounce algorithm captures stable button states - button_matrix.scan_matrix(&mut delay); - } - if button_matrix.buttons_pressed()[BUTTON_FRONT_LEFT_LOWER] { - status_led.update(StatusMode::Bootloader); - let gpio_activity_pin_mask: u32 = 0; - let disable_interface_mask: u32 = 0; - rp2040_hal::rom_data::reset_to_usb_boot(gpio_activity_pin_mask, disable_interface_mask); - } - - // Timer configuration: cadence for LED updates, scans, processing and USB - - // Initialize hardware timer peripheral - let timer = Timer::new(pac.TIMER, &mut pac.RESETS, &clocks); - - let mut status_led_count_down = timer.count_down(); - status_led_count_down.start(timers::STATUS_LED_INTERVAL_MS.millis()); - - // Removed unused millisecond countdown timer - - let mut scan_count_down = timer.count_down(); - scan_count_down.start(timers::SCAN_INTERVAL_US.micros()); - - let mut usb_update_count_down = timer.count_down(); - usb_update_count_down.start(timers::USB_UPDATE_INTERVAL_MS.millis()); - - let mut usb_activity: bool = false; - let mut usb_active: bool = false; - let mut usb_initialized: bool = false; - let mut usb_suspended: bool = false; - let mut usb_send_pending: bool = false; - let mut vt_enable: bool = false; - let mut idle_mode: bool = false; - let mut usb_activity_timeout_count: u32 = 0; - let mut wake_on_input: bool = false; - - let mut axis_manager = AxisManager::new(); - let mut button_manager = ButtonManager::new(); - let mut calibration_manager = CalibrationManager::new(); - - // Signal processing: expo LUTs and smoothing filters - - // Create exponential curve lookup tables (avoids floating-point math in real-time) - let expo_lut = ExpoLUT::new(0.3); - let expo_lut_virtual = ExpoLUT::new(0.6); - - // Initialize digital smoothing filters for each gimbal axis - let mut smoother = AxisManager::create_smoothers(); - - // USB HID configuration (full‑speed joystick class) - - // Initialize USB bus allocator for RP2040 - let usb_bus = UsbBusAllocator::new(rp2040_hal::usb::UsbBus::new( - pac.USBCTRL_REGS, - pac.USBCTRL_DPRAM, - clocks.usb_clock, - true, - &mut pac.RESETS, - )); + let BoardParts { + mut button_matrix, + mut status_led, + mut delay, + timer, + mut adc, + mut axis_pins, + mut left_extra_button, + mut right_extra_button, + mut eeprom, + usb_bus, + } = Board::new().into_parts(); let mut usb_hid_joystick = UsbHidClassBuilder::new() .add_device(JoystickConfig::default()) - .build(&usb_bus); + .build(usb_bus); let mut usb_dev = - UsbDeviceBuilder::new(&usb_bus, UsbVidPid(hardware::USB_VID, hardware::USB_PID)) + UsbDeviceBuilder::new(usb_bus, UsbVidPid(hardware::USB_VID, hardware::USB_PID)) .strings(&[StringDescriptors::default() .manufacturer(hardware::usb::MANUFACTURER) .product(hardware::usb::PRODUCT) @@ -275,63 +46,45 @@ fn main() -> ! { .unwrap() .build(); - // Calibration data initialization: load axis calibration and gimbal mode from EEPROM + let mut state = JoystickState::new(); + status_led.update(StatusMode::Error); - // Load calibration data from EEPROM using CalibrationManager - let mut read_fn = |addr: u32| eeprom.read_byte(addr).map_err(|_| ()); - CalibrationManager::load_axis_calibration(&mut axis_manager.axes, &mut read_fn); - let gimbal_mode = CalibrationManager::load_gimbal_mode(&mut read_fn); - axis_manager.set_gimbal_mode(gimbal_mode); - calibration_manager.set_gimbal_mode(gimbal_mode); + button_matrix.prime(&mut delay, 10); + let initial_pressed = button_matrix.buttons_pressed(); + if bootloader::startup_requested(&initial_pressed) { + bootloader::enter(&mut status_led); + } + + { + let mut read_fn = |addr: u32| eeprom.read_byte(addr).map_err(|_| ()); + state.load_calibration(&mut read_fn); + } + + let mut scan_tick = timer.count_down(); + scan_tick.start(timers::SCAN_INTERVAL_US.micros()); + + let mut status_tick = timer.count_down(); + status_tick.start(timers::STATUS_LED_INTERVAL_MS.millis()); + + let mut usb_tick = timer.count_down(); + usb_tick.start(timers::USB_UPDATE_INTERVAL_MS.millis()); + + let mut status_time_ms: u32 = 0; loop { - // Main control loop: poll USB, scan inputs, process data, send reports - - // Handle USB device polling and maintain connection state if usb_dev.poll(&mut [&mut usb_hid_joystick]) { - if !usb_initialized { - usb_initialized = true; - } - if !usb_active { - usb_activity = true; // Force initial report - idle_mode = false; - usb_activity_timeout_count = 0; - usb_send_pending = true; - } - usb_active = true; + state.usb_state().on_poll(); } - // Check USB device state for suspend/resume handling let usb_state = usb_dev.state(); - let was_suspended = usb_suspended; - usb_suspended = usb_state == UsbDeviceState::Suspend; + state.usb_state().on_suspend_change(usb_state); - // Handle USB resume transition - if was_suspended && !usb_suspended { - // Device was suspended and is now resumed - usb_activity = true; - idle_mode = false; - usb_activity_timeout_count = 0; - usb_send_pending = true; - wake_on_input = false; + 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); } - // Handle USB suspend transition - if !was_suspended && usb_suspended { - // Device has just been suspended - enter power saving mode - idle_mode = true; - usb_activity = false; - usb_send_pending = false; - wake_on_input = true; - - // Reduce LED update frequency to save power when suspended - // LED will be off anyway (Suspended mode), so slow updates are fine - } - - // Skip high-frequency scanning when suspended to save power - // Only scan periodically to detect wake-up inputs - let should_scan = if usb_suspended { - // When suspended, reduce scan frequency by factor of 10 (every ~2ms instead of 200μs) + let should_scan = if state.usb_state().suspended { static mut SUSPENDED_SCAN_COUNTER: u8 = 0; unsafe { SUSPENDED_SCAN_COUNTER = (SUSPENDED_SCAN_COUNTER + 1) % 10; @@ -341,219 +94,83 @@ fn main() -> ! { true }; - if scan_count_down.wait().is_ok() && should_scan { - // ## High-Frequency Input Sampling (~5 kHz) - // - // Sample all inputs at high frequency for responsive control: - // - Button matrix scanning with debouncing - // - ADC reading from all 4 gimbal axes - // - Digital filtering for noise reduction - - // Scan 5x5 button matrix for input changes + if should_scan && scan_tick.wait().is_ok() { button_matrix.scan_matrix(&mut delay); - // Read raw 12-bit ADC values from all 4 gimbal potentiometers let mut raw_values = [ - adc.read(&mut adc_pin_left_x).unwrap(), - adc.read(&mut adc_pin_left_y).unwrap(), - adc.read(&mut adc_pin_right_x).unwrap(), - adc.read(&mut adc_pin_right_y).unwrap(), + adc.read(&mut axis_pins.left_x).unwrap(), + adc.read(&mut axis_pins.left_y).unwrap(), + adc.read(&mut axis_pins.right_x).unwrap(), + adc.read(&mut axis_pins.right_y).unwrap(), ]; + state.tick_smoothers(&mut raw_values); - // Apply hardware-specific axis compensation (M10/M7 differences) - axis_manager.apply_gimbal_compensation(&mut raw_values); - - // Apply digital smoothing filters to reduce ADC noise and jitter - axis_manager.update_smoothers(&mut smoother, &raw_values); - - // ## Immediate Data Processing (formerly 1000 Hz) - // - // Process all input data right after sampling for minimal latency. - - // Update button states from matrix scan and extra buttons - button_manager.update_from_matrix(&mut button_matrix); - button_manager.update_extra_buttons(&mut left_extra_button, &mut right_extra_button); - button_manager.filter_hat_switches(); - - // Process special button combinations for system control - let action = button_manager.check_special_combinations( - axis_manager.get_value_before_hold(), - calibration_manager.is_active(), + state.update_button_states( + &mut button_matrix, + &mut left_extra_button, + &mut right_extra_button, ); - match action { - SpecialAction::Bootloader => { - status_led.update(StatusMode::Bootloader); - let gpio_activity_pin_mask: u32 = 0; - let disable_interface_mask: u32 = 0; - rp2040_hal::rom_data::reset_to_usb_boot( - gpio_activity_pin_mask, - disable_interface_mask, - ); - } - SpecialAction::StartCalibration => { - for (index, item) in axis_manager.axes.iter_mut().enumerate() { - item.center = smoother[index].value() as u16; - item.min = item.center; - item.max = item.center; - } - axis_manager.clear_throttle_hold(); // Clear throttle hold when cancelling calibration - calibration_manager.start_calibration(); - } - SpecialAction::CancelCalibration => { - calibration_manager.stop_calibration(); - } - SpecialAction::ThrottleHold(hold_value) => { - axis_manager.set_throttle_hold(hold_value); - } - SpecialAction::VirtualThrottleToggle => { - vt_enable = !vt_enable; - } - SpecialAction::CalibrationSetModeM10 => { - // Set gimbal mode to M10 and reset calibration - if calibration_manager.set_gimbal_mode_m10(&mut axis_manager.axes, &smoother) { - axis_manager.set_gimbal_mode(calibration_manager.get_gimbal_mode()); - axis_manager.clear_throttle_hold(); // Clear holds after mode change + + let action = state.check_special_action(); + if matches!(action, SpecialAction::Bootloader) { + if !state.usb_state().suspended { + let clear_report = JoystickState::empty_report(); + for _ in 0..3 { + match usb_hid_joystick.device().write_report(&clear_report) { + Ok(_) => break, + Err(UsbHidError::WouldBlock) => { + let _ = usb_hid_joystick.tick(); + } + Err(_) => break, + } } } - SpecialAction::CalibrationSetModeM7 => { - // Set gimbal mode to M7 and reset calibration - if calibration_manager.set_gimbal_mode_m7(&mut axis_manager.axes, &smoother) { - axis_manager.set_gimbal_mode(calibration_manager.get_gimbal_mode()); - axis_manager.clear_throttle_hold(); // Clear holds after mode change - } - } - SpecialAction::CalibrationSave => { - // Save calibration data and end calibration mode - calibration_manager - .save_calibration(&axis_manager.axes, &mut |page: u32, data: &[u8]| { - eeprom.write_page(page, data).map_err(|_| ()) - }); - } - SpecialAction::None => {} + bootloader::enter(&mut status_led); + } else if !matches!(action, SpecialAction::None) { + let mut write_page = + |page: u32, data: &[u8]| eeprom.write_page(page, data).map_err(|_| ()); + state.handle_special_action(action, &mut write_page); } - // Always update calibration for dynamic min/max tracking when active - calibration_manager.update_dynamic_calibration(&mut axis_manager.axes, &smoother); + state.update_calibration_tracking(); - // Process gimbal axes through calibration, expo curves, and scaling - if axis_manager.process_axis_values(&smoother, &expo_lut) { - usb_activity = true; - usb_activity_timeout_count = 0; // Reset timeout on real input activity - idle_mode = false; - usb_send_pending = true; - - // Wake from USB suspend if input detected - if wake_on_input && usb_suspended { - // TODO: Implement remote wakeup if supported by host - wake_on_input = false; - } + if state.process_axes() { + state.usb_state().handle_input_activity(); } - // Update virtual axes based on front button states - if axis_manager.update_virtual_axes(button_manager.buttons(), vt_enable) { - usb_activity = true; - usb_activity_timeout_count = 0; // Reset timeout on real input activity - idle_mode = false; - usb_send_pending = true; - - // Wake from USB suspend if input detected - if wake_on_input && usb_suspended { - // TODO: Implement remote wakeup if supported by host - wake_on_input = false; - } + if state.update_virtual_axes() { + state.usb_state().handle_input_activity(); } - // Process button logic (press types, timing, USB mapping) - if button_manager.process_button_logic_with_timer(&timer) { - usb_activity = true; - usb_activity_timeout_count = 0; // Reset timeout on real input activity - idle_mode = false; - usb_send_pending = true; - - // Wake from USB suspend if input detected - if wake_on_input && usb_suspended { - // TODO: Implement remote wakeup if supported by host - wake_on_input = false; - } + if state.finalize_button_logic(&timer) { + state.usb_state().handle_input_activity(); } } - if status_led_count_down.wait().is_ok() { - // ## Status LED Updates (100Hz) - // - // Update status LED to reflect current system state: - // - Green: Normal operation with USB connection - // - Blue: Calibration mode active - // - Yellow: Throttle hold or Virtual Throttle enabled - // - Red: Error state or disconnected - // - Purple: Bootloader mode - - let system_state = SystemState { - usb_active, - usb_initialized, - usb_suspended, - idle_mode, - calibration_active: calibration_manager.is_active(), - throttle_hold_enable: axis_manager.throttle_hold_enable, - vt_enable, - }; - status_led.update_from_system_state( - system_state, - (timer.get_counter().ticks() / 1000) as u32, - ); + let usb_tick_elapsed = usb_tick.wait().is_ok(); + if usb_tick_elapsed { + state + .usb_state() + .advance_idle_timer(timers::USB_UPDATE_INTERVAL_MS); } - // ## USB HID Report Transmission (up to 1 kHz) - // - // Transmit USB HID reports only when there is input activity. - // This power-management approach prevents the computer from staying - // awake unnecessarily while maintaining responsive control. - // - // The report includes: - // - All 7 analog axes with proper scaling - // - 32-button bitmask with USB mapping - // - 8-direction HAT switch state - // - Virtual throttle mode handling - // Only transmit USB reports when input activity is detected and not suspended - let usb_tick = usb_update_count_down.wait().is_ok(); - if usb_activity && (usb_tick || usb_send_pending) && !usb_suspended { - let mut send_report = || { - let virtual_ry_value = axis_manager.get_virtual_ry_value(&expo_lut_virtual); - let virtual_rz_value = axis_manager.get_virtual_rz_value(&expo_lut_virtual); - match usb_hid_joystick.device().write_report(&get_joystick_report( - button_manager.buttons_mut(), - &mut axis_manager.axes, - virtual_ry_value, - virtual_rz_value, - &vt_enable, - )) { - Err(UsbHidError::WouldBlock) => {} - Ok(_) => { - usb_send_pending = false; - } - Err(e) => { - status_led.update(StatusMode::Error); - core::panic!("Failed to write joystick report: {:?}", e); - } + if state.usb_state().activity + && (usb_tick_elapsed || state.usb_state().send_pending) + && !state.usb_state().suspended + { + let report = state.build_report(); + match usb_hid_joystick.device().write_report(&report) { + Err(UsbHidError::WouldBlock) => {} + Ok(_) => { + state.usb_state().acknowledge_report(); } - }; - - if usb_tick { - usb_activity_timeout_count += timers::USB_UPDATE_INTERVAL_MS; - if usb_activity_timeout_count >= timers::USB_ACTIVITY_TIMEOUT_MS { - usb_activity = false; - usb_activity_timeout_count = 0; - idle_mode = true; - usb_send_pending = false; - } else { - send_report(); + Err(error) => { + status_led.update(StatusMode::Error); + panic!("Failed to write joystick report: {:?}", error); } - } else { - send_report(); } - } else if usb_tick && usb_active && !usb_suspended { - // Only update idle mode for non-suspended devices - idle_mode = true; + } else if usb_tick_elapsed && state.usb_state().active && !state.usb_state().suspended { + state.usb_state().idle_mode = true; } } } diff --git a/tools/copy_uf2.py b/tools/copy_uf2.py new file mode 100755 index 0000000..d513191 --- /dev/null +++ b/tools/copy_uf2.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Copy a UF2 artifact to a detected RP2040 mass-storage mount.""" + +from __future__ import annotations + +import argparse +import os +import shutil +import sys +import time +from pathlib import Path + +INFO_FILE = "INFO_UF2.TXT" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source", type=Path, required=True, help="Path to the UF2 file to copy") + parser.add_argument("--timeout", type=float, default=10.0, help="Seconds to wait for the mount") + parser.add_argument( + "--mount", + type=str, + default=os.environ.get("MOUNT", ""), + help="Explicit mount point (default: auto-detect)", + ) + return parser.parse_args() + + +def candidate_paths(explicit: str, user: str) -> list[Path]: + paths: list[Path] = [] + if explicit: + paths.append(Path(explicit)) + roots = [ + Path("/Volumes"), + Path("/media"), + Path(f"/media/{user}"), + Path("/run/media"), + Path(f"/run/media/{user}"), + ] + for root in roots: + if not root.exists() or not root.is_dir(): + continue + for child in root.iterdir(): + if child.is_dir(): + paths.append(child) + return paths + + +def choose_mount(explicit: str, user: str) -> Path | None: + candidates = candidate_paths(explicit, user) + if explicit: + path = Path(explicit) + return path if path.exists() and path.is_dir() else None + info_candidates = [path for path in candidates if (path / INFO_FILE).exists()] + if info_candidates: + return info_candidates[0] + for path in candidates: + if path.exists() and path.is_dir(): + return path + return None + + +def main() -> int: + args = parse_args() + source = args.source + if not source.exists(): + print(f"UF2 source file not found: {source}", file=sys.stderr) + return 1 + + explicit_mount = args.mount.strip() + user = os.environ.get("USER", "") + deadline = time.time() + float(args.timeout) + + while time.time() <= deadline: + mount = choose_mount(explicit_mount, user) + if mount is not None: + if not mount.exists() or not mount.is_dir(): + time.sleep(1) + continue + destination = mount / source.name + try: + shutil.copy2(source, destination) + try: + if hasattr(os, "sync"): + os.sync() + except Exception: + pass + time.sleep(0.5) + except Exception as exc: # noqa: BLE001 + print(f"Failed to copy UF2 to {destination}: {exc}", file=sys.stderr) + return 1 + print(f"Copied {source} to {destination}") + return 0 + time.sleep(1) + + if explicit_mount: + print( + f"Mount point '{explicit_mount}' not found within {args.timeout} seconds", + file=sys.stderr, + ) + else: + print( + "Unable to detect RP2040 UF2 mount. Pass one via mount=/path", + file=sys.stderr, + ) + return 1 + + +if __name__ == "__main__": # pragma: no cover + sys.exit(main())