diff --git a/rp2040/src/board.rs b/rp2040/src/board.rs index 0a75d25..01d2df9 100644 --- a/rp2040/src/board.rs +++ b/rp2040/src/board.rs @@ -86,6 +86,7 @@ impl Board { hardware::MATRIX_DEBOUNCE_SCANS_PRESS, hardware::MATRIX_DEBOUNCE_SCANS_RELEASE, hardware::MIN_PRESS_SPACING_SCANS, + hardware::RELEASE_GRACE_PERIOD_SCANS, ); button_matrix.init_pins(); diff --git a/rp2040/src/button_matrix.rs b/rp2040/src/button_matrix.rs index ec17d99..390ec36 100644 --- a/rp2040/src/button_matrix.rs +++ b/rp2040/src/button_matrix.rs @@ -63,8 +63,10 @@ pub struct ButtonMatrix ButtonMatrix @@ -76,6 +78,7 @@ where press_threshold: u8, release_threshold: u8, min_press_gap_scans: u32, + release_grace_period_scans: u32, ) -> Self { debug_assert_eq!(KEYS, ROWS * COLS); Self { @@ -85,8 +88,10 @@ where release_threshold, debounce_counter: [0; KEYS], last_press_scan: [0; KEYS], + last_release_scan: [0; KEYS], scan_counter: 0, min_press_gap_scans, + release_grace_period_scans, } } @@ -133,7 +138,11 @@ where if self.debounce_counter[button_index] >= threshold { self.pressed[button_index] = match current_state { true => self.should_register_press(button_index), - false => false, + false => { + // Track release events for grace period + self.last_release_scan[button_index] = self.scan_counter; + false + } }; self.debounce_counter[button_index] = 0; } @@ -145,9 +154,13 @@ where } fn should_register_press(&mut self, button_index: usize) -> bool { - let elapsed = self.scan_counter.wrapping_sub(self.last_press_scan[button_index]); - let can_register = self.last_press_scan[button_index] == 0 - || elapsed >= self.min_press_gap_scans; + let press_elapsed = self.scan_counter.wrapping_sub(self.last_press_scan[button_index]); + let release_elapsed = self.scan_counter.wrapping_sub(self.last_release_scan[button_index]); + + let can_register = (self.last_press_scan[button_index] == 0 + || press_elapsed >= self.min_press_gap_scans) + && (self.last_release_scan[button_index] == 0 + || release_elapsed >= self.release_grace_period_scans); if can_register { self.last_press_scan[button_index] = self.scan_counter; @@ -211,7 +224,7 @@ mod tests { let row_state = Rc::new(Cell::new(false)); let column_state = Rc::new(Cell::new(false)); let pins = MockMatrixPins::new(row_state.clone(), column_state.clone()); - let matrix = ButtonMatrix::new(pins, 5, 5, 200); + let matrix = ButtonMatrix::new(pins, 5, 5, 200, 100); (matrix, row_state, column_state) } diff --git a/rp2040/src/hardware.rs b/rp2040/src/hardware.rs index 743d898..ef47cb0 100644 --- a/rp2040/src/hardware.rs +++ b/rp2040/src/hardware.rs @@ -7,13 +7,17 @@ use rp2040_hal::gpio::Pins; pub const XTAL_FREQ_HZ: u32 = 12_000_000; /// Debounce scans required before a key state toggles. -/// Increased from 2/3 to 5/5 to prevent double characters from key bounce. -/// At 250μs scan rate: 5 scans = 1.25ms debounce time. -pub const MATRIX_DEBOUNCE_SCANS_PRESS: u8 = 5; -pub const MATRIX_DEBOUNCE_SCANS_RELEASE: u8 = 5; +/// Increased from 5/5 to 8/8 to prevent double characters from key bounce. +/// At 250μs scan rate: 8 scans = 2ms debounce time. +pub const MATRIX_DEBOUNCE_SCANS_PRESS: u8 = 8; +pub const MATRIX_DEBOUNCE_SCANS_RELEASE: u8 = 8; -/// Minimum scans between two press events for the same key (50ms at 250μs scan cadence). -pub const MIN_PRESS_SPACING_SCANS: u32 = 200; +/// Minimum scans between two press events for the same key (75ms at 250μs scan cadence). +pub const MIN_PRESS_SPACING_SCANS: u32 = 300; + +/// Grace period after key release before allowing new press (37.5ms at 250μs scan cadence). +/// This prevents phantom presses from release bounce. +pub const RELEASE_GRACE_PERIOD_SCANS: u32 = 150; /// Initial scan iterations before trusting key state. pub const INITIAL_SCAN_PASSES: usize = 50; diff --git a/rp2040/src/status.rs b/rp2040/src/status.rs index f408896..ae6716b 100644 --- a/rp2040/src/status.rs +++ b/rp2040/src/status.rs @@ -354,7 +354,7 @@ mod tests { #[test] fn idle_mode_backdates_start_time() { // Idle mode should start mid-breath so the LED resumes smoothly. - let now = 10_000; + let now: u32 = 10_000; let expected = now.saturating_sub(BREATH_PERIOD_MS / 2); assert_eq!(mode_start_time(StatusMode::Idle, now), expected); assert_eq!(mode_start_time(StatusMode::Active, now), now); diff --git a/tools/copy_uf2.py b/tools/copy_uf2.py index 2ea3e8f..82cede1 100755 --- a/tools/copy_uf2.py +++ b/tools/copy_uf2.py @@ -41,9 +41,13 @@ def candidate_paths(explicit: str, user: str) -> list[Path]: if not root.exists() or not root.is_dir(): continue # Many systems mount the UF2 volume directly as a child of the root directory. - for child in root.iterdir(): - if child.is_dir(): - paths.append(child) + try: + for child in root.iterdir(): + if child.is_dir(): + paths.append(child) + except PermissionError: + # Skip directories we can't read + continue return paths @@ -53,13 +57,11 @@ def choose_mount(explicit: str, user: str) -> Path | None: # For an explicit mount we only care whether it exists. path = Path(explicit) return path if path.exists() and path.is_dir() else None - # Prefer candidates containing INFO_UF2.TXT, fall back to first existing directory. + # Only accept candidates containing INFO_UF2.TXT (real RP2040 bootloader mounts) 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 + # Don't fall back to random directories - only accept real RP2040 mounts return None @@ -104,9 +106,13 @@ def main() -> int: ) else: print( - "Unable to detect RP2040 UF2 mount. Pass one via mount=/path", + "Unable to detect RP2040 UF2 mount. Make sure device is in bootloader mode:", file=sys.stderr, ) + print("1. Press reset/boot button on rp2040 Zero board", file=sys.stderr) + print("2. Press upper left corner key when connecting USB", file=sys.stderr) + print("3. Press Fn+Fn+Shift+Shift+Ctrl chord", file=sys.stderr) + print("Then try again or pass explicit mount via mount=/run/media/$USER/RPI-RP2", file=sys.stderr) return 1