Skip to content

Commit

Permalink
Enhance global error type to optionally provide the user whose action…
Browse files Browse the repository at this point in the history
…s caused the error
  • Loading branch information
zargony committed Dec 20, 2024
1 parent 2104f4f commit 4d982d4
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 70 deletions.
2 changes: 1 addition & 1 deletion firmware/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[env]
# Task arena size of embassy-executor, see https://docs.embassy.dev/embassy-executor/git/cortex-m/index.html#task-arena
EMBASSY_EXECUTOR_TASK_ARENA_SIZE = "30720"
EMBASSY_EXECUTOR_TASK_ARENA_SIZE = "34816"
# Log filter for esp-println to apply at runtime. Also, a feature of the log crate strips all
# logging above info level from release builds at compile time (feature `release_max_level_info`).
ESP_LOG = "info,touch_n_drink=debug"
Expand Down
86 changes: 80 additions & 6 deletions firmware/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,84 @@
use crate::user::UserId;
use crate::{display, nfc, vereinsflieger};
use core::fmt;
use core::future::Future;

/// Main error type
#[derive(Debug)]
#[allow(clippy::enum_variant_names)]
pub enum Error {
pub struct Error {
/// Error kind with optional embedded causing error type
kind: ErrorKind,
/// Optional user whose action caused the error
user_id: Option<UserId>,
}

impl<T: Into<ErrorKind>> From<T> for Error {
fn from(err: T) -> Self {
Self {
kind: err.into(),
user_id: None,
}
}
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.kind)
}
}

impl Error {
/// Error kind
#[allow(dead_code)]
pub fn kind(&self) -> &ErrorKind {
&self.kind
}

/// True if user cancelled an action
pub fn is_cancel(&self) -> bool {
matches!(self.kind, ErrorKind::Cancel)
}

/// True if user interaction timed out
pub fn is_user_timeout(&self) -> bool {
matches!(self.kind, ErrorKind::UserTimeout)
}

/// User whose action caused the error, if any
pub fn user_id(&self) -> Option<UserId> {
self.user_id
}

/// Try running the provided closure and associate the given user id with any error that might
/// be returned by it
#[allow(dead_code)]
pub fn try_with<T, F>(user_id: UserId, f: F) -> Result<T, Self>
where
F: FnOnce() -> Result<T, Self>,
{
f().map_err(|err| Self {
kind: err.kind,
user_id: Some(user_id),
})
}

/// Try running the provided future and associate the given user id with any error that might
/// be returned by it
pub async fn try_with_async<T, F>(user_id: UserId, fut: F) -> Result<T, Self>
where
F: Future<Output = Result<T, Self>>,
{
fut.await.map_err(|err| Self {
kind: err.kind,
user_id: Some(user_id),
})
}
}

/// Error kind with optional embedded causing error type
#[derive(Debug)]
#[allow(clippy::module_name_repetitions)]
pub enum ErrorKind {
/// Display output error
DisplayError(display::Error),
/// NFC reader error
Expand All @@ -21,25 +95,25 @@ pub enum Error {
ArticleNotFound,
}

impl From<display::Error> for Error {
impl From<display::Error> for ErrorKind {
fn from(err: display::Error) -> Self {
Self::DisplayError(err)
}
}

impl From<nfc::Error> for Error {
impl From<nfc::Error> for ErrorKind {
fn from(err: nfc::Error) -> Self {
Self::NFCError(err)
}
}

impl From<vereinsflieger::Error> for Error {
impl From<vereinsflieger::Error> for ErrorKind {
fn from(err: vereinsflieger::Error) -> Self {
Self::VereinsfliegerError(err)
}
}

impl fmt::Display for Error {
impl fmt::Display for ErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::DisplayError(err) => write!(f, "Display: {err}"),
Expand Down
22 changes: 13 additions & 9 deletions firmware/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,12 +248,14 @@ async fn main(spawner: Spawner) {

loop {
match ui.init().await {
// Success or cancel: continue
Ok(()) | Err(error::Error::Cancel) => break,
// Success: continue
Ok(()) => break,
// User cancelled: continue
Err(err) if err.is_cancel() => break,
// Display error to user and try again
Err(err) => {
error!("Initialization error: {:?}", err);
let _ = ui.show_error(&err, false).await;
let _ = ui.show_error(&err).await;
}
}
}
Expand All @@ -262,14 +264,16 @@ async fn main(spawner: Spawner) {
match ui.run().await {
// Success: start over again
Ok(()) => (),
// Cancel: start over again
Err(error::Error::Cancel) => info!("User cancelled, starting over..."),
// Timeout: start over again
Err(error::Error::UserTimeout) => info!("Timeout waiting for user, starting over..."),
// User cancelled: start over again
Err(err) if err.is_cancel() => info!("User cancelled, starting over..."),
// User interaction timeout: start over again
Err(err) if err.is_user_timeout() => {
info!("Timeout waiting for user, starting over...");
}
// Display error to user and start over again
Err(err) => {
error!("User flow error: {:?}", err);
let _ = ui.show_error(&err, true).await;
error!("Error: {:?}", err);
let _ = ui.show_error(&err).await;
}
}
}
Expand Down
21 changes: 8 additions & 13 deletions firmware/src/screen.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::error;
use crate::{GIT_SHA_STR, VERSION_STR};
use core::fmt;
use embedded_graphics::draw_target::DrawTarget;
use embedded_graphics::image::{Image, ImageRaw};
use embedded_graphics::pixelcolor::BinaryColor;
Expand Down Expand Up @@ -136,28 +136,23 @@ impl Screen for Splash {
}

/// Failure screen
pub struct Failure<M> {
message: M,
pub struct Failure<'a> {
error: &'a error::Error,
}

impl<M: fmt::Display> Failure<M> {
pub fn new(message: M) -> Self {
Self { message }
impl<'a> Failure<'a> {
pub fn new(error: &'a error::Error) -> Self {
Self { error }
}
}

impl<M: fmt::Display> Screen for Failure<M> {
impl Screen for Failure<'_> {
fn draw<D: DrawTarget<Color = BinaryColor>>(
&self,
target: &mut D,
) -> Result<(), Error<D::Error>> {
centered(&TITLE_FONT, 26, "FEHLER!", target)?;
centered(
&SMALL_FONT,
26 + 12,
format_args!("{}", self.message),
target,
)?;
centered(&SMALL_FONT, 26 + 12, format_args!("{}", self.error), target)?;
footer("* Abbruch", "", target)?;
Ok(())
}
Expand Down
89 changes: 48 additions & 41 deletions firmware/src/ui.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::article::{ArticleId, Articles};
use crate::buzzer::Buzzer;
use crate::display::Display;
use crate::error::Error;
use crate::error::{Error, ErrorKind};
use crate::http::Http;
use crate::keypad::{Key, Keypad};
use crate::nfc::Nfc;
Expand All @@ -13,7 +13,6 @@ use crate::vereinsflieger::Vereinsflieger;
use crate::wifi::Wifi;
use alloc::string::String;
use core::convert::Infallible;
use core::fmt;
use embassy_futures::select::{select, Either};
use embassy_time::{with_timeout, Duration, TimeoutError, Timer};
use embedded_hal_async::digital::Wait;
Expand Down Expand Up @@ -107,15 +106,13 @@ impl<'a, RNG: RngCore, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, RNG, I2C,
}

/// Show error screen and wait for keypress or timeout
pub async fn show_error<M: fmt::Display>(
&mut self,
message: M,
interactive: bool,
) -> Result<(), Error> {
info!("UI: Displaying error: {}", message);
pub async fn show_error(&mut self, error: &Error) -> Result<(), Error> {
info!("UI: Displaying error: {}", error);

self.display.screen(&screen::Failure::new(error)).await?;

self.display.screen(&screen::Failure::new(message)).await?;
if interactive {
// Sound the error buzzer if the error was caused by a user's interaction
if error.user_id().is_some() {
let _ = self.buzzer.error().await;
}

Expand All @@ -128,7 +125,7 @@ impl<'a, RNG: RngCore, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, RNG, I2C,
// Cancel key cancels
Ok(()) => Ok(()),
// User interaction timeout
Err(TimeoutError) => Err(Error::UserTimeout),
Err(TimeoutError) => Err(ErrorKind::UserTimeout)?,
}
}

Expand All @@ -150,9 +147,9 @@ impl<'a, RNG: RngCore, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, RNG, I2C,
// Network has become available
Ok(Either::First(())) => Ok(()),
// Cancel key cancels
Ok(Either::Second(())) => Err(Error::Cancel),
Ok(Either::Second(())) => Err(ErrorKind::Cancel)?,
// Timeout waiting for network
Err(TimeoutError) => Err(Error::NoNetwork),
Err(TimeoutError) => Err(ErrorKind::NoNetwork)?,
}
}

Expand Down Expand Up @@ -241,39 +238,49 @@ impl<'a, RNG: RngCore, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, RNG, I2C,
// Id card read
Either::First(res) => res?,
// Schedule time
Either::Second(()) => return self.schedule().await,
Either::Second(()) => {
self.schedule().await?;
return Ok(());
}
};

// Get user information
let user = self.users.get(user_id);
let user_name = user.map_or(String::new(), |u| u.name.clone());
Error::try_with_async(user_id, async {
// Get user information
let user = self.users.get(user_id);
let user_name = user.map_or(String::new(), |u| u.name.clone());

// Ask for number of drinks
let num_drinks = self.get_number_of_drinks(&user_name).await?;
// Ask for number of drinks
let num_drinks = self.get_number_of_drinks(&user_name).await?;

// Get article information
let article_id = self.articles.id(0).ok_or(Error::ArticleNotFound)?.clone();
let article = self.articles.get(0).ok_or(Error::ArticleNotFound)?;
// Get article information
let article_id = self
.articles
.id(0)
.ok_or(ErrorKind::ArticleNotFound)?
.clone();
let article = self.articles.get(0).ok_or(ErrorKind::ArticleNotFound)?;

// Calculate total price. It's ok to cast num_drinks to f32 as it's always a small number.
#[allow(clippy::cast_precision_loss)]
let amount = num_drinks as f32;
let total_price = article.price * amount;
// Calculate total price. It's ok to cast num_drinks to f32 as it's always a small number.
#[allow(clippy::cast_precision_loss)]
let amount = num_drinks as f32;
let total_price = article.price * amount;

// Show total price and ask for confirmation
self.confirm_purchase(num_drinks, total_price).await?;
// Show total price and ask for confirmation
self.confirm_purchase(num_drinks, total_price).await?;

// Store purchase
self.purchase(&article_id, amount, user_id, total_price)
.await?;
// Store purchase
self.purchase(&article_id, amount, user_id, total_price)
.await?;

// Submit telemetry data if needed
self.submit_telemetry().await?;
// Submit telemetry data if needed
self.submit_telemetry().await?;

// Show success and affirm to take drinks
self.show_success(num_drinks).await?;
// Show success and affirm to take drinks
self.show_success(num_drinks).await?;

Ok(())
Ok(())
})
.await
}

/// Run schedule
Expand Down Expand Up @@ -350,11 +357,11 @@ impl<RNG: RngCore, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'_, RNG, I2C, IRQ
// Ignore any other digit
Ok(Key::Digit(_)) => (),
// Cancel key cancels
Ok(Key::Cancel) => break Err(Error::Cancel),
Ok(Key::Cancel) => Err(ErrorKind::Cancel)?,
// Ignore any other key
Ok(_) => (),
// User interaction timeout
Err(TimeoutError) => break Err(Error::UserTimeout),
Err(TimeoutError) => Err(ErrorKind::UserTimeout)?,
}
}
}
Expand All @@ -374,11 +381,11 @@ impl<RNG: RngCore, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'_, RNG, I2C, IRQ
// Enter key confirms purchase
Ok(Key::Enter) => break Ok(()),
// Cancel key cancels
Ok(Key::Cancel) => break Err(Error::Cancel),
Ok(Key::Cancel) => Err(ErrorKind::Cancel)?,
// Ignore any other key
Ok(_) => (),
// User interaction timeout
Err(TimeoutError) => break Err(Error::UserTimeout),
Err(TimeoutError) => Err(ErrorKind::UserTimeout)?,
}
}
}
Expand Down Expand Up @@ -435,7 +442,7 @@ impl<RNG: RngCore, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'_, RNG, I2C, IRQ
// Enter key continues
Ok(()) => Ok(()),
// User interaction timeout
Err(TimeoutError) => Err(Error::UserTimeout),
Err(TimeoutError) => Err(ErrorKind::UserTimeout)?,
}
}
}

0 comments on commit 4d982d4

Please sign in to comment.