diff --git a/Cargo.lock b/Cargo.lock index c607e2e..004980d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -318,6 +318,7 @@ dependencies = [ "embedded-graphics", "embedded-graphics-core", "embedded-hal 1.0.0", + "embedded-hal-async", "embedded-hal-bus", "embedded-io 0.7.1", "esp-alloc", diff --git a/Cargo.toml b/Cargo.toml index c4d40b4..626171a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ pn532 = "0.5.0" ndef = "0.5.0" num_enum = { version = "0.7.6", default-features = false } embassy-sync = "0.8.0" +embedded-hal-async = "1.0.0" i2c-character-display = "0.5.1" # For fine tuning these settings, please refer to https://doc.rust-lang.org/cargo/reference/profiles.html diff --git a/src/bin/main.rs b/src/bin/main.rs index 56441ea..617b405 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -95,12 +95,12 @@ async fn main(spawner: Spawner) { let button_config = InputConfig::default().with_pull(Pull::Up); let inputs = Inputs { - up: Input::new(peripherals.GPIO38, button_config.clone()), - down: Input::new(peripherals.GPIO39, button_config.clone()), - left: Input::new(peripherals.GPIO40, button_config.clone()), - right: Input::new(peripherals.GPIO41, button_config.clone()), - back: Input::new(peripherals.GPIO42, button_config.clone()), - ok: Input::new(peripherals.GPIO45, button_config.clone()), + up: Input::new(peripherals.GPIO38, button_config), + down: Input::new(peripherals.GPIO39, button_config), + left: Input::new(peripherals.GPIO40, button_config), + right: Input::new(peripherals.GPIO41, button_config), + back: Input::new(peripherals.GPIO42, button_config), + ok: Input::new(peripherals.GPIO45, button_config), }; log::info!("Setup complete, entering main loop"); diff --git a/src/navigation/inputs.rs b/src/navigation/inputs.rs index b29a094..d18b1f8 100644 --- a/src/navigation/inputs.rs +++ b/src/navigation/inputs.rs @@ -1,3 +1,7 @@ +use embassy_futures::select::select_array; +use embassy_time::{Duration, Timer}; +use embedded_hal::digital::InputPin; +use embedded_hal_async::digital::Wait; use esp_hal::gpio::Input; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -16,3 +20,62 @@ pub struct Inputs { pub back: Input<'static>, pub ok: Input<'static>, } + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ButtonAction { + Up = 0, + Down = 1, + Left = 2, + Right = 3, + Ok = 4, + Back = 5, +} + +impl Inputs { + pub async fn wait_for_press(&mut self) -> ButtonAction { + let (_, index) = select_array([ + wait_for_button(&mut self.up), + wait_for_button(&mut self.down), + wait_for_button(&mut self.left), + wait_for_button(&mut self.right), + wait_for_button(&mut self.ok), + wait_for_button(&mut self.back), + ]) + .await; + + match index { + 0 => ButtonAction::Up, + 1 => ButtonAction::Down, + 2 => ButtonAction::Left, + 3 => ButtonAction::Right, + 4 => ButtonAction::Ok, + 5 => ButtonAction::Back, + id => panic!("Unknown button {id} pressed"), + } + } +} + +/// Waits for a button press, filtering out fast transients and mechanical bounce. +async fn wait_for_button

(pin: &mut P) +where + P: Wait + InputPin, +{ + loop { + // 1. Wait for the hardware interrupt (could be a real press or crosstalk) + let _ = pin.wait_for_falling_edge().await; + + // 2. Debounce window: let the noise settle + Timer::after(Duration::from_millis(30)).await; + + // 3. Verify the pin is still held down + // (Adapt unwrap() based on your specific HAL's Error type) + if pin.is_low().unwrap_or(false) { + // OPTIONAL: Wait for the button to be released before returning + // so holding it down doesn't spam triggers. + // let _ = pin.wait_for_rising_edge().await; + // Timer::after(Duration::from_millis(30)).await; + + return; + } + } +} diff --git a/src/navigation/navigation.rs b/src/navigation/navigation.rs index 2dcec57..d72af27 100644 --- a/src/navigation/navigation.rs +++ b/src/navigation/navigation.rs @@ -1,13 +1,12 @@ use crate::card::model::Card; +use crate::navigation::inputs::ButtonAction; use crate::navigation::state::NavigationState; use crate::views::view::View; use crate::{display::primary_lcd::PrimaryLcdDisplay, navigation::inputs::Inputs}; -use embassy_futures::select::{Either, Either6, select}; -use embassy_futures::select::select6; -use embassy_time::{Duration, Timer}; +use embassy_futures::select::{Either, select}; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; - +use embassy_time::{Duration, Timer}; pub type Mutex = embassy_sync::mutex::Mutex; pub type Signal = embassy_sync::signal::Signal; @@ -16,12 +15,7 @@ const DEBOUNCE_DURATION_MILLIS: u64 = 100; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Action { - Up, - Down, - Left, - Right, - Ok, - Back, + Button(ButtonAction), Timer, } @@ -40,39 +34,29 @@ pub trait Navigable { } #[embassy_executor::task] -pub async fn run(mut inputs: Inputs, display: &'static mut PrimaryLcdDisplay<'static>, ) { +pub async fn run(mut inputs: Inputs, display: &'static mut PrimaryLcdDisplay<'static>) { log::info!("starting navigation"); let mut state = NavigationState::new(); state.screens.last().unwrap().display(display).await; loop { - let selection_buttons = select6( - inputs.up.wait_for_low(), - inputs.down.wait_for_low(), - inputs.left.wait_for_low(), - inputs.right.wait_for_low(), - inputs.ok.wait_for_low(), - inputs.back.wait_for_low(), - ); - - let selection = select(selection_buttons, Timer::after(Duration::from_millis(DEBOUNCE_DURATION_MILLIS))).await; + let selection = select( + inputs.wait_for_press(), + Timer::after(Duration::from_millis(DEBOUNCE_DURATION_MILLIS)), + ) + .await; let action = match selection { - Either::First(selection_buttons) => match selection_buttons { - Either6::First(_) => Action::Up, - Either6::Second(_) => Action::Down, - Either6::Third(_) => Action::Left, - Either6::Fourth(_) => Action::Right, - Either6::Fifth(_) => Action::Ok, - Either6::Sixth(_) => Action::Back, - }, + Either::First(button) => Action::Button(button), Either::Second(_) => Action::Timer, }; + if Action::Timer != action { log::info!("Action: {:?}", action); + } - let new_state = if action == Action::Back { + let new_state = if action == Action::Button(ButtonAction::Back) { if state.screens.len() > 1 { state.screens.pop(); } diff --git a/src/views/main_menu.rs b/src/views/main_menu.rs index 41470a5..1755d5c 100644 --- a/src/views/main_menu.rs +++ b/src/views/main_menu.rs @@ -1,10 +1,10 @@ use alloc::vec; -use core::future; use crate::card::model::Card; use crate::card::{decoder::split_nfc_hex, mock::*}; use crate::display::primary_lcd::PrimaryLcdDisplay; +use crate::navigation::inputs::ButtonAction; use crate::views::card_view::CardView; use crate::views::journal_view::JournalView; use crate::views::{menu_item, settings_menu::SettingsMenu, view::View}; @@ -37,43 +37,54 @@ impl Navigable for MainMenu { } fn handle_input(&self, input: Action) -> impl core::future::Future + Send { - let card_mock1 = Card::try_from(split_nfc_hex(&MOCK_PAYLOAD3).unwrap()).unwrap(); - let card_mock2 = Card::try_from(split_nfc_hex(&MOCK_PAYLOAD2).unwrap()).unwrap(); - - let new_menu = match input { - Action::Up => { - let new_selected = if (self.selected - 1) < 0 { - MAX_SELECTED - 1 - } else { - self.selected - 1 + async move { + let Action::Button(input) = input else { + // Skip timer inputs + return NewState { + view: View::Main(self.clone()), + replace_view: true, + redraw: false, }; - View::Main(MainMenu { - selected: new_selected, - }) - } - Action::Down => { - let new_selected = self.selected.wrapping_add(1) % MAX_SELECTED; - View::Main(MainMenu { - selected: new_selected, - }) - } - Action::Ok => match self.selected { - 0 => View::Scan(crate::views::scan_menu::ScanMenu { progress: 0 }), - 1 => View::Card(CardView { card: card_mock1 }), - 2 => View::Journal(JournalView { - cards: vec![card_mock1, card_mock2], - selected_card: 0, - }), - 3 => View::Settings(SettingsMenu { selected: 0 }), - _ => View::Main(self.clone()), - }, - _ => View::Main(self.clone()), - }; + }; - future::ready(NewState { - replace_view: matches!(new_menu, View::Main(_)), - view: new_menu, - redraw: !matches!(input, Action::Timer), - }) + let card_mock1 = Card::try_from(split_nfc_hex(&MOCK_PAYLOAD3).unwrap()).unwrap(); + let card_mock2 = Card::try_from(split_nfc_hex(&MOCK_PAYLOAD2).unwrap()).unwrap(); + + let new_menu = match input { + ButtonAction::Up => { + let new_selected = if (self.selected - 1) < 0 { + MAX_SELECTED - 1 + } else { + self.selected - 1 + }; + View::Main(MainMenu { + selected: new_selected, + }) + } + ButtonAction::Down => { + let new_selected = self.selected.wrapping_add(1) % MAX_SELECTED; + View::Main(MainMenu { + selected: new_selected, + }) + } + ButtonAction::Ok => match self.selected { + 0 => View::Scan(crate::views::scan_menu::ScanMenu { progress: 0 }), + 1 => View::Card(CardView { card: card_mock1 }), + 2 => View::Journal(JournalView { + cards: vec![card_mock1, card_mock2], + selected_card: 0, + }), + 3 => View::Settings(SettingsMenu { selected: 0 }), + _ => View::Main(self.clone()), + }, + _ => View::Main(self.clone()), + }; + + NewState { + replace_view: matches!(new_menu, View::Main(_)), + view: new_menu, + redraw: true, + } + } } } diff --git a/src/views/scan_menu.rs b/src/views/scan_menu.rs index 6de643e..725567d 100644 --- a/src/views/scan_menu.rs +++ b/src/views/scan_menu.rs @@ -1,4 +1,5 @@ use crate::display::primary_lcd::PrimaryLcdDisplay; +use crate::navigation::inputs::ButtonAction; use crate::navigation::navigation::CARD_DATA; use crate::navigation::navigation::NewState; @@ -96,7 +97,7 @@ impl Navigable for ScanMenu { } } else { let new_progress = match input { - Action::Up => self.progress.wrapping_add(5), + Action::Button(ButtonAction::Up) => self.progress.wrapping_add(5), _ => self.progress, }; NewState { @@ -104,7 +105,7 @@ impl Navigable for ScanMenu { progress: new_progress, }), replace_view: true, - redraw: matches!(input, Action::Up | Action::Timer), + redraw: matches!(input, Action::Button(ButtonAction::Up) | Action::Timer), } } }