diff --git a/Cargo.lock b/Cargo.lock index c7ada03..2c79bcb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1702,8 +1702,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "i2c-character-display" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86a620ec16541e28c0e52ce21cd23d734a5d7efaae3d609b2a58694b0ef67739" +source = "git+https://github.com/ede1998/i2c-character-display.git?rev=57aef1f49da183c120b8e995ef77549e3515d3c6#57aef1f49da183c120b8e995ef77549e3515d3c6" dependencies = [ "bitfield 0.17.0", "embedded-hal 1.0.0", diff --git a/Cargo.toml b/Cargo.toml index 55b0e77..f7b92a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ ch1115 = { version = "0.1.2", features = ["graphics"] } [patch.crates-io] ch1115 = { git = "https://github.com/ede1998/ch1115.git", rev = "2d3a3ba38050efe012e6a210afe3e125ec280c37" } +i2c-character-display = { git = "https://github.com/ede1998/i2c-character-display.git", rev = "57aef1f49da183c120b8e995ef77549e3515d3c6" } # 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 fa8a950..b0ae8c7 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -7,14 +7,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::shared_bus::{DualSecondaryDisplay, init_dual_displays}; -use creaturedex::display::tertiary_lcd::{init_tertiary_lcd, write_wrapped}; +use creaturedex::drivers::i2c_bus::I2cBusPinConfiguration; use creaturedex::drivers::nfc_pn532::NfcPn532Driver; +use creaturedex::drivers::secondary_oled::SecondaryDisplay; +use creaturedex::drivers::spi_bus; +use creaturedex::drivers::tertiary_lcd::{TertiaryDisplay, init_tertiary_lcd, write_wrapped}; use creaturedex::navigation::inputs::Inputs; use creaturedex::navigation::navigation::{self, CARD_DATA}; +use creaturedex::navigation::outputs::Outputs; use creaturedex::network::wifi::setup_wifi; use embassy_executor::Spawner; use embassy_time::Instant; @@ -25,11 +27,10 @@ use embedded_graphics::{ text::Text, }; use embedded_hal_bus::i2c::AtomicDevice; -use embedded_hal_bus::util::AtomicCell; use esp_hal::Blocking; use esp_hal::clock::CpuClock; -use esp_hal::gpio::{Input, InputConfig, Level, Output, OutputConfig, Pull}; -use esp_hal::i2c::master::{Config as I2cConfig, I2c}; +use esp_hal::gpio::{Input, InputConfig, Pull}; +use esp_hal::i2c::master::I2c; use esp_hal::timer::timg::TimerGroup; use log::error; @@ -65,46 +66,35 @@ async fn main(spawner: Spawner) { let timg0 = TimerGroup::new(peripherals.TIMG0); esp_rtos::start(timg0.timer0, sw_interrupt.software_interrupt0); - let (display, oled) = Box::leak(Box::new(init_dual_displays( - peripherals.SPI2, - peripherals.GPIO36, // sck - peripherals.GPIO35, // mosi - peripherals.GPIO37, // miso - peripherals.GPIO11, // primary cs - peripherals.GPIO13, // oled cs - peripherals.GPIO10, // primary reset - peripherals.GPIO47, // secondary reset - peripherals.GPIO12, // shared dc - ))); + let (primary, mut secondaries) = spi_bus::DisplayPinConfiguration { + spi_peripheral: peripherals.SPI2, + sck: peripherals.GPIO36, + mosi: peripherals.GPIO35, + miso: peripherals.GPIO37, + cs_primary: peripherals.GPIO11, + cs_secondary_1: peripherals.GPIO13, + cs_secondary_2: peripherals.GPIO14, + cs_secondary_3: peripherals.GPIO21, + reset_primary: peripherals.GPIO10, + reset_secondary: peripherals.GPIO47, + dc_pin: peripherals.GPIO12, + } + .build(); - let _secondary_oled_cs_2 = Output::new( - peripherals.GPIO14, - Level::High, - OutputConfig::default(), - ); - let _secondary_oled_cs_3 = Output::new( - peripherals.GPIO21, - Level::High, - OutputConfig::default(), - ); - - let config = I2cConfig::default().with_frequency(esp_hal::time::Rate::from_khz(400)); - let i2c = match I2c::new(peripherals.I2C0, config) { - Ok(bus) => bus - .with_sda(peripherals.GPIO17) - .with_scl(peripherals.GPIO18), - Err(e) => { - log::error!("Shared I2C bus initialization failed for GPIO17/GPIO18: {e:?}"); - panic!("Shared I2C bus initialization failed"); - } + // Initialize the shared I2C bus using the refactored i2c_bus module. + let shared_i2c = { + let config = I2cBusPinConfiguration { + i2c_peripheral: peripherals.I2C0, + scl: peripherals.GPIO18, + sda: peripherals.GPIO17, + }; + config.build() }; - let shared_i2c = Box::leak(Box::new(AtomicCell::new(i2c))); - log::info!("Shared I2C bus ready on GPIO17 (SDA) and GPIO18 (SCL)"); - let mut lcd = init_tertiary_lcd(AtomicDevice::new(shared_i2c)); - if let Err(_) = write_wrapped(&mut lcd, "Testing more than 16 chars what happens now?") { + let mut lcd: TertiaryDisplay = init_tertiary_lcd(AtomicDevice::new(shared_i2c)); + if let Err(e) = write_wrapped(&mut lcd, "Testing more than 16 chars what happens now?") { log::error!( - "Tertiary LCD startup text write failed over shared I2C bus; check the shared bus, wiring, and device responses" + "Tertiary LCD startup text write failed over shared I2C bus; check the shared bus, wiring, and device responses: {e}" ); } @@ -127,14 +117,32 @@ async fn main(spawner: Spawner) { ok: Input::new(peripherals.GPIO0, button_config), }; - display_shit(oled); + display_shit(&mut secondaries[0], "foo 1"); + display_shit(&mut secondaries[1], "bar 1"); + display_shit(&mut secondaries[2], "baz 1"); + + let outputs = { + let [sec_1, sec_2, sec_3] = secondaries; + Outputs { + primary_display: primary, + secondary_display_1: sec_1, + secondary_display_2: sec_2, + secondary_display_3: sec_3, + tertiary_display: lcd, + _led_a: (), + } + }; log::info!("Setup complete, entering main loop"); - spawner.spawn(navigation::run(inputs, display).expect("run task failed")); + spawner.spawn(navigation::run(inputs, outputs).expect("run task failed")); spawner.spawn(nfc_driver_task(nfc_driver).expect("nfc driver task failed")); } #[embassy_executor::task] +#[allow( + clippy::large_stack_frames, + reason = "ignoring this for now because it still works" +)] async fn nfc_driver_task( mut nfc_driver: NfcPn532Driver>>, ) { @@ -167,11 +175,11 @@ async fn nfc_driver_task( } } -fn display_shit(oled: &mut DualSecondaryDisplay<'static>) { +fn display_shit(oled: &mut SecondaryDisplay<'static>, text: &str) { oled.clear().unwrap(); let text_style = MonoTextStyle::new(&FONT_6X10, BinaryColor::On); - Text::new("Hello OLED", Point::new(10, 20), text_style) + Text::new(text, Point::new(10, 20), text_style) .draw(oled) .unwrap(); oled.flush().unwrap(); diff --git a/src/card/model.rs b/src/card/model.rs index 5747671..bb2ee96 100644 --- a/src/card/model.rs +++ b/src/card/model.rs @@ -48,9 +48,9 @@ impl Display for CardType { } pub type PrimaryColor = Rgb565; -impl Into for CardType { - fn into(self) -> PrimaryColor { - match self { +impl From for PrimaryColor { + fn from(val: CardType) -> Self { + match val { CardType::Default => Rgb888::new(0xD8, 0x68, 0x30).into(), CardType::Blueprint => Rgb888::new(0x6D, 0x81, 0xFF).into(), CardType::CYBER => Rgb888::new(0xFF, 0xFF, 0x04).into(), @@ -64,9 +64,9 @@ impl Into for CardType { } pub type Palette = [Rgb565; 16]; -impl Into for CardType { - fn into(self) -> Palette { - match self { +impl From for Palette { + fn from(val: CardType) -> Self { + match val { CardType::Default => [ Rgb888::new(31, 23, 35).into(), Rgb888::new(60, 47, 82).into(), diff --git a/src/display.rs b/src/display.rs index 614d495..2013c3f 100644 --- a/src/display.rs +++ b/src/display.rs @@ -1,5 +1 @@ -pub mod primary_lcd; -pub mod secondary_oled; -pub mod shared_bus; pub mod sprite; -pub mod tertiary_lcd; diff --git a/src/display/primary_lcd.rs b/src/display/primary_lcd.rs deleted file mode 100644 index a8a5578..0000000 --- a/src/display/primary_lcd.rs +++ /dev/null @@ -1,43 +0,0 @@ -use display_interface_spi::SPIInterface; -use embedded_graphics::draw_target::DrawTarget; -use embedded_graphics::pixelcolor::Rgb565; -use embedded_graphics::prelude::RgbColor; -use embedded_hal::digital::OutputPin as EhOutputPin; -use embedded_hal_bus::spi::AtomicDevice; -use embedded_hal_bus::util::AtomicCell; -use esp_hal::delay::Delay; -use esp_hal::gpio::{Level, Output, OutputConfig, OutputPin}; -use ili9341::{DisplaySize240x320, Ili9341, Orientation}; - -pub type PrimaryLcdDisplay<'a, BUS, DC, RES> = - Ili9341, Delay>, DC>, RES>; - -pub fn init_primary_lcd_on_bus<'a, BUS, CS, RES, DC>( - spi_bus: &'a AtomicCell, - cs: CS, - reset: RES, - dc: DC, - delay: &mut Delay, -) -> PrimaryLcdDisplay<'a, BUS, DC, RES> -where - BUS: embedded_hal::spi::SpiBus, - CS: OutputPin + 'static, - RES: EhOutputPin, - DC: EhOutputPin, -{ - let cs_pin = Output::new(cs, Level::High, OutputConfig::default()); - let spi_dev = AtomicDevice::new(spi_bus, cs_pin, *delay).unwrap(); - let iface = SPIInterface::new(spi_dev, dc); - - let mut display = Ili9341::new( - iface, - reset, - delay, - Orientation::Portrait, - DisplaySize240x320, - ) - .unwrap(); - - display.clear(Rgb565::BLACK).unwrap(); - display -} diff --git a/src/display/secondary_oled.rs b/src/display/secondary_oled.rs deleted file mode 100644 index b003bdb..0000000 --- a/src/display/secondary_oled.rs +++ /dev/null @@ -1,34 +0,0 @@ -use ch1115::{Ch1115, Size128x64}; -use display_interface_spi::SPIInterface; -use embedded_hal::digital::OutputPin as EhOutputPin; -use embedded_hal_bus::spi::AtomicDevice; -use embedded_hal_bus::util::AtomicCell; -use esp_hal::delay::Delay; -use esp_hal::gpio::{Level, Output, OutputConfig, OutputPin}; - -pub type SecondaryOledDisplay<'a, BUS, RST, DC> = - Ch1115, Delay>, DC>, RST, Size128x64>; - -pub fn init_secondary_oled_on_bus<'a, BUS, CS, RES, DC>( - spi_bus: &'a AtomicCell, - cs: CS, - res: RES, - dc: DC, - delay: &mut Delay, -) -> SecondaryOledDisplay<'a, BUS, RES, DC> -where - BUS: embedded_hal::spi::SpiBus, - CS: OutputPin + 'static, - RES: EhOutputPin, - DC: EhOutputPin, -{ - let cs_pin = Output::new(cs, Level::High, OutputConfig::default()); - let spi_dev = AtomicDevice::new(spi_bus, cs_pin, *delay).unwrap(); - let interface = SPIInterface::new(spi_dev, dc); - - let mut display = Ch1115::new(interface, res, Size128x64); - - display.init(&mut Delay::new()).unwrap(); - - display -} diff --git a/src/display/shared_bus.rs b/src/display/shared_bus.rs deleted file mode 100644 index 2a42521..0000000 --- a/src/display/shared_bus.rs +++ /dev/null @@ -1,113 +0,0 @@ -use alloc::boxed::Box; -use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; -use embedded_hal::digital::ErrorType; -use embedded_hal::digital::OutputPin as EhOutputPin; -use embedded_hal_bus::util::AtomicCell; -use esp_hal::delay::Delay; -use esp_hal::gpio::interconnect::{PeripheralInput, PeripheralOutput}; -use esp_hal::gpio::{Level, Output, OutputConfig, OutputPin}; -use esp_hal::spi::Mode; -use esp_hal::spi::master::{Config as SpiConfig, Spi}; -use esp_hal::time::Rate; - -use crate::display::secondary_oled::{SecondaryOledDisplay, init_secondary_oled_on_bus}; -use crate::display::primary_lcd::{PrimaryLcdDisplay, init_primary_lcd_on_bus}; - -use core::cell::RefCell; -use embassy_sync::blocking_mutex::Mutex as BlockingMutex; - -#[derive(Clone)] -pub struct SharedPin<'a, P> { - mutex: &'a BlockingMutex>, -} - -impl<'a, P> SharedPin<'a, P> { - pub fn new(mutex: &'a BlockingMutex>) -> Self { - Self { mutex } - } - - pub fn pre_wrap(pin: P) -> &'static BlockingMutex> { - let mutexed = BlockingMutex::new(RefCell::new(pin)); - Box::leak(Box::new(mutexed)) - } -} - -impl<'a, P: EhOutputPin> ErrorType for SharedPin<'a, P> { - type Error = P::Error; -} - -impl<'a, P: EhOutputPin> EhOutputPin for SharedPin<'a, P> { - fn set_low(&mut self) -> Result<(), Self::Error> { - self.mutex.lock(|p| p.borrow_mut().set_low()) - } - - fn set_high(&mut self) -> Result<(), Self::Error> { - self.mutex.lock(|p| p.borrow_mut().set_high()) - } -} - -pub type DisplaySpiBus = Spi<'static, esp_hal::Blocking>; -pub type DualPrimaryDisplay<'a> = - PrimaryLcdDisplay<'a, DisplaySpiBus, SharedPin<'a, Output<'a>>, SharedPin<'a, Output<'a>>>; -pub type DualSecondaryDisplay<'a> = - SecondaryOledDisplay<'a, DisplaySpiBus, SharedPin<'a, Output<'a>>, SharedPin<'a, Output<'a>>>; - -pub fn init_dual_displays( - spi_peripheral: SPI, - sck: SCK, - mosi: MOSI, - miso: MISO, - cs_primary: CS1, - cs_secondary: CS2, - reset_primary: RES1, - reset_secondary: RES2, - dc_pin: DC, -) -> (DualPrimaryDisplay<'static>, DualSecondaryDisplay<'static>) -where - SPI: esp_hal::spi::master::Instance + 'static, - SCK: PeripheralOutput<'static>, - MOSI: PeripheralOutput<'static>, - MISO: PeripheralInput<'static>, - CS1: OutputPin + 'static, - CS2: OutputPin + 'static, - RES1: OutputPin + 'static, - RES2: OutputPin + 'static, - DC: OutputPin + 'static, -{ - let spi = Spi::new( - spi_peripheral, - SpiConfig::default() - .with_frequency(Rate::from_khz(20_000)) - .with_mode(Mode::_0), - ) - .unwrap() - .with_sck(sck) - .with_mosi(mosi) - .with_miso(miso); - - let bus_static: &'static AtomicCell<_> = Box::leak(Box::new(AtomicCell::new(spi))); - let reset_primary_static = - SharedPin::pre_wrap(Output::new(reset_primary, Level::High, OutputConfig::default())); - let reset_secondary_static = - SharedPin::pre_wrap(Output::new(reset_secondary, Level::High, OutputConfig::default())); - let dc_static = SharedPin::pre_wrap(Output::new(dc_pin, Level::High, OutputConfig::default())); - let mut delay = Delay::new(); - - let primary = init_primary_lcd_on_bus( - bus_static, - cs_primary, - SharedPin::new(reset_primary_static), - SharedPin::new(dc_static), - &mut delay, - ); - - let secondary = init_secondary_oled_on_bus( - bus_static, - cs_secondary, - SharedPin::new(reset_secondary_static), - SharedPin::new(dc_static), - &mut delay, - ); - - (primary, secondary) -} diff --git a/src/display/sprite.rs b/src/display/sprite.rs index e2d6d5d..b5dec5b 100644 --- a/src/display/sprite.rs +++ b/src/display/sprite.rs @@ -47,10 +47,7 @@ where let width = u32::try_from(end_x - start_x).unwrap(); let height = u32::try_from(end_y - start_y).unwrap(); - let area = Rectangle::new( - Point::new(start_x.try_into().unwrap(), start_y.try_into().unwrap()), - Size::new(width, height), - ); + let area = Rectangle::new(Point::new(start_x, start_y), Size::new(width, height)); let _ = ili9341.fill_solid(&area, color); } diff --git a/src/drivers.rs b/src/drivers.rs index 0becd6c..a4704c0 100644 --- a/src/drivers.rs +++ b/src/drivers.rs @@ -1 +1,6 @@ +pub mod i2c_bus; pub mod nfc_pn532; +pub mod primary_lcd; +pub mod secondary_oled; +pub mod spi_bus; +pub mod tertiary_lcd; diff --git a/src/drivers/i2c_bus.rs b/src/drivers/i2c_bus.rs new file mode 100644 index 0000000..fecf1d2 --- /dev/null +++ b/src/drivers/i2c_bus.rs @@ -0,0 +1,36 @@ +use alloc::boxed::Box; +use embedded_hal_bus::util::AtomicCell; +use esp_hal::gpio::interconnect::PeripheralInput; +use esp_hal::i2c::master::Config; +use esp_hal::{gpio::interconnect::PeripheralOutput, i2c::master::I2c}; + +type InnerI2cBus = esp_hal::i2c::master::I2c<'static, esp_hal::Blocking>; +pub type I2cBus = &'static AtomicCell; + +pub struct I2cBusPinConfiguration { + pub i2c_peripheral: I2C, + pub scl: SCL, + pub sda: SDA, +} + +impl I2cBusPinConfiguration +where + I2C: esp_hal::i2c::master::Instance + 'static, + SCL: PeripheralOutput<'static> + PeripheralInput<'static>, + SDA: PeripheralOutput<'static> + PeripheralInput<'static>, +{ + pub fn build(self) -> I2cBus { + let config = Config::default().with_frequency(esp_hal::time::Rate::from_khz(400)); + let i2c = match I2c::new(self.i2c_peripheral, config) { + Ok(bus) => bus.with_sda(self.sda).with_scl(self.scl), + Err(e) => { + log::error!("Shared I2C bus initialization failed for GPIO17/GPIO18: {e:?}"); + panic!("Shared I2C bus initialization failed"); + } + }; + let shared_i2c = Box::leak(Box::new(AtomicCell::new(i2c))); + log::info!("Shared I2C bus ready on GPIO17 (SDA) and GPIO18 (SCL)"); + + shared_i2c + } +} diff --git a/src/drivers/nfc_pn532.rs b/src/drivers/nfc_pn532.rs index 1cb6554..9297bd1 100644 --- a/src/drivers/nfc_pn532.rs +++ b/src/drivers/nfc_pn532.rs @@ -1,4 +1,5 @@ use alloc::vec::Vec; +use core::error::Error; use embedded_hal::i2c::I2c; use log::{error, info}; use pn532::i2c::I2CInterface; @@ -8,6 +9,7 @@ use pn532::{Pn532, Request}; pub type Pn532Device = Pn532, (), 34>; pub const PAGES_PER_READ: usize = 4; +pub const BYTES_PER_READ: usize = PAGES_PER_READ * 4; /// PN532 NFC reader driver. pub struct NfcPn532Driver @@ -17,6 +19,35 @@ where pn532: Pn532Device, } +#[derive(Debug, Clone)] +struct PageParseError { + iteration: u8, + bytes: [u8; N], + status_code: Option, +} + +impl Error for PageParseError {} + +impl core::fmt::Display for PageParseError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let PageParseError { + iteration, + bytes, + status_code, + } = self; + match status_code { + Some(code) => write!( + f, + "NFC pages parse error in iteration {iteration} with error code 0x{code:02x}: {bytes:02x?}" + ), + None => write!( + f, + "No data available for NFC page in iteration {iteration}: {bytes:02x?}" + ), + } + } +} + impl NfcPn532Driver where I2C: I2c, @@ -57,16 +88,21 @@ where } } - pub fn parse_nfc_page(iteration: u8, bytes: &[u8]) -> Result<[u8; PAGES_PER_READ * 4], ()> { - let success = bytes.first().is_some_and(|x| *x == 0); - if success { - let slice_len = bytes.len().min(PAGES_PER_READ * 4 + 1); - let mut arr = [0; PAGES_PER_READ * 4]; - arr[..slice_len - 1].copy_from_slice(&bytes[1..slice_len]); - Ok(arr) - } else { - info!("err {iteration}: {:02x?}", bytes); - Err(()) + fn parse_nfc_page( + iteration: u8, + bytes: &[u8], + ) -> Result<[u8; BYTES_PER_READ], PageParseError> { + let slice_len = bytes.len().min(BYTES_PER_READ + 1); + let mut arr = [0; BYTES_PER_READ]; + arr[..slice_len - 1].copy_from_slice(&bytes[1..slice_len]); + + match bytes.first() { + Some(0x00) => Ok(arr), + error => Err(PageParseError { + iteration, + bytes: arr, + status_code: error.copied(), + }), } } @@ -79,18 +115,25 @@ where let mut data: Vec = Vec::with_capacity(858); for i in (0x0B..=230).step_by(PAGES_PER_READ) { - let mut parse_result = Err(()); - while parse_result.is_err() { - let Ok(result) = self + let bytes = loop { + let read = self .pn532 - .process_async(&Request::ntag_read(i), PAGES_PER_READ * 4 + 1) - .await - else { - continue; + .process_async(&Request::ntag_read(i), BYTES_PER_READ + 1) + .await; + + let bytes = match read { + Ok(bytes) => bytes, + Err(e) => { + error!("Reading page {i} failed: {e:?}"); + continue; + } }; - parse_result = Self::parse_nfc_page(i, result); - } - let bytes = parse_result.unwrap(); + + match Self::parse_nfc_page(i, bytes) { + Ok(data) => break data, + Err(e) => error!("Parsing page failed: {e}"), + } + }; let bytes = if i == 0x0B { &bytes[1..] } else { &bytes }; data.extend(bytes); } diff --git a/src/drivers/primary_lcd.rs b/src/drivers/primary_lcd.rs new file mode 100644 index 0000000..34a4a8c --- /dev/null +++ b/src/drivers/primary_lcd.rs @@ -0,0 +1,42 @@ +use display_interface_spi::SPIInterface; +use embedded_graphics::draw_target::DrawTarget; +use embedded_graphics::pixelcolor::Rgb565; +use embedded_graphics::prelude::RgbColor; +use embedded_hal_bus::spi::AtomicDevice; +use embedded_hal_bus::util::AtomicCell; +use esp_hal::delay::Delay; +use esp_hal::gpio::Output; +use ili9341::{DisplaySize240x320, Ili9341, Orientation}; + +use crate::drivers::spi_bus::{DisplaySpiBus, SharedOutput}; + +type GenericPrimaryDisplay<'a, BUS, DC, RES> = + Ili9341, Delay>, DC>, RES>; +pub type PrimaryDisplay<'a> = + GenericPrimaryDisplay<'a, DisplaySpiBus, SharedOutput<'a>, Output<'a>>; + +pub struct PrimaryDisplayPinConfiguration<'a> { + pub spi_bus: &'a AtomicCell, + pub cs: Output<'a>, + pub reset: Output<'a>, + pub dc: SharedOutput<'a>, +} + +impl<'a> PrimaryDisplayPinConfiguration<'a> { + pub fn build(self) -> PrimaryDisplay<'a> { + let spi_dev = AtomicDevice::new(self.spi_bus, self.cs, Delay::new()).unwrap(); + let iface = SPIInterface::new(spi_dev, self.dc); + + let mut display = Ili9341::new( + iface, + self.reset, + &mut Delay::new(), + Orientation::Portrait, + DisplaySize240x320, + ) + .unwrap(); + + display.clear(Rgb565::BLACK).unwrap(); + display + } +} diff --git a/src/drivers/secondary_oled.rs b/src/drivers/secondary_oled.rs new file mode 100644 index 0000000..86b6101 --- /dev/null +++ b/src/drivers/secondary_oled.rs @@ -0,0 +1,59 @@ +use alloc::format; +use ch1115::{Ch1115, Size128x64}; +use display_interface_spi::SPIInterface; +use embedded_hal_bus::spi::AtomicDevice; +use embedded_hal_bus::util::AtomicCell; +use esp_hal::delay::Delay; +use esp_hal::gpio::Output; + +use crate::drivers::spi_bus::{DisplaySpiBus, SharedOutput, SharedReset}; + +pub type GenericSecondaryDisplay<'a, BUS, RST, DC> = + Ch1115, Delay>, DC>, RST, Size128x64>; +pub type SecondaryDisplay<'a> = + GenericSecondaryDisplay<'a, DisplaySpiBus, SharedReset<'a>, SharedOutput<'a>>; + +pub struct SecondaryDisplayPinConfiguration<'a> { + pub spi_bus: &'a AtomicCell, + pub cs_pins: [Output<'a>; 3], + pub res: SharedReset<'a>, + pub dc: SharedOutput<'a>, +} + +impl<'a> SecondaryDisplayPinConfiguration<'a> { + pub fn build(self) -> [SecondaryDisplay<'a>; 3] { + let mut displays = self.cs_pins.map(|cs| { + let spi_dev = AtomicDevice::new(self.spi_bus, cs, Delay::new()).unwrap(); + let interface = SPIInterface::new(spi_dev, self.dc.clone()); + + Ch1115::new(interface, self.res.clone(), Size128x64) + }); + + init_single_reset(&mut displays, self.res); + + displays + } +} + +fn init_single_reset<'a: 'b, 'b>( + displays: impl IntoIterator>, + shared_reset: SharedReset<'a>, +) { + let mut displays = displays.into_iter().peekable(); + + // Reset all secondaries via shared reset + let Some(any_display) = displays.peek_mut() else { + // no display to initialize + return; + }; + any_display.hard_reset(&mut Delay::new()).unwrap(); + + shared_reset.disable(true); + + for (i, display) in displays.enumerate() { + display + .init(&mut Delay::new()) + .map_err(|e| format!("Failed to init secondary display {}: {e:?}", i + 1)) + .unwrap(); + } +} diff --git a/src/drivers/spi_bus.rs b/src/drivers/spi_bus.rs new file mode 100644 index 0000000..036b1a5 --- /dev/null +++ b/src/drivers/spi_bus.rs @@ -0,0 +1,187 @@ +use alloc::boxed::Box; +use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; +use embedded_hal::digital::ErrorType; +use embedded_hal::digital::OutputPin as EhOutputPin; +use embedded_hal_bus::util::AtomicCell; +use esp_hal::gpio::interconnect::{PeripheralInput, PeripheralOutput}; +use esp_hal::gpio::{Level, Output, OutputConfig, OutputPin}; +use esp_hal::spi::Mode; +use esp_hal::spi::master::{Config as SpiConfig, Spi}; +use esp_hal::time::Rate; + +use crate::drivers::primary_lcd::PrimaryDisplay; +use crate::drivers::primary_lcd::PrimaryDisplayPinConfiguration; +use crate::drivers::secondary_oled::SecondaryDisplay; +use crate::drivers::secondary_oled::SecondaryDisplayPinConfiguration; + +use core::cell::RefCell; +use embassy_sync::blocking_mutex::Mutex as BlockingMutex; + +pub struct SharedPin<'a, P> { + mutex: &'a BlockingMutex>, +} + +impl<'a, P> Clone for SharedPin<'a, P> { + fn clone(&self) -> Self { + Self { mutex: self.mutex } + } +} + +impl<'a, P> SharedPin<'a, P> { + pub fn new(pin: P) -> Self { + let mutexed = BlockingMutex::new(RefCell::new(pin)); + let mutex = Box::leak(Box::new(mutexed)); + Self { mutex } + } +} + +impl<'a, P: EhOutputPin> ErrorType for SharedPin<'a, P> { + type Error = P::Error; +} + +impl<'a, P: EhOutputPin> EhOutputPin for SharedPin<'a, P> { + fn set_low(&mut self) -> Result<(), Self::Error> { + self.mutex.lock(|p| p.borrow_mut().set_low()) + } + + fn set_high(&mut self) -> Result<(), Self::Error> { + self.mutex.lock(|p| p.borrow_mut().set_high()) + } +} + +pub struct SharedResetPin<'a, P> { + mutex: &'a BlockingMutex>, +} + +impl<'a, P> SharedResetPin<'a, P> { + pub fn new(pin: P, disabled: bool) -> Self { + let mutexed = BlockingMutex::new(RefCell::new((pin, disabled))); + let mutex = Box::leak(Box::new(mutexed)); + Self { mutex } + } + + pub fn disable(&self, disabled: bool) { + self.mutex.lock(|p| { + p.borrow_mut().1 = disabled; + }); + if disabled { + log::debug!("Reset pin disabled (masked). No more resets now."); + } else { + log::debug!("Reset pin enabled (unmasked). Reset now possible."); + } + } +} + +impl<'a, P> Clone for SharedResetPin<'a, P> { + fn clone(&self) -> Self { + Self { mutex: self.mutex } + } +} + +impl<'a, P: EhOutputPin> ErrorType for SharedResetPin<'a, P> { + type Error = P::Error; +} + +impl<'a, P: EhOutputPin> EhOutputPin for SharedResetPin<'a, P> { + fn set_low(&mut self) -> Result<(), Self::Error> { + self.mutex.lock(|p| { + let mut guard = p.borrow_mut(); + let disabled = guard.1; + let pin = &mut guard.0; + if !disabled { pin.set_low() } else { Ok(()) } + }) + } + + fn set_high(&mut self) -> Result<(), Self::Error> { + self.mutex.lock(|p| { + let mut guard = p.borrow_mut(); + let disabled = guard.1; + let pin = &mut guard.0; + if !disabled { pin.set_high() } else { Ok(()) } + }) + } +} + +pub type SharedOutput<'a> = SharedPin<'a, Output<'a>>; +pub type SharedReset<'a> = SharedResetPin<'a, Output<'a>>; +pub type DisplaySpiBus = Spi<'static, esp_hal::Blocking>; + +pub struct DisplayPinConfiguration { + pub spi_peripheral: SPI, + pub sck: SCK, + pub mosi: MOSI, + pub miso: MISO, + pub cs_primary: CS0, + pub cs_secondary_1: CS1, + pub cs_secondary_2: CS2, + pub cs_secondary_3: CS3, + pub reset_primary: RES1, + pub reset_secondary: RES2, + pub dc_pin: DC, +} + +impl + DisplayPinConfiguration +where + SPI: esp_hal::spi::master::Instance + 'static, + SCK: PeripheralOutput<'static>, + MOSI: PeripheralOutput<'static>, + MISO: PeripheralInput<'static>, + CS0: OutputPin + 'static, + CS1: OutputPin + 'static, + CS2: OutputPin + 'static, + CS3: OutputPin + 'static, + RES1: OutputPin + 'static, + RES2: OutputPin + 'static, + DC: OutputPin + 'static, +{ + pub fn build(self) -> (PrimaryDisplay<'static>, [SecondaryDisplay<'static>; 3]) { + let spi = Spi::new( + self.spi_peripheral, + SpiConfig::default() + .with_frequency(Rate::from_khz(20_000)) + .with_mode(Mode::_0), + ) + .unwrap() + .with_sck(self.sck) + .with_mosi(self.mosi) + .with_miso(self.miso); + + let bus_static: &'static AtomicCell<_> = Box::leak(Box::new(AtomicCell::new(spi))); + let reset_secondary = SharedResetPin::new( + Output::new(self.reset_secondary, Level::High, OutputConfig::default()), + false, + ); + let dc = SharedPin::new(Output::new( + self.dc_pin, + Level::High, + OutputConfig::default(), + )); + + let primary = PrimaryDisplayPinConfiguration { + spi_bus: bus_static, + cs: low_active(self.cs_primary), + reset: low_active(self.reset_primary), + dc: dc.clone(), + } + .build(); + + let secondaries = SecondaryDisplayPinConfiguration { + spi_bus: bus_static, + cs_pins: [ + low_active(self.cs_secondary_1), + low_active(self.cs_secondary_2), + low_active(self.cs_secondary_3), + ], + res: reset_secondary, + dc, + } + .build(); + + (primary, secondaries) + } +} + +fn low_active<'a, CS: OutputPin + 'a>(pin: CS) -> Output<'a> { + Output::new(pin, Level::High, OutputConfig::default()) +} diff --git a/src/display/tertiary_lcd.rs b/src/drivers/tertiary_lcd.rs similarity index 86% rename from src/display/tertiary_lcd.rs rename to src/drivers/tertiary_lcd.rs index e066344..5fc11dc 100644 --- a/src/display/tertiary_lcd.rs +++ b/src/drivers/tertiary_lcd.rs @@ -1,11 +1,14 @@ use embedded_hal::i2c::I2c; +use embedded_hal_bus::i2c::AtomicDevice; use esp_hal::delay::Delay; use i2c_character_display::{CharacterDisplayPCF8574T, LcdDisplayType}; use log::error; -pub type TertiaryLcd = CharacterDisplayPCF8574T; +type GenericTertiaryDisplay = CharacterDisplayPCF8574T; +pub type TertiaryDisplay<'a> = + GenericTertiaryDisplay>>; -pub fn init_tertiary_lcd(i2c: I2C) -> TertiaryLcd +pub fn init_tertiary_lcd(i2c: I2C) -> GenericTertiaryDisplay where I2C: I2c, { @@ -25,7 +28,7 @@ where } pub fn write_line( - lcd: &mut TertiaryLcd, + lcd: &mut GenericTertiaryDisplay, line: u8, text: &str, ) -> Result<(), i2c_character_display::CharacterDisplayError> @@ -47,7 +50,7 @@ where } pub fn write_wrapped( - lcd: &mut TertiaryLcd, + lcd: &mut GenericTertiaryDisplay, text: &str, ) -> Result<(), i2c_character_display::CharacterDisplayError> where diff --git a/src/navigation.rs b/src/navigation.rs index 6bee9cb..968af8e 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -1,3 +1,4 @@ pub mod inputs; pub mod navigation; +pub mod outputs; pub mod state; diff --git a/src/navigation/navigation.rs b/src/navigation/navigation.rs index dd8fc34..d0f3ecb 100644 --- a/src/navigation/navigation.rs +++ b/src/navigation/navigation.rs @@ -1,15 +1,13 @@ use crate::card::model::Card; -use crate::display::shared_bus::DualPrimaryDisplay; use crate::navigation::inputs::ButtonAction; use crate::navigation::inputs::Inputs; +use crate::navigation::outputs::Outputs; use crate::navigation::state::NavigationState; use crate::views::view::View; use embassy_futures::select::{Either, select}; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_time::{Duration, Timer}; -use embedded_graphics::draw_target::DrawTarget; -use embedded_graphics::pixelcolor::Rgb565; pub type Mutex = embassy_sync::mutex::Mutex; pub type Signal = embassy_sync::signal::Signal; @@ -29,19 +27,16 @@ pub struct NewState { } pub trait Navigable { - fn display(&self, display: &mut D) -> impl core::future::Future + Send - where - D: DrawTarget + Send, - D::Error: core::fmt::Debug; + fn display(&self, outputs: &mut Outputs) -> 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 DualPrimaryDisplay<'static>) { +pub async fn run(mut inputs: Inputs, mut outputs: Outputs) { log::info!("starting navigation"); let mut state = NavigationState::new(); - state.screens.last().unwrap().display(display).await; + state.screens.last().unwrap().display(&mut outputs).await; loop { let selection = select( @@ -87,7 +82,7 @@ pub async fn run(mut inputs: Inputs, display: &'static mut DualPrimaryDisplay<'s state.screens.push(new_state.view); if action != Action::Timer || new_state.redraw { - state.screens.last().unwrap().display(display).await; + state.screens.last().unwrap().display(&mut outputs).await; } Timer::after(Duration::from_millis(DEBOUNCE_DURATION_MILLIS)).await; diff --git a/src/navigation/outputs.rs b/src/navigation/outputs.rs new file mode 100644 index 0000000..03aba5a --- /dev/null +++ b/src/navigation/outputs.rs @@ -0,0 +1,27 @@ +use embedded_graphics::{ + draw_target::DrawTarget as _, + pixelcolor::{Rgb565, RgbColor as _}, +}; + +use crate::drivers::{ + primary_lcd::PrimaryDisplay, secondary_oled::SecondaryDisplay, tertiary_lcd::TertiaryDisplay, +}; + +pub struct Outputs { + pub primary_display: PrimaryDisplay<'static>, + pub secondary_display_1: SecondaryDisplay<'static>, + pub secondary_display_2: SecondaryDisplay<'static>, + pub secondary_display_3: SecondaryDisplay<'static>, + pub tertiary_display: TertiaryDisplay<'static>, + pub _led_a: (), +} + +impl Outputs { + pub fn clear_screens(&mut self) { + self.primary_display.clear(Rgb565::BLACK).unwrap(); + self.secondary_display_1.clear().unwrap(); + // self.secondary_display_2.clear().unwrap(); + // self.secondary_display_3.clear().unwrap(); + self.tertiary_display.clear().unwrap(); + } +} diff --git a/src/navigation/state.rs b/src/navigation/state.rs index 63ae293..a564a74 100644 --- a/src/navigation/state.rs +++ b/src/navigation/state.rs @@ -24,16 +24,16 @@ impl NavigationState { match &self.screens.len() { 0 => { log::info!("No screens to go back to"); - return self.screens.last().unwrap(); + self.screens.last().unwrap() } 1 => { log::info!("Only one screen on stack, cannot go back"); - return self.screens.last().unwrap(); + self.screens.last().unwrap() } _ => { log::info!("Going back to previous screen"); self.screens.pop(); - return self.screens.last().unwrap(); + self.screens.last().unwrap() } } } diff --git a/src/views/card_view.rs b/src/views/card_view.rs index 5f84710..f3ad385 100644 --- a/src/views/card_view.rs +++ b/src/views/card_view.rs @@ -1,5 +1,6 @@ use crate::display::sprite::render_sprite_onto_ili9341; use crate::navigation::navigation::NewState; +use crate::navigation::outputs::Outputs; use alloc::format; use embedded_graphics::mono_font::MonoTextStyle; use embedded_graphics::mono_font::ascii::FONT_6X10; @@ -24,80 +25,77 @@ pub struct CardView { } impl Navigable for CardView { - fn display(&self, display: &mut D) -> impl core::future::Future + Send - where - D: DrawTarget, - D::Error: core::fmt::Debug, - { - display.clear(Rgb565::BLACK).unwrap(); + fn display(&self, outputs: &mut Outputs) -> impl core::future::Future + Send { + async move { + let display = &mut outputs.primary_display; + display.clear(Rgb565::BLACK).unwrap(); - let background = Rectangle::new(Point::new(0, 0), Size::new(240, 320)); - let bg_color = self.card.cardtype.clone().into(); + let background = Rectangle::new(Point::new(0, 0), Size::new(240, 320)); + let bg_color = self.card.cardtype.clone().into(); - let _ = display.fill_solid(&background, bg_color); + let _ = display.fill_solid(&background, bg_color); - let style = MonoTextStyle::new(&FONT_6X10, Rgb565::WHITE); - Text::new( - &format!("Name: {}", self.card.name), - Point::new(20, 240), - style, - ) - .draw(display) - .unwrap(); - Text::new( - &format!("Type: {}", self.card.cardtype), - Point::new(20, 250), - style, - ) - .draw(display) - .unwrap(); - Text::new( - &format!("Event: {}", self.card.event), - Point::new(20, 260), - style, - ) - .draw(display) - .unwrap(); - Text::new( - &format!("UUID: {}", self.card.uuid), - Point::new(20, 270), - style, - ) - .draw(display) - .unwrap(); - Text::new( - &format!("Trait1: {}", self.card.trait1), - Point::new(20, 280), - style, - ) - .draw(display) - .unwrap(); - Text::new( - &format!("Trait2: {}", self.card.trait2), - Point::new(20, 290), - style, - ) - .draw(display) - .unwrap(); - Text::new( - &format!("Trait3: {}", self.card.trait3), - Point::new(20, 300), - style, - ) - .draw(display) - .unwrap(); - Text::new( - &format!("Secret: {}", self.card.secret), - Point::new(20, 310), - style, - ) - .draw(display) - .unwrap(); + let style = MonoTextStyle::new(&FONT_6X10, Rgb565::WHITE); + Text::new( + &format!("Name: {}", self.card.name), + Point::new(20, 240), + style, + ) + .draw(display) + .unwrap(); + Text::new( + &format!("Type: {}", self.card.cardtype), + Point::new(20, 250), + style, + ) + .draw(display) + .unwrap(); + Text::new( + &format!("Event: {}", self.card.event), + Point::new(20, 260), + style, + ) + .draw(display) + .unwrap(); + Text::new( + &format!("UUID: {}", self.card.uuid), + Point::new(20, 270), + style, + ) + .draw(display) + .unwrap(); + Text::new( + &format!("Trait1: {}", self.card.trait1), + Point::new(20, 280), + style, + ) + .draw(display) + .unwrap(); + Text::new( + &format!("Trait2: {}", self.card.trait2), + Point::new(20, 290), + style, + ) + .draw(display) + .unwrap(); + Text::new( + &format!("Trait3: {}", self.card.trait3), + Point::new(20, 300), + style, + ) + .draw(display) + .unwrap(); + Text::new( + &format!("Secret: {}", self.card.secret), + Point::new(20, 310), + style, + ) + .draw(display) + .unwrap(); - let palette: Palette = self.card.cardtype.clone().into(); - render_sprite_onto_ili9341(display, self.card.sprite.data.as_slice(), &palette); - - core::future::ready(()) + let palette: Palette = self.card.cardtype.clone().into(); + render_sprite_onto_ili9341(display, self.card.sprite.data.as_slice(), &palette); + } } fn handle_input(&self, input: Action) -> impl core::future::Future + Send { diff --git a/src/views/journal_view.rs b/src/views/journal_view.rs index 34b7632..952bcc9 100644 --- a/src/views/journal_view.rs +++ b/src/views/journal_view.rs @@ -8,7 +8,10 @@ use embedded_graphics_core::draw_target::DrawTarget; use crate::{ card::model::Card, - navigation::navigation::{Action, Navigable, NewState}, + navigation::{ + navigation::{Action, Navigable, NewState}, + outputs::Outputs, + }, views::view::View, }; @@ -19,14 +22,10 @@ pub struct JournalView { } impl Navigable for JournalView { - fn display(&self, display: &mut D) -> impl core::future::Future + Send - where - D: DrawTarget, - D::Error: core::fmt::Debug, - { - display.clear(Rgb565::BLACK).unwrap(); - - core::future::ready(()) + fn display(&self, outputs: &mut Outputs) -> impl core::future::Future + Send { + async move { + outputs.primary_display.clear(Rgb565::BLACK).unwrap(); + } } fn handle_input(&self, input: Action) -> impl core::future::Future + Send { diff --git a/src/views/main_menu.rs b/src/views/main_menu.rs index ddc61e3..4ff0e91 100644 --- a/src/views/main_menu.rs +++ b/src/views/main_menu.rs @@ -4,6 +4,7 @@ use crate::card::model::Card; use crate::card::{decoder::split_nfc_hex, mock::*}; use crate::navigation::inputs::ButtonAction; +use crate::navigation::outputs::Outputs; use crate::views::card_view::CardView; use crate::views::journal_view::JournalView; use crate::views::{menu_item, settings_menu::SettingsMenu, view::View}; @@ -21,11 +22,8 @@ pub struct MainMenu { } impl Navigable for MainMenu { - fn display(&self, display: &mut D) -> impl core::future::Future + Send - where - D: DrawTarget, - D::Error: core::fmt::Debug, - { + fn display(&self, outputs: &mut Outputs) -> impl core::future::Future + Send { + let display = &mut outputs.primary_display; display.clear(Rgb565::BLACK).unwrap(); menu_item::show(display, "Scan card", 0, self.selected); diff --git a/src/views/menu_item.rs b/src/views/menu_item.rs index 5802bb4..f97e8a9 100644 --- a/src/views/menu_item.rs +++ b/src/views/menu_item.rs @@ -7,7 +7,7 @@ use embedded_graphics::prelude::Point; use embedded_graphics::prelude::RgbColor; use embedded_graphics::text::Text; -pub fn show<'a, D>(display: &mut D, name: &str, position: i32, selected: i32) +pub fn show(display: &mut D, name: &str, position: i32, selected: i32) where D: DrawTarget, D::Error: core::fmt::Debug, diff --git a/src/views/scan_menu.rs b/src/views/scan_menu.rs index d9c40c9..3d92773 100644 --- a/src/views/scan_menu.rs +++ b/src/views/scan_menu.rs @@ -1,10 +1,12 @@ use crate::navigation::inputs::ButtonAction; use crate::navigation::navigation::CARD_DATA; use crate::navigation::navigation::NewState; +use crate::navigation::outputs::Outputs; use alloc::format; use embedded_graphics::Drawable; use embedded_graphics::geometry::AngleUnit as _; +use embedded_graphics::geometry::Dimensions as _; use embedded_graphics::geometry::Size; use embedded_graphics::mono_font::MonoTextStyle; use embedded_graphics::mono_font::ascii::FONT_10X20; @@ -31,11 +33,8 @@ pub struct ScanMenu { } impl Navigable for ScanMenu { - fn display(&self, display: &mut D) -> impl core::future::Future + Send - where - D: DrawTarget, - D::Error: core::fmt::Debug, - { + fn display(&self, outputs: &mut Outputs) -> impl core::future::Future + Send { + let display = &mut outputs.primary_display; let style = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE); display.clear(Rgb565::BLACK).unwrap(); diff --git a/src/views/settings_menu.rs b/src/views/settings_menu.rs index f050572..f855e91 100644 --- a/src/views/settings_menu.rs +++ b/src/views/settings_menu.rs @@ -1,4 +1,5 @@ use crate::navigation::navigation::{Action, Navigable, NewState}; +use crate::navigation::outputs::Outputs; use crate::views::{menu_item, view::View}; use embedded_graphics::pixelcolor::Rgb565; @@ -12,11 +13,8 @@ pub struct SettingsMenu { } impl Navigable for SettingsMenu { - fn display(&self, display: &mut D) -> impl core::future::Future + Send - where - D: DrawTarget, - D::Error: core::fmt::Debug, - { + fn display(&self, outputs: &mut Outputs) -> impl core::future::Future + Send { + let display = &mut outputs.primary_display; display.clear(Rgb565::BLACK).unwrap(); menu_item::show(display, "System Inforation", 0, self.selected); menu_item::show(display, "Wifi Information", 1, self.selected); diff --git a/src/views/view.rs b/src/views/view.rs index 8683638..f3da7c9 100644 --- a/src/views/view.rs +++ b/src/views/view.rs @@ -1,6 +1,4 @@ -use embedded_graphics::draw_target::DrawTarget; -use embedded_graphics::pixelcolor::Rgb565; - +use crate::navigation::outputs::Outputs; use crate::views::card_view::CardView; use crate::views::{ journal_view::JournalView, main_menu::MainMenu, scan_menu::ScanMenu, @@ -19,18 +17,14 @@ pub enum View { } impl Navigable for View { - fn display(&self, display: &mut D) -> impl core::future::Future + Send - where - D: DrawTarget + Send, - D::Error: core::fmt::Debug, - { + fn display(&self, outputs: &mut Outputs) -> impl core::future::Future + Send { async move { match self { - View::Main(main_menu) => main_menu.display(display).await, - View::Scan(scan_menu) => scan_menu.display(display).await, - View::Settings(settings_menu) => settings_menu.display(display).await, - View::Journal(journal_view) => journal_view.display(display).await, - View::Card(card_view) => card_view.display(display).await, + View::Main(main_menu) => main_menu.display(outputs).await, + View::Scan(scan_menu) => scan_menu.display(outputs).await, + View::Settings(settings_menu) => settings_menu.display(outputs).await, + View::Journal(journal_view) => journal_view.display(outputs).await, + View::Card(card_view) => card_view.display(outputs).await, } } }