implement error view which displays all error on the display #48

Merged
rhetenor merged 1 commits from rhetenor/errorview AGit into main 2026-08-29 22:21:19 +02:00
17 changed files with 316 additions and 92 deletions
Generated
+18
View File
@@ -363,6 +363,7 @@ dependencies = [
"embedded-hal-bus",
"embedded-io 0.7.1",
"embedded-storage",
"embedded-text",
"esp-alloc",
"esp-bootloader-esp-idf",
"esp-hal",
@@ -1049,6 +1050,17 @@ dependencies = [
"embedded-storage",
]
[[package]]
name = "embedded-text"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cf5c72c52db2f7dbe4a9c1ed81cd21301e8d66311b194fa41c04fb4f71843ba"
dependencies = [
"az",
"embedded-graphics",
"object-chain",
]
[[package]]
name = "enumset"
version = "1.1.14"
@@ -2130,6 +2142,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "object-chain"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41af26158b0f5530f7b79955006c2727cd23d0d8e7c3109dc316db0a919784dd"
[[package]]
name = "once_cell"
version = "1.21.4"
+1
View File
@@ -54,6 +54,7 @@ esp-storage = "0.9.0"
embedded-storage = "0.3.1"
binary_serde = "1.0.25"
static_assertions = { version = "1.1.0", default-features = false }
embedded-text = "0.7.3"
[build-dependencies]
log = "0.4.27"
+37 -5
View File
@@ -1,10 +1,15 @@
use crate::alloc::string::ToString;
use crate::card::model::Card;
use crate::navigation::inputs::ButtonAction;
use crate::navigation::inputs::Inputs;
use crate::navigation::outputs::Outputs;
use crate::navigation::state::NavigationState;
use crate::peripherals::Peripherals;
use crate::views::error_view::ErrorView;
use crate::views::view::View;
use alloc::boxed::Box;
use alloc::string::String;
use core::error;
use embassy_futures::select::{Either, select};
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
@@ -32,21 +37,45 @@ pub trait Navigable {
&self,
outputs: &mut Outputs,
peripherals: &Peripherals,
) -> impl core::future::Future<Output = ()> + Send;
) -> impl core::future::Future<Output = Result<(), Box<dyn error::Error>>> + Send;
fn handle_input(&self, input: Action) -> impl core::future::Future<Output = NewState> + Send;
}
#[embassy_executor::task]
pub async fn run(mut inputs: Inputs, mut outputs: Outputs, peripherals: &'static Peripherals) {
async fn display_error(
error: String,
state: &mut NavigationState,
outputs: &mut Outputs,
peripherals: &Peripherals,
) -> () {
let error_view = ErrorView {
error: error.to_string(),
};
state.screens.push(View::Error(error_view));
state
.screens
.last()
// Unwrap: we just pushed the state
.unwrap()
.display(outputs, peripherals)
.await
// Unwrap: display will panic on error in error view
.unwrap();
}
log::info!("starting navigation");
let mut state = NavigationState::new();
state
if let Err(error) = state
.screens
.last()
.unwrap()
.display(&mut outputs, peripherals)
.await;
.await
{
display_error(error.to_string(), &mut state, &mut outputs, &peripherals).await;
};
loop {
let selection = select(
@@ -92,12 +121,15 @@ pub async fn run(mut inputs: Inputs, mut outputs: Outputs, peripherals: &'static
state.screens.push(new_state.view);
if action != Action::Timer || new_state.redraw {
state
if let Err(error) = state
.screens
.last()
.unwrap()
.display(&mut outputs, peripherals)
.await;
.await
{
display_error(error.to_string(), &mut state, &mut outputs, &peripherals).await;
}
}
Timer::after(Duration::from_millis(DEBOUNCE_DURATION_MILLIS)).await;
+6 -6
View File
@@ -1,6 +1,6 @@
pub struct MemoryRegion {
offset: u32,
size: usize,
pub offset: u32,
pub size: usize,
}
impl MemoryRegion {
@@ -15,13 +15,13 @@ impl MemoryRegion {
}
}
const MAX_FLASH: usize = 16 * 1024 * 1024;
pub const MAX_FLASH: usize = 16 * 1024 * 1024;
const MAGIC_REGION: MemoryRegion = MemoryRegion::new(0, 4);
pub const MAGIC_REGION: MemoryRegion = MemoryRegion::new(0, 4);
const SETTINGS_REGION: MemoryRegion = MemoryRegion::new(MAGIC_REGION.end() as u32, 1024);
pub const SETTINGS_REGION: MemoryRegion = MemoryRegion::new(MAGIC_REGION.end() as u32, 1024);
const CARDSTORE_REGION: MemoryRegion = MemoryRegion::new(
pub const CARDSTORE_REGION: MemoryRegion = MemoryRegion::new(
SETTINGS_REGION.end() as u32,
MAX_FLASH - SETTINGS_REGION.end(),
);
+16 -2
View File
@@ -1,5 +1,7 @@
use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use core::error::Error;
use core::fmt::Display;
use binary_serde::{BinarySerde, DeserializeError, Endianness};
@@ -67,7 +69,19 @@ impl AllocationTableEntry {
pub enum CardStoreError {
Store(FlashStoreError),
CorruptedEntry(DeserializeError),
NoEntry,
NoEntry(u32),
}
impl Error for CardStoreError {}
impl Display for CardStoreError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
CardStoreError::Store(e) => e.fmt(f),
CardStoreError::CorruptedEntry(e) => e.fmt(f),
CardStoreError::NoEntry(uuid) => write!(f, "No Card Entry\nuuid: 0x{uuid:x}"),
}
}
}
impl From<FlashStoreError> for CardStoreError {
@@ -129,7 +143,7 @@ impl CardStore {
let entry = self
.allocation_table
.get(&uuid)
.ok_or(CardStoreError::NoEntry)?;
.ok_or(CardStoreError::NoEntry(uuid))?;
let mut raw_card: [u8; size_of::<RawCard>()] = [0; size_of::<RawCard>()];
self.store.read(entry.offset, &mut raw_card).await?;
+32 -5
View File
@@ -1,3 +1,6 @@
use core::error::Error;
use core::fmt::Display;
use crate::navigation::navigation::Mutex;
use embedded_storage::nor_flash::NorFlash;
use embedded_storage::nor_flash::ReadNorFlash;
@@ -5,7 +8,23 @@ use esp_storage::{FlashStorage, FlashStorageError};
use crate::peripherals::storage::MAGIC_REGION;
pub type FlashStoreError = FlashStorageError;
#[derive(Debug)]
pub struct FlashStoreError(FlashStorageError);
impl From<FlashStorageError> for FlashStoreError {
fn from(value: FlashStorageError) -> Self {
Self(value)
}
}
impl Error for FlashStoreError {}
impl Display for FlashStoreError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
use core::fmt::Debug;
self.0.fmt(f)
}
}
const FLASH_ADDR: u32 = 0x9000;
@@ -37,12 +56,17 @@ impl FlashStore {
}
pub async fn read(&self, offset: u32, bytes: &mut [u8]) -> Result<(), FlashStoreError> {
self.storage.lock().await.read(FLASH_ADDR + offset, bytes)
self.storage.lock().await.read(FLASH_ADDR + offset, bytes)?;
Ok(())
}
pub async fn write(&self, offset: u32, bytes: &mut [u8]) -> Result<(), FlashStoreError> {
log::debug!("Writing {} bytes at offset 0x{offset:08x}", bytes.len());
self.storage.lock().await.write(FLASH_ADDR + offset, bytes)
self.storage
.lock()
.await
.write(FLASH_ADDR + offset, bytes)?;
Ok(())
}
pub async fn write_erase(&self, offset: u32, bytes: &mut [u8]) -> Result<(), FlashStoreError> {
@@ -52,7 +76,8 @@ impl FlashStore {
);
let mut lock = self.storage.lock().await;
lock.erase(FLASH_ADDR + offset, FLASH_ADDR + bytes.len() as u32)?;
lock.write(FLASH_ADDR + offset, bytes)
lock.write(FLASH_ADDR + offset, bytes)?;
Ok(())
}
pub fn has_magic_bytes(storage: &mut FlashStorage) -> Result<bool, FlashStoreError> {
@@ -75,7 +100,9 @@ impl FlashStore {
if Self::has_magic_bytes(storage)? {
Ok(())
} else {
Err(FlashStoreError::Other(FLASH_INITIALIZE_MAGIC as i32))
Err(FlashStoreError(FlashStorageError::Other(
FLASH_INITIALIZE_MAGIC as i32,
)))
}
}
}
+1
View File
@@ -1,4 +1,5 @@
pub mod card_view;
pub mod error_view;
pub mod flash_info_view;
pub mod journal_view;
pub mod main_menu;
+4 -1
View File
@@ -2,7 +2,9 @@ use crate::display::sprite::render_sprite_onto_ili9341;
use crate::navigation::navigation::NewState;
use crate::navigation::outputs::Outputs;
use crate::peripherals::Peripherals;
use alloc::boxed::Box;
use alloc::format;
use core::error;
use embedded_graphics::mono_font::MonoTextStyle;
use embedded_graphics::mono_font::ascii::FONT_6X10;
use embedded_graphics::pixelcolor::Rgb565;
@@ -30,7 +32,7 @@ impl Navigable for CardView {
&self,
outputs: &mut Outputs,
_: &Peripherals,
) -> impl core::future::Future<Output = ()> + Send {
) -> impl core::future::Future<Output = Result<(), Box<dyn error::Error>>> + Send {
async move {
let display = &mut outputs.primary_display;
display.clear(Rgb565::BLACK).unwrap();
@@ -100,6 +102,7 @@ impl Navigable for CardView {
let palette: Palette = self.card.cardtype.clone().into();
render_sprite_onto_ili9341(display, self.card.sprite.data.as_slice(), &palette);
Ok(())
}
}
+94
View File
@@ -0,0 +1,94 @@
use crate::navigation::navigation::Action;
use crate::navigation::navigation::Navigable;
use crate::navigation::navigation::NewState;
use crate::navigation::outputs::Outputs;
use crate::peripherals::Peripherals;
use crate::views::view::View;
use alloc::boxed::Box;
use alloc::format;
use alloc::string::String;
use core::error;
use embedded_graphics::{
Drawable,
mono_font::{MonoTextStyle, ascii::FONT_10X20},
pixelcolor::Rgb565,
prelude::*,
primitives::Rectangle,
text::Text,
};
use embedded_text::{
TextBox,
alignment::HorizontalAlignment,
style::{HeightMode, TextBoxStyleBuilder},
};
#[derive(Debug, Clone)]
pub struct ErrorView {
pub error: String,
}
impl Navigable for ErrorView {
fn display(
&self,
outputs: &mut Outputs,
_: &Peripherals,
) -> impl core::future::Future<Output = Result<(), Box<dyn error::Error>>> + Send {
async move {
let style = MonoTextStyle::new(&FONT_10X20, Rgb565::RED);
let display_area = outputs.primary_display.bounding_box();
Text::new(
"Error",
Point::new((display_area.size.width / 2 - 25) as i32, 30),
style,
)
.draw(&mut outputs.primary_display)
.unwrap_or_else(|error| {
panic!(
"Error: {error:?}\nDraw error while rendering error: {}",
self.error
)
});
let style = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE);
let textbox_style = TextBoxStyleBuilder::new()
.height_mode(HeightMode::FitToText)
.alignment(HorizontalAlignment::Center)
.build();
let bounds = Rectangle::new(Point::new(0, 30), display_area.size);
TextBox::with_textbox_style(&format!("{}", self.error), bounds, style, textbox_style)
.draw(&mut outputs.primary_display)
.unwrap_or_else(|error| {
panic!(
"Error: {error:?}\nDraw error while rendering error: {}",
self.error
)
});
Ok(())
}
}
fn handle_input(&self, input: Action) -> impl core::future::Future<Output = NewState> + Send {
async move {
let Action::Button(input) = input else {
// Skip timer inputs
return NewState {
view: View::Error(self.clone()),
replace_view: true,
redraw: false,
};
};
let new_menu = match input {
_ => View::Error(self.clone()),
};
NewState {
replace_view: matches!(new_menu, View::FlashInfo(_)),
view: new_menu,
redraw: true,
}
}
}
}
+17 -6
View File
@@ -1,10 +1,12 @@
use crate::navigation::inputs::ButtonAction;
use crate::navigation::navigation::Action;
use crate::navigation::navigation::Navigable;
use crate::navigation::navigation::NewState;
use crate::navigation::outputs::Outputs;
use crate::peripherals::Peripherals;
use crate::peripherals::storage::MAGIC_REGION;
use crate::views::view::View;
use alloc::boxed::Box;
use core::error;
use embedded_graphics::mono_font::MonoTextStyle;
use embedded_graphics::mono_font::ascii::FONT_10X20;
use embedded_graphics::pixelcolor::Rgb565;
@@ -22,11 +24,20 @@ impl Navigable for FlashInfoView {
&self,
outputs: &mut Outputs,
peripherals: &Peripherals,
) -> impl core::future::Future<Output = ()> + Send {
let style = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE);
Text::new("Flash Initialized: {:?}", Point::new(40, 40), style);
core::future::ready(())
) -> impl core::future::Future<Output = Result<(), Box<dyn error::Error>>> + Send {
async move {
let style = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE);
let mut magic_bytes: [u8; MAGIC_REGION.size] = [0; MAGIC_REGION.size];
peripherals
.store
.lock()
.await
.flash_store
.read(MAGIC_REGION.offset, &mut magic_bytes)
.await?;
Text::new("Flash Initialized: {:?}", Point::new(40, 40), style);
Ok(())
}
}
fn handle_input(&self, input: Action) -> impl core::future::Future<Output = NewState> + Send {
+4 -1
View File
@@ -1,3 +1,5 @@
use alloc::boxed::Box;
use core::error;
use crate::peripherals::Peripherals;
use core::future;
@@ -27,9 +29,10 @@ impl Navigable for JournalView {
&self,
outputs: &mut Outputs,
_: &Peripherals,
) -> impl core::future::Future<Output = ()> + Send {
) -> impl core::future::Future<Output = Result<(), Box<dyn error::Error>>> + Send {
async move {
outputs.primary_display.clear(Rgb565::BLACK).unwrap();
Ok(())
}
}
+14 -9
View File
@@ -1,5 +1,7 @@
use crate::peripherals::Peripherals;
use alloc::boxed::Box;
use alloc::vec;
use core::error;
use crate::card::model::Card;
use crate::card::{decoder::split_nfc_hex, mock::*};
@@ -15,6 +17,7 @@ use embedded_graphics::pixelcolor::Rgb565;
use embedded_graphics::prelude::RgbColor;
use embedded_graphics_core::draw_target::DrawTarget;
use crate::peripherals::storage::cardstore::CardStoreError;
pub const MAX_SELECTED: i32 = 4;
#[derive(Debug, Clone)]
@@ -26,17 +29,19 @@ impl Navigable for MainMenu {
fn display(
&self,
outputs: &mut Outputs,
_: &Peripherals
) -> impl core::future::Future<Output = ()> + Send {
let display = &mut outputs.primary_display;
display.clear(Rgb565::BLACK).unwrap();
_: &Peripherals,
) -> impl core::future::Future<Output = Result<(), Box<dyn error::Error>>> + Send {
async move {
let display = &mut outputs.primary_display;
display.clear(Rgb565::BLACK).unwrap();
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);
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(())
Ok(())
}
}
fn handle_input(&self, input: Action) -> impl core::future::Future<Output = NewState> + Send {
+2
View File
@@ -1,3 +1,5 @@
use alloc::boxed::Box;
use core::error;
use crate::peripherals::Peripherals;
use embedded_graphics::Drawable;
use embedded_graphics::draw_target::DrawTarget;
+47 -44
View File
@@ -3,6 +3,8 @@ use crate::navigation::navigation::CARD_DATA;
use crate::navigation::navigation::NewState;
use crate::navigation::outputs::Outputs;
use crate::peripherals::Peripherals;
use alloc::boxed::Box;
use core::error;
use alloc::format;
use embedded_graphics::Drawable;
@@ -37,55 +39,56 @@ impl Navigable for ScanMenu {
fn display(
&self,
outputs: &mut Outputs,
_: &Peripherals
) -> impl core::future::Future<Output = ()> + Send {
let display = &mut outputs.primary_display;
let style = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE);
display.clear(Rgb565::BLACK).unwrap();
_: &Peripherals,
) -> impl core::future::Future<Output = Result<(), Box<dyn error::Error>>> + Send {
async move {
let display = &mut outputs.primary_display;
let style = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE);
display.clear(Rgb565::BLACK).unwrap();
let display_area = display.bounding_box();
let display_area = display.bounding_box();
Text::new("Scanning...", Point::new(20, 30), style)
Text::new("Scanning...", Point::new(20, 30), style)
.draw(display)
.unwrap();
// Create styles used by the drawing operations.
let arc_stroke = PrimitiveStyleBuilder::new()
.stroke_color(Rgb565::WHITE)
.stroke_width(5)
.stroke_alignment(StrokeAlignment::Inside)
.build();
let character_style = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE);
let text_style = TextStyleBuilder::new()
.baseline(Baseline::Middle)
.alignment(Alignment::Center)
.build();
let sweep = self.progress as f32 * 360.0 / 255.0;
let width = display_area.size.width;
let bounding_box =
Rectangle::with_center(display_area.center(), Size::new_equal(4 * width / 5));
// log::info!("bounding_box: {:?}", bounding_box);
// log::info!("display_area: {:?}", display_area);
// Draw an arc with a 5px wide stroke.
Arc::new(
bounding_box.top_left,
bounding_box.size.width,
90.0.deg(),
sweep.deg(),
)
.into_styled(arc_stroke)
.draw(display)
.unwrap();
// Create styles used by the drawing operations.
let arc_stroke = PrimitiveStyleBuilder::new()
.stroke_color(Rgb565::WHITE)
.stroke_width(5)
.stroke_alignment(StrokeAlignment::Inside)
.build();
let character_style = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE);
let text_style = TextStyleBuilder::new()
.baseline(Baseline::Middle)
.alignment(Alignment::Center)
.build();
let sweep = self.progress as f32 * 360.0 / 255.0;
let width = display_area.size.width;
let bounding_box =
Rectangle::with_center(display_area.center(), Size::new_equal(4 * width / 5));
// log::info!("bounding_box: {:?}", bounding_box);
// log::info!("display_area: {:?}", display_area);
// Draw an arc with a 5px wide stroke.
Arc::new(
bounding_box.top_left,
bounding_box.size.width,
90.0.deg(),
sweep.deg(),
)
.into_styled(arc_stroke)
.draw(display)
.unwrap();
// Draw centered text.
let text = format!("{:.2}%", 100. * (self.progress as f32) / 255.);
Text::with_text_style(&text, display_area.center(), character_style, text_style)
.draw(display)
.unwrap();
core::future::ready(())
// Draw centered text.
let text = format!("{:.2}%", 100. * (self.progress as f32) / 255.);
Text::with_text_style(&text, display_area.center(), character_style, text_style)
.draw(display)
.unwrap();
Ok(())
}
}
fn handle_input(&self, input: Action) -> impl core::future::Future<Output = NewState> + Send {
+12 -9
View File
@@ -2,6 +2,8 @@ use crate::navigation::navigation::{Action, Navigable, NewState};
use crate::navigation::outputs::Outputs;
use crate::peripherals::Peripherals;
use crate::views::{flash_info_view::FlashInfoView, menu_item, view::View};
use alloc::boxed::Box;
use core::error;
use crate::navigation::inputs::ButtonAction;
@@ -20,15 +22,16 @@ impl Navigable for SettingsMenu {
fn display(
&self,
outputs: &mut Outputs,
_: &Peripherals
) -> impl core::future::Future<Output = ()> + 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);
menu_item::show(display, "Flash Information", 2, self.selected);
core::future::ready(())
_: &Peripherals,
) -> impl core::future::Future<Output = Result<(), Box<dyn error::Error>>> + Send {
async move {
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);
menu_item::show(display, "Flash Information", 2, self.selected);
Ok(())
}
}
fn handle_input(&self, input: Action) -> impl core::future::Future<Output = NewState> + Send {
+2
View File
@@ -1 +1,3 @@
use alloc::boxed::Box;
use core::error;
// Status bar view module
+9 -4
View File
@@ -2,14 +2,17 @@ use crate::navigation::outputs::Outputs;
use crate::peripherals::Peripherals;
use crate::views::card_view::CardView;
use crate::views::{
flash_info_view::FlashInfoView, journal_view::JournalView, main_menu::MainMenu,
scan_menu::ScanMenu, settings_menu::SettingsMenu,
error_view::ErrorView, flash_info_view::FlashInfoView, journal_view::JournalView,
main_menu::MainMenu, scan_menu::ScanMenu, settings_menu::SettingsMenu,
};
use alloc::boxed::Box;
use core::error;
use crate::navigation::navigation::{Navigable, NewState};
#[derive(Debug, Clone)]
pub enum View {
Error(ErrorView),
Main(MainMenu),
Scan(ScanMenu),
Settings(SettingsMenu),
@@ -22,10 +25,11 @@ impl Navigable for View {
fn display(
&self,
outputs: &mut Outputs,
peripherals: &Peripherals
) -> impl core::future::Future<Output = ()> + Send {
peripherals: &Peripherals,
) -> impl core::future::Future<Output = Result<(), Box<dyn error::Error>>> + Send {
async move {
match self {
View::Error(error_view) => error_view.display(outputs, peripherals).await,
View::Main(main_menu) => main_menu.display(outputs, peripherals).await,
View::Scan(scan_menu) => scan_menu.display(outputs, peripherals).await,
View::Settings(settings_menu) => settings_menu.display(outputs, peripherals).await,
@@ -44,6 +48,7 @@ impl Navigable for View {
) -> impl core::future::Future<Output = NewState> + Send {
async move {
match self {
View::Error(error_view) => error_view.handle_input(input).await,
View::Main(main_menu) => main_menu.handle_input(input).await,
View::Scan(scan_menu) => scan_menu.handle_input(input).await,
View::Settings(settings_menu) => settings_menu.handle_input(input).await,