Skip to content

Commit

Permalink
Merge branch 'auto-update'
Browse files Browse the repository at this point in the history
  • Loading branch information
zargony committed Dec 9, 2024
2 parents 12a3d15 + 7a1e870 commit 578f545
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 27 deletions.
2 changes: 2 additions & 0 deletions firmware/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

- Automatically refresh article and user information once a day

## 0.2.0 - 2024-11-27

- Show random greetings to user
Expand Down
7 changes: 3 additions & 4 deletions firmware/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::json::{self, FromJson, ToJson};
use crate::time;
use crate::wifi::{DnsSocket, TcpClient, TcpConnection, Wifi};
use alloc::vec::Vec;
use chrono::{DateTime, Utc};
use chrono::DateTime;
use core::convert::Infallible;
use core::{fmt, str};
use embedded_io_async::{BufRead, Read};
Expand Down Expand Up @@ -219,10 +219,9 @@ impl Connection<'_> {
.headers()
.find_map(|(k, v)| (k == "Date").then_some(v))
.and_then(|v| str::from_utf8(v).ok())
.and_then(|s| DateTime::parse_from_rfc2822(s).ok())
.map(|d| d.with_timezone(&Utc));
.and_then(|s| DateTime::parse_from_rfc2822(s).ok());
if let Some(time) = time {
time::set(time);
time::set(&time);
}

// Check HTTP response status
Expand Down
8 changes: 5 additions & 3 deletions firmware/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ mod json;
mod keypad;
mod nfc;
mod pn532;
mod schedule;
mod screen;
mod time;
mod ui;
Expand Down Expand Up @@ -217,6 +218,9 @@ async fn main(spawner: Spawner) {
let mut buzzer = buzzer::Buzzer::new(peripherals.LEDC, peripherals.GPIO4);
let _ = buzzer.startup().await;

// Initialize scheduler
let mut schedule = schedule::Daily::new();

// Create UI
let mut ui = ui::Ui::new(
rng,
Expand All @@ -229,11 +233,9 @@ async fn main(spawner: Spawner) {
&mut vereinsflieger,
&mut articles,
&mut users,
&mut schedule,
);

// Show splash screen for a while, ignore any error
let _ = ui.show_splash().await;

loop {
match ui.init().await {
// Success or cancel: continue
Expand Down
2 changes: 1 addition & 1 deletion firmware/src/nfc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ impl<I2C: I2c, IRQ: Wait<Error = Infallible>> Nfc<I2C, IRQ> {
)
.await
{
Ok(res) => res,
Ok(bytes) => bytes,
// On timeout (no target detected), restart detection
Err(Pn532Error::TimeoutResponse) => continue,
// Error listing targets, cancel loop and return
Expand Down
70 changes: 70 additions & 0 deletions firmware/src/schedule.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use core::fmt;
use embassy_time::Timer;
use embassy_time::{Duration, Instant};
use log::info;

/// Simple time interval of 24h
#[cfg(not(debug_assertions))]
const DAILY_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60);
#[cfg(debug_assertions)]
const DAILY_INTERVAL: Duration = Duration::from_secs(30 * 60);

/// Duration display helper
struct DisplayDuration(Duration);

impl fmt::Display for DisplayDuration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let hours = self.0.as_secs() / 3600;
let min = self.0.as_secs() % 3600 / 60;
let secs = self.0.as_secs() % 60;
write!(f, "{hours}h{min}m{secs}s")
}
}

/// Scheduler for daily events
#[derive(Debug)]
pub struct Daily {
next: Instant,
}

impl Daily {
/// Create new daily scheduler
pub fn new() -> Self {
let mut daily = Self {
next: Instant::now(),
};
daily.schedule_next();
daily
}

/// Returns true when schedule time is expired
pub fn is_expired(&self) -> bool {
self.next <= Instant::now()
}

/// Time left until schedule time
pub fn time_left(&self) -> Duration {
self.next.saturating_duration_since(Instant::now())
}

/// Timer that can be awaited on to wait for schedule time
pub fn timer(&self) -> Timer {
Timer::at(self.next)
}

/// After expiring, schedule next event
pub fn schedule_next(&mut self) {
if self.is_expired() {
// Simple schedule: run again 24h later
self.next += DAILY_INTERVAL;
}
if self.is_expired() {
// Simple schedule: run in 24h from now
self.next = Instant::now() + DAILY_INTERVAL;
}
info!(
"Schedule: next daily event scheduled in {} from now",
DisplayDuration(self.time_left())
);
}
}
7 changes: 4 additions & 3 deletions firmware/src/time.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use chrono::{DateTime, TimeDelta, Utc};
use chrono::{DateTime, TimeDelta, TimeZone, Utc};
use core::cell::RefCell;
use embassy_sync::blocking_mutex::CriticalSectionMutex;
use embassy_time::Instant;
Expand Down Expand Up @@ -49,13 +49,14 @@ pub fn uptime() -> Option<TimeDelta> {
Instant::now().to_duration()
}

/// Current time
/// Current time. Always given in UTC since the local timezone is unknown.
pub fn now() -> Option<DateTime<Utc>> {
Instant::now().to_datetime()
}

/// Set current time by using the given current time to calculate the time of system start
pub fn set(now: DateTime<Utc>) {
pub fn set<TZ: TimeZone>(now: &DateTime<TZ>) {
let now = now.with_timezone(&Utc);
if let Some(uptime) = uptime() {
set_start_time(now - uptime);
debug!("Time: Current time set to {}", now);
Expand Down
61 changes: 45 additions & 16 deletions firmware/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::error::Error;
use crate::http::Http;
use crate::keypad::{Key, Keypad};
use crate::nfc::Nfc;
use crate::schedule::Daily;
use crate::screen;
use crate::user::{UserId, Users};
use crate::vereinsflieger::Vereinsflieger;
Expand Down Expand Up @@ -49,6 +50,7 @@ pub struct Ui<'a, RNG, I2C, IRQ> {
vereinsflieger: &'a mut Vereinsflieger<'a>,
articles: &'a mut Articles<1>,
users: &'a mut Users,
schedule: &'a mut Daily,
}

impl<'a, RNG: RngCore, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, RNG, I2C, IRQ> {
Expand All @@ -65,6 +67,7 @@ impl<'a, RNG: RngCore, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, RNG, I2C,
vereinsflieger: &'a mut Vereinsflieger<'a>,
articles: &'a mut Articles<1>,
users: &'a mut Users,
schedule: &'a mut Daily,
) -> Self {
Self {
rng,
Expand All @@ -77,6 +80,7 @@ impl<'a, RNG: RngCore, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, RNG, I2C,
vereinsflieger,
articles,
users,
schedule,
}
}

Expand All @@ -94,12 +98,8 @@ impl<'a, RNG: RngCore, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, RNG, I2C,

self.display.screen(&screen::Splash).await?;

match with_timeout(SPLASH_TIMEOUT, self.keypad.read()).await {
// Key pressed
Ok(_key) => Ok(()),
// User interaction timeout
Err(TimeoutError) => Err(Error::UserTimeout),
}
let _ = with_timeout(SPLASH_TIMEOUT, self.keypad.read()).await;
Ok(())
}

/// Show error screen and wait for keypress or timeout
Expand Down Expand Up @@ -162,6 +162,8 @@ impl<'a, RNG: RngCore, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, RNG, I2C,
self.display
.screen(&screen::PleaseWait::UpdatingData)
.await?;

// Connect to Vereinsflieger API
let mut vf = self.vereinsflieger.connect(self.http).await?;

// Show authenticated user information when debugging
Expand All @@ -179,6 +181,9 @@ impl<'a, RNG: RngCore, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, RNG, I2C,

/// Initialize user interface
pub async fn init(&mut self) -> Result<(), Error> {
// Show splash screen for a while
self.show_splash().await?;

// Wait for network to become available (if not already)
self.wait_network_up().await?;

Expand All @@ -190,13 +195,21 @@ impl<'a, RNG: RngCore, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, RNG, I2C,

/// Run the user interface flow
pub async fn run(&mut self) -> Result<(), Error> {
// Wait for id card and verify identification
let user_id = self.authenticate_user().await?;
// Either wait for id card read or schedule time
let schedule_timer = self.schedule.timer();
let user_id = match select(self.authenticate_user(), schedule_timer).await {
// Id card read
Either::First(res) => res?,
// Schedule time
Either::Second(()) => return self.schedule().await,
};

// 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 name = user.map_or(String::new(), |u| u.name.clone());
let num_drinks = self.get_number_of_drinks(&name).await?;
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();
Expand All @@ -219,6 +232,20 @@ impl<'a, RNG: RngCore, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, RNG, I2C,

Ok(())
}

/// Run schedule
pub async fn schedule(&mut self) -> Result<(), Error> {
if self.schedule.is_expired() {
info!("UI: Running schedule...");

// Schedule next event
self.schedule.schedule_next();

// Refresh article and user information
self.refresh_articles_and_users().await?;
}
Ok(())
}
}

impl<RNG: RngCore, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'_, RNG, I2C, IRQ> {
Expand Down Expand Up @@ -274,15 +301,15 @@ impl<RNG: RngCore, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'_, RNG, I2C, IRQ
#[allow(clippy::match_same_arms)]
match with_timeout(USER_TIMEOUT, self.keypad.read()).await {
// Any digit 1..=9 selects number of drinks
Ok(Key::Digit(n)) if (1..=9).contains(&n) => return Ok(n as usize),
Ok(Key::Digit(n)) if (1..=9).contains(&n) => break Ok(n as usize),
// Ignore any other digit
Ok(Key::Digit(_)) => (),
// Cancel key cancels
Ok(Key::Cancel) => return Err(Error::Cancel),
Ok(Key::Cancel) => break Err(Error::Cancel),
// Ignore any other key
Ok(_) => (),
// User interaction timeout
Err(TimeoutError) => return Err(Error::UserTimeout),
Err(TimeoutError) => break Err(Error::UserTimeout),
}
}
}
Expand All @@ -300,13 +327,13 @@ impl<RNG: RngCore, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'_, RNG, I2C, IRQ
loop {
match with_timeout(USER_TIMEOUT, self.keypad.read()).await {
// Enter key confirms purchase
Ok(Key::Enter) => return Ok(()),
Ok(Key::Enter) => break Ok(()),
// Cancel key cancels
Ok(Key::Cancel) => return Err(Error::Cancel),
Ok(Key::Cancel) => break Err(Error::Cancel),
// Ignore any other key
Ok(_) => (),
// User interaction timeout
Err(TimeoutError) => return Err(Error::UserTimeout),
Err(TimeoutError) => break Err(Error::UserTimeout),
}
}
}
Expand All @@ -328,6 +355,8 @@ impl<RNG: RngCore, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'_, RNG, I2C, IRQ
);

self.display.screen(&screen::PleaseWait::Purchasing).await?;

// Connect to Vereinsflieger API
let mut vf = self.vereinsflieger.connect(self.http).await?;

// Store purchase
Expand Down

0 comments on commit 578f545

Please sign in to comment.