diff --git a/Cargo.lock b/Cargo.lock index 99be1df..831ec41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -307,6 +307,7 @@ dependencies = [ "display-interface-spi", "embassy-executor", "embassy-futures", + "embassy-sync 0.8.0", "embassy-time", "embedded-graphics", "embedded-graphics-core", diff --git a/Cargo.toml b/Cargo.toml index 9bff070..298d773 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ base64 = { version = "0.22", default-features = false, features = ["alloc"] } pn532 = "0.5.0" ndef = "0.5.0" num_enum = { version = "0.7.6", default-features = false } +embassy-sync = "0.8.0" # For fine tuning these settings, please refer to https://doc.rust-lang.org/cargo/reference/profiles.html [profile.dev] diff --git a/src/bin/main.rs b/src/bin/main.rs index 0f616a6..6da0014 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -8,14 +8,16 @@ #![deny(clippy::large_stack_frames)] use alloc::boxed::Box; +use creaturedex::card::decoder::split_nfc_hex; +use creaturedex::card::model::Card; use creaturedex::display::primary_lcd::{PrimaryLcdDisplay, init_primary_lcd}; use creaturedex::drivers::nfc_pn532::NfcPn532Driver; use creaturedex::navigation::inputs::Inputs; -use creaturedex::navigation::navigation; +use creaturedex::navigation::navigation::{self, CARD_DATA}; +use creaturedex::network::wifi::setup_wifi; use embassy_executor::Spawner; use esp_hal::clock::CpuClock; use esp_hal::gpio::{Input, InputConfig, Pull}; -use esp_hal::time::Duration; use esp_hal::timer::timg::TimerGroup; use log::error; @@ -61,18 +63,16 @@ async fn main(spawner: Spawner) { peripherals.GPIO12, // shared dc ))); - //setup_wifi(peripherals.WIFI, peripherals.FLASH, &spawner).await; + setup_wifi(peripherals.WIFI, peripherals.FLASH, &spawner).await; let mut nfc_driver = NfcPn532Driver::new(peripherals.I2C0, peripherals.GPIO17, peripherals.GPIO18); - let timeout = Duration::from_millis(1000); - if nfc_driver.configure_sam(timeout).is_ok() { + if nfc_driver.configure_sam().await.is_ok() { log::info!("NFC SAM configuration successful"); } else { log::error!("NFC SAM configuration failed"); } - let button_config = InputConfig::default().with_pull(Pull::Up); let inputs = Inputs { up: Input::new(peripherals.GPIO38, button_config.clone()), @@ -85,4 +85,28 @@ async fn main(spawner: Spawner) { log::info!("Setup complete, entering main loop"); spawner.spawn(navigation::run(inputs, display).expect("run task failed")); + spawner.spawn(nfc_driver_task(nfc_driver).expect("nfc driver task failed")); +} + + +#[embassy_executor::task] +async fn nfc_driver_task(mut nfc_driver: NfcPn532Driver<'static>) { + loop { + while CARD_DATA.lock().await.is_none() { + match nfc_driver.read_card_data().await { + Ok(card_data) => { + log::info!("NFC Card Data Read: {:?}", card_data); + let Some(split_data) = split_nfc_hex(&card_data) else { + log::error!("Failed to split NFC card data"); + continue; + }; + CARD_DATA.lock().await.replace(Card::try_from(split_data).unwrap()); + } + Err(_) => { + log::info!("No NFC card found"); + } + } + } + embassy_time::Timer::after(embassy_time::Duration::from_millis(1000)).await; + } } diff --git a/src/card/model.rs b/src/card/model.rs index ea0c11c..5747671 100644 --- a/src/card/model.rs +++ b/src/card/model.rs @@ -235,10 +235,10 @@ pub struct Card { pub sprite: Sprite, } -impl TryFrom> for Card { +impl<'a> TryFrom> for Card { type Error = &'static str; - fn try_from(payload: NfcPayload) -> Result { + fn try_from(payload: NfcPayload<'a>) -> Result { let uuid: String = "empty for now".into(); //str::from_utf8(payload.card_uuid).unwrap().into(); let event = decode_known_event(payload.event_encoding); let cardtype = CardType::from(payload.card_type); diff --git a/src/display/sprite.rs b/src/display/sprite.rs index 7f54d40..a47fe7e 100644 --- a/src/display/sprite.rs +++ b/src/display/sprite.rs @@ -1,6 +1,6 @@ use crate::card::model::Palette; use embedded_graphics::{ - geometry::Size, pixelcolor::Rgb565, pixelcolor::RgbColor, prelude::Point, primitives::Rectangle, + geometry::Size, pixelcolor::Rgb565, prelude::Point, primitives::Rectangle, }; use embedded_graphics_core::draw_target::DrawTarget; diff --git a/src/drivers/nfc_pn532.rs b/src/drivers/nfc_pn532.rs index 831d70b..1b6a789 100644 --- a/src/drivers/nfc_pn532.rs +++ b/src/drivers/nfc_pn532.rs @@ -55,7 +55,7 @@ impl pn532::CountDown for TimerWrapper { } } -pub type Pn532Device<'a> = Pn532>, TimerWrapper, 32>; +pub type Pn532Device<'a> = Pn532>, (), 32>; /// PN532 NFC reader driver. pub struct NfcPn532Driver<'a> { @@ -74,16 +74,15 @@ impl<'a> NfcPn532Driver<'a> { .with_sda(sda) .with_scl(scl); - let pn532 = Pn532::new(I2CInterface { i2c }, TimerWrapper::new()); + let pn532 = Pn532::new_async(I2CInterface { i2c }); Self { pn532 } } - pub fn configure_sam(&mut self, timeout: Duration) -> Result<(), ()> { - if let Err(e) = self.pn532.process( + pub async fn configure_sam(&mut self) -> Result<(), ()> { + if let Err(e) = self.pn532.process_async( &Request::sam_configuration(SAMMode::Normal, false), 0, - timeout, - ) { + ).await { error!("Could not initialize PN532: {e:?}"); Err(()) } else { @@ -92,8 +91,8 @@ impl<'a> NfcPn532Driver<'a> { } } - pub fn poll_target(&mut self, timeout: Duration) -> Result<(), ()> { - match self.pn532.process(&Request::INLIST_ONE_ISO_A_TARGET, 23, timeout) { + pub async fn poll_target(&mut self) -> Result<(), ()> { + match self.pn532.process_async(&Request::INLIST_ONE_ISO_A_TARGET, 23).await { Ok(uid) => { info!("uid = {uid:?}"); Ok(()) @@ -117,21 +116,26 @@ impl<'a> NfcPn532Driver<'a> { } } - pub fn read_card_data(&mut self) -> Result, ()> { - let ms_1000 = Duration::from_millis(1000); - self.poll_target(Duration::from_secs(10))?; + pub async fn read_card_data(&mut self) -> Result, ()> { + self.poll_target().await?; let mut data: Vec = Vec::with_capacity(858); - for i in 0x04..=230 { + for i in 0x0B..=230 { + let mut parse_result = Err(()); while parse_result.is_err() { - let Ok(result) = self.pn532.process(&Request::ntag_read(i), 17, ms_1000) else { + let Ok(result) = self.pn532.process_async(&Request::ntag_read(i), 17).await else { continue; }; parse_result = Self::parse_nfc_page(i, result); } let bytes = parse_result.unwrap(); + let bytes = if i == 0x0B { + &bytes[1..] + } else { + &bytes + }; data.extend(bytes); } diff --git a/src/navigation/navigation.rs b/src/navigation/navigation.rs index 39539dd..794dacf 100644 --- a/src/navigation/navigation.rs +++ b/src/navigation/navigation.rs @@ -1,11 +1,17 @@ +use crate::card::model::Card; use crate::navigation::state::NavigationState; use crate::views::view::View; use crate::{display::primary_lcd::PrimaryLcdDisplay, navigation::inputs::Inputs}; -use embassy_futures::select::Either6; +use embassy_futures::select::{Either, Either6, select}; use embassy_futures::select::select6; use embassy_time::{Duration, Timer}; +use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; + +pub type Mutex = embassy_sync::mutex::Mutex; +pub type Signal = embassy_sync::signal::Signal; +pub static CARD_DATA: Mutex> = Mutex::new(None); const DEBOUNCE_DURATION_MILLIS: u64 = 100; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -16,6 +22,13 @@ pub enum Action { Right, Ok, Back, + Timer, +} + +pub struct NewState { + pub view: View, + pub replace_view: bool, + pub redraw: bool, } pub trait Navigable { @@ -23,52 +36,72 @@ pub trait Navigable { &self, display: &mut PrimaryLcdDisplay<'static>, ) -> impl core::future::Future + Send; - fn handle_input(&self, input: Action) -> impl core::future::Future + Send; + fn handle_input(&self, input: Action) -> impl core::future::Future + Send; } #[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 = select6( + 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(), - ) - .await; + ); + + let selection = select(selection_buttons, Timer::after(Duration::from_millis(DEBOUNCE_DURATION_MILLIS))).await; let action = match selection { - 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(selection_buttons) => match selection_buttons { + Either6::First(_) => Action::Up, + Either6::Second(_) => Action::Down, + Either6::Third(_) => Action::Back, + Either6::Fourth(_) => Action::Ok, + Either6::Fifth(_) => Action::Ok, + Either6::Sixth(_) => Action::Back, + }, + Either::Second(_) => Action::Timer, }; log::info!("Action: {:?}", action); - let screen = match action { - Action::Back => state.back(), - _ => { - state - .screens - .last() - .expect("no screen on stack") - .handle_input(action) - .await + let new_state = if action == Action::Back { + if state.screens.len() > 1 { + state.screens.pop(); } + NewState { + view: state.screens.last().expect("no screen on stack").clone(), + replace_view: false, + redraw: true, + } + } else { + let new_state = state + .screens + .last() + .expect("no screen on stack") + .handle_input(action) + .await; + if action == Action::Timer && !new_state.redraw { + continue; + } + new_state }; - screen.display(display).await; - state.screens.push(screen); + if new_state.replace_view { + state.screens.pop(); + } + state.screens.push(new_state.view); + + if action != Action::Timer || new_state.redraw { + state.screens.last().unwrap().display(display).await; + } Timer::after(Duration::from_millis(DEBOUNCE_DURATION_MILLIS)).await; } diff --git a/src/navigation/state.rs b/src/navigation/state.rs index 5fdcb13..8fad746 100644 --- a/src/navigation/state.rs +++ b/src/navigation/state.rs @@ -20,12 +20,22 @@ impl NavigationState { Self::default() } - pub fn back(&mut self) -> View { - let screen = self.screens.pop().unwrap(); - if self.screens.is_empty() { - log::info!("last screen"); - return screen; + pub fn back(&mut self) -> &View { + match &self.screens.len() { + 0 => { + log::info!("No screens to go back to"); + return self.screens.last().unwrap(); + } + 1 => { + log::info!("Only one screen on stack, cannot go back"); + return self.screens.last().unwrap(); + } + _ => { + log::info!("Going back to previous screen"); + self.screens.pop(); + return self.screens.last().unwrap(); + } + } - self.screens.pop().unwrap() } } diff --git a/src/views/card_view.rs b/src/views/card_view.rs index 505049b..a5440f8 100644 --- a/src/views/card_view.rs +++ b/src/views/card_view.rs @@ -1,4 +1,5 @@ use crate::display::sprite::render_sprite_onto_ili9341; +use crate::navigation::navigation::NewState; use alloc::format; use embedded_graphics::mono_font::MonoTextStyle; use embedded_graphics::mono_font::ascii::FONT_6X10; @@ -99,12 +100,13 @@ impl Navigable for CardView { core::future::ready(()) } - fn handle_input(&self, input: Action) -> impl core::future::Future + Send { - let new_menu = match input { - Action::Ok => View::Card(self.clone()), - _ => View::Card(self.clone()), + fn handle_input(&self, input: Action) -> impl core::future::Future + Send { + let new_state = NewState { + view: View::Card(self.clone()), + replace_view: true, + redraw: !matches!(input, Action::Timer), }; - future::ready(new_menu) + future::ready(new_state) } } diff --git a/src/views/journal_view.rs b/src/views/journal_view.rs index e9b7c37..0438786 100644 --- a/src/views/journal_view.rs +++ b/src/views/journal_view.rs @@ -7,10 +7,7 @@ use embedded_graphics::prelude::RgbColor; use embedded_graphics_core::draw_target::DrawTarget; use crate::{ - card::model::Card, - display::primary_lcd::PrimaryLcdDisplay, - navigation::navigation::{Action, Navigable}, - views::view::View, + card::model::Card, display::primary_lcd::PrimaryLcdDisplay, navigation::navigation::{Action, Navigable, NewState}, views::view::View, }; #[derive(Debug, Clone)] @@ -29,12 +26,13 @@ impl Navigable for JournalView { core::future::ready(()) } - fn handle_input(&self, input: Action) -> impl core::future::Future + Send { - let new_menu = match input { - Action::Ok => View::Journal(self.clone()), - _ => View::Journal(self.clone()), + fn handle_input(&self, input: Action) -> impl core::future::Future + Send { + let new_state = NewState { + view: View::Journal(self.clone()), + replace_view: true, + redraw: !matches!(input, Action::Timer), }; - future::ready(new_menu) + future::ready(new_state) } } diff --git a/src/views/main_menu.rs b/src/views/main_menu.rs index de42ec7..41470a5 100644 --- a/src/views/main_menu.rs +++ b/src/views/main_menu.rs @@ -9,12 +9,12 @@ use crate::views::card_view::CardView; use crate::views::journal_view::JournalView; use crate::views::{menu_item, settings_menu::SettingsMenu, view::View}; -use crate::navigation::navigation::{Action, Navigable}; +use crate::navigation::navigation::{Action, Navigable, NewState}; use embedded_graphics::pixelcolor::Rgb565; use embedded_graphics::prelude::RgbColor; use embedded_graphics_core::draw_target::DrawTarget; -pub const MAX_SELECTED: i32 = 3; +pub const MAX_SELECTED: i32 = 4; #[derive(Debug, Clone)] pub struct MainMenu { @@ -28,14 +28,15 @@ impl Navigable for MainMenu { ) -> impl core::future::Future + Send { display.clear(Rgb565::BLACK).unwrap(); - menu_item::show(display, "Last card", 0, self.selected); - menu_item::show(display, "Journal", 1, self.selected); - menu_item::show(display, "Settings", 2, self.selected); + menu_item::show(display, "Scan card", 0, self.selected); + menu_item::show(display, "Last card", 1, self.selected); + menu_item::show(display, "Journal", 2, self.selected); + menu_item::show(display, "Settings", 3, self.selected); core::future::ready(()) } - fn handle_input(&self, input: Action) -> impl core::future::Future + Send { + 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(); @@ -57,17 +58,22 @@ impl Navigable for MainMenu { }) } Action::Ok => match self.selected { - 0 => View::Card(CardView { card: card_mock1 }), - 1 => View::Journal(JournalView { + 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, }), - 2 => View::Settings(SettingsMenu { selected: 0 }), + 3 => View::Settings(SettingsMenu { selected: 0 }), _ => View::Main(self.clone()), }, _ => View::Main(self.clone()), }; - future::ready(new_menu) + future::ready(NewState { + replace_view: matches!(new_menu, View::Main(_)), + view: new_menu, + redraw: !matches!(input, Action::Timer), + }) } } diff --git a/src/views/scan_menu.rs b/src/views/scan_menu.rs index e113390..6de643e 100644 --- a/src/views/scan_menu.rs +++ b/src/views/scan_menu.rs @@ -1,4 +1,6 @@ use crate::display::primary_lcd::PrimaryLcdDisplay; +use crate::navigation::navigation::CARD_DATA; +use crate::navigation::navigation::NewState; use alloc::format; use embedded_graphics::Drawable; @@ -82,16 +84,29 @@ impl Navigable for ScanMenu { core::future::ready(()) } - fn handle_input(&self, input: Action) -> impl core::future::Future + Send { + fn handle_input(&self, input: Action) -> impl core::future::Future + Send { async move { - let new_progress = match input { - Action::Up => self.progress.wrapping_add(5), - _ => self.progress, - }; - - View::Scan(ScanMenu { - progress: new_progress, - }) + log::info!("Scan view detected, checking for NFC card data"); + if let Some(card_data) = CARD_DATA.lock().await.take() { + log::info!("NFC Card to be displayed: {:?}", card_data); + NewState { + view: View::Card(crate::views::card_view::CardView { card: card_data }), + replace_view: true, + redraw: true, + } + } else { + let new_progress = match input { + Action::Up => self.progress.wrapping_add(5), + _ => self.progress, + }; + NewState { + view: View::Scan(ScanMenu { + progress: new_progress, + }), + replace_view: true, + redraw: matches!(input, Action::Up | Action::Timer), + } + } } } } diff --git a/src/views/settings_menu.rs b/src/views/settings_menu.rs index e0a28d2..d6f2097 100644 --- a/src/views/settings_menu.rs +++ b/src/views/settings_menu.rs @@ -1,6 +1,6 @@ use crate::display::primary_lcd::PrimaryLcdDisplay; -use crate::navigation::navigation::{Action, Navigable}; +use crate::navigation::navigation::{Action, Navigable, NewState}; use crate::views::{menu_item, view::View}; use embedded_graphics::pixelcolor::Rgb565; @@ -25,7 +25,13 @@ impl Navigable for SettingsMenu { core::future::ready(()) } - fn handle_input(&self, _input: Action) -> impl core::future::Future + Send { - async move { View::Settings(SettingsMenu { selected: 0 }) } + fn handle_input(&self, input: Action) -> impl core::future::Future + Send { + async move { + NewState { + view: View::Settings(SettingsMenu { selected: 0 }), + replace_view: true, + redraw: !matches!(input, Action::Timer), + } + } } } diff --git a/src/views/view.rs b/src/views/view.rs index e60760b..52cf112 100644 --- a/src/views/view.rs +++ b/src/views/view.rs @@ -4,7 +4,7 @@ use crate::views::{ settings_menu::SettingsMenu, }; -use crate::navigation::navigation::Navigable; +use crate::navigation::navigation::{Navigable, NewState}; #[derive(Debug, Clone)] pub enum View { @@ -34,7 +34,7 @@ impl Navigable for View { fn handle_input( &self, input: crate::navigation::navigation::Action, - ) -> impl core::future::Future + Send { + ) -> impl core::future::Future + Send { async move { match self { View::Main(main_menu) => main_menu.handle_input(input).await,