Skip to content

Commit

Permalink
Fetch users from Vereinsflieger
Browse files Browse the repository at this point in the history
  • Loading branch information
zargony committed Oct 28, 2024
1 parent 4a91dfb commit fe1546d
Show file tree
Hide file tree
Showing 9 changed files with 402 additions and 20 deletions.
1 change: 1 addition & 0 deletions firmware/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions firmware/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ esp-partition-table = "0.1"
esp-println = { version = "0.12", features = ["esp32c3", "log"] }
esp-storage = { version = "0.3", features = ["esp32c3"] }
esp-wifi = { version = "0.10", default-features = false, features = ["esp32c3", "esp-alloc", "async", "embassy-net", "log", "phy-enable-usb", "wifi"] }
hex = { version = "0.4", default-features = false, features = ["alloc"] }
log = { version = "0.4", features = ["release_max_level_info"] }
pn532 = "0.4"
rand_core = "0.6"
Expand Down
1 change: 0 additions & 1 deletion firmware/src/buzzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ impl<'a> Buzzer<'a> {
}

/// Output a long denying tone
#[allow(dead_code)]
pub async fn deny(&mut self) -> Result<(), Error> {
debug!("Buzzer: Playing deny tone");
self.tone(392, Duration::from_millis(500)).await?; // G4
Expand Down
5 changes: 4 additions & 1 deletion firmware/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ mod nfc;
mod pn532;
mod screen;
mod ui;
mod user;
mod vereinsflieger;
mod wifi;

Expand Down Expand Up @@ -130,8 +131,9 @@ async fn main(spawner: Spawner) {
// Read system configuration
let config = config::Config::read().await;

// Initialize list of articles
// Initialize article and user look up tables
let mut articles = article::Articles::new([config.vf_article_id]);
let mut users = user::Users::new();

// Initialize I2C controller
let i2c = I2c::new_with_timeout_async(
Expand Down Expand Up @@ -220,6 +222,7 @@ async fn main(spawner: Spawner) {
&wifi,
&mut vereinsflieger,
&mut articles,
&mut users,
);

// Show splash screen for a while, ignore any error
Expand Down
27 changes: 26 additions & 1 deletion firmware/src/nfc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ use crate::pn532;

use core::convert::Infallible;
use core::fmt::{self, Debug};
use core::str::FromStr;
use embassy_time::{Duration, Timer};
use embedded_hal_async::digital::Wait;
use embedded_hal_async::i2c::I2c;
use hex::FromHex;
use log::{debug, info, warn};
use pn532::{Error as Pn532Error, I2CInterfaceWithIrq, Pn532, Request, SAMMode};

Expand Down Expand Up @@ -181,6 +183,7 @@ impl<I2C: I2c, IRQ: Wait<Error = Infallible>> Nfc<I2C, IRQ> {

// Return UID if retrieved, continue looping otherwise
if let Some(uid) = maybe_uid {
debug!("NFC: Detected NFC card: {}", uid);
return Ok(uid);
}
}
Expand All @@ -192,7 +195,7 @@ impl<I2C: I2c, IRQ: Wait<Error = Infallible>> Nfc<I2C, IRQ> {
pub struct InvalidUid;

/// NFC UID
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Uid {
/// Single Size UID (4 bytes), Mifare Classic
Single([u8; 4]),
Expand All @@ -216,6 +219,28 @@ impl TryFrom<&[u8]> for Uid {
}
}

impl FromStr for Uid {
type Err = InvalidUid;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.len() {
8 => {
let bytes = <[u8; 4]>::from_hex(s).map_err(|_e| InvalidUid)?;
Ok(Self::Single(bytes))
}
14 => {
let bytes = <[u8; 7]>::from_hex(s).map_err(|_e| InvalidUid)?;
Ok(Self::Double(bytes))
}
20 => {
let bytes = <[u8; 10]>::from_hex(s).map_err(|_e| InvalidUid)?;
Ok(Self::Triple(bytes))
}
_ => Err(InvalidUid),
}
}
}

fn write_hex_bytes(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
for b in bytes {
write!(f, "{:02x}", *b)?;
Expand Down
50 changes: 33 additions & 17 deletions firmware/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ use crate::buzzer::Buzzer;
use crate::display::Display;
use crate::error::Error;
use crate::keypad::{Key, Keypad};
use crate::nfc::{Nfc, Uid};
use crate::nfc::Nfc;
use crate::screen;
use crate::user::{UserId, Users};
use crate::vereinsflieger::Vereinsflieger;
use crate::wifi::Wifi;
use core::convert::Infallible;
Expand Down Expand Up @@ -42,10 +43,12 @@ pub struct Ui<'a, I2C, IRQ> {
wifi: &'a Wifi,
vereinsflieger: &'a mut Vereinsflieger<'a>,
articles: &'a mut Articles<1>,
users: &'a mut Users,
}

impl<'a, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, I2C, IRQ> {
/// Create user interface with given human interface devices
#[allow(clippy::too_many_arguments)]
pub fn new(
display: &'a mut Display<I2C>,
keypad: &'a mut Keypad<'a, 3, 4>,
Expand All @@ -54,6 +57,7 @@ impl<'a, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, I2C, IRQ> {
wifi: &'a Wifi,
vereinsflieger: &'a mut Vereinsflieger<'a>,
articles: &'a mut Articles<1>,
users: &'a mut Users,
) -> Self {
Self {
display,
Expand All @@ -63,6 +67,7 @@ impl<'a, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, I2C, IRQ> {
wifi,
vereinsflieger,
articles,
users,
}
}

Expand Down Expand Up @@ -138,12 +143,12 @@ impl<'a, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, I2C, IRQ> {
}
}

/// Refresh article names and prices
pub async fn refresh_articles(&mut self) -> Result<(), Error> {
/// Refresh article and user information
pub async fn refresh_articles_and_users(&mut self) -> Result<(), Error> {
// Wait for network to become available (if not already)
self.wait_network_up().await?;

info!("UI: Refreshing articles...");
info!("UI: Refreshing articles and users...");

self.display
.screen(&screen::PleaseWait::ApiQuerying)
Expand All @@ -154,9 +159,12 @@ impl<'a, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, I2C, IRQ> {
#[cfg(debug_assertions)]
vf.get_user_information().await?;

// Refresh articles
// Refresh article information
vf.refresh_articles(self.articles).await?;

// Refresh user information
vf.refresh_users(self.users).await?;

Ok(())
}

Expand All @@ -165,16 +173,17 @@ impl<'a, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, I2C, IRQ> {
// Wait for network to become available (if not already)
self.wait_network_up().await?;

// Refresh article names and prices
self.refresh_articles().await?;
// Refresh articles and users
self.refresh_articles_and_users().await?;

Ok(())
}

/// Run the user interface flow
pub async fn run(&mut self) -> Result<(), Error> {
// Wait for id card and verify identification
let _uid = self.read_id_card().await?;
let userid = self.authenticate_user().await?;
let _user = self.users.get(userid);
// Ask for number of drinks
let num_drinks = self.get_number_of_drinks().await?;
// Get article price
Expand All @@ -188,15 +197,15 @@ impl<'a, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, I2C, IRQ> {
// TODO: Process payment
let _ = screen::Success::new(num_drinks);
let _ = self.show_error("Not implemented yet", true).await;
let _key = self.keypad.read().await;
Ok(())
}
}

impl<'a, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, I2C, IRQ> {
/// Wait for id card and read it. On idle timeout, enter power saving (turn off display).
/// Any key pressed leaves power saving (turn on display).
async fn read_id_card(&mut self) -> Result<Uid, Error> {
/// Authentication: wait for id card, read it and look up the associated user. On idle timeout,
/// enter power saving (turn off display). Any key pressed leaves power saving (turn on
/// display).
async fn authenticate_user(&mut self) -> Result<UserId, Error> {
info!("UI: Waiting for NFC card...");

let mut saving_power = false;
Expand All @@ -209,7 +218,7 @@ impl<'a, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, I2C, IRQ> {
let uid = match with_timeout(IDLE_TIMEOUT, select(self.nfc.read(), self.keypad.read()))
.await
{
// Id card read
// Id card detected
Ok(Either::First(res)) => res?,
// Key pressed while saving power, leave power saving
Ok(Either::Second(_key)) if saving_power => {
Expand All @@ -225,10 +234,17 @@ impl<'a, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, I2C, IRQ> {
// Otherwise, do nothing
_ => continue,
};
info!("UI: Detected NFC card: {}", uid);
let _ = self.buzzer.confirm().await;
// TODO: Verify identification and return user information
return Ok(uid);
saving_power = false;
// Look up user id by detected NFC uid
if let Some(id) = self.users.id(&uid) {
// User found, authorized
info!("UI: NFC card {} identified as user {}", uid, id);
let _ = self.buzzer.confirm().await;
break Ok(id);
}
// User not found, unauthorized
info!("UI: NFC card {} unknown, rejecting", uid);
let _ = self.buzzer.deny().await;
}
}

Expand Down
85 changes: 85 additions & 0 deletions firmware/src/user.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use crate::nfc::Uid;
use alloc::collections::BTreeMap;
use alloc::string::String;

/// Extra NFC card uids to add
static EXTRA_UIDS: [(Uid, UserId); 2] = [
// Test card #1 (Mifare Classic 1k)
(Uid::Single([0x13, 0xbd, 0x5b, 0x2a]), 1271),
// Test token #1 (Mifare Classic 1k)
(Uid::Single([0xb7, 0xd3, 0x65, 0x26]), 1271),
];

/// User id
/// Equivalent to the Vereinsflieger `memberid` attribute
#[allow(clippy::module_name_repetitions)]
pub type UserId = u32;

/// User information
#[derive(Debug, Clone, PartialEq)]
pub struct User {
// pub uids: Vec<Uid>,
// pub id: UserId,
pub name: String,
}

/// User lookup table
/// Provides a look up of user information (member id and name) by NFC uid.
#[derive(Debug)]
pub struct Users {
/// Look up NFC uid to user id
ids: BTreeMap<Uid, UserId>,
/// Look up user id to user details
users: BTreeMap<UserId, User>,
}

impl Users {
/// Create new user lookup table
pub fn new() -> Self {
let mut this = Self {
ids: BTreeMap::new(),
users: BTreeMap::new(),
};
this.clear();
this
}

/// Clear all user information
pub fn clear(&mut self) {
self.ids.clear();
self.users.clear();

// Add extra uids and user for testing
for (uid, id) in &EXTRA_UIDS {
self.ids.insert(uid.clone(), *id);
self.users.entry(*id).or_insert_with(|| User {
name: String::from("Test-User"),
});
}
}

/// Add/update NFC uid for given user id
pub fn update_uid(&mut self, uid: Uid, id: UserId) {
self.ids.insert(uid, id);
}

/// Add/update user with given user id
pub fn update_user(&mut self, id: UserId, name: String) {
self.users.insert(id, User { name });
}

/// Number of users
pub fn count(&self) -> usize {
self.users.len()
}

/// Look up user id by NFC uid
pub fn id(&self, uid: &Uid) -> Option<UserId> {
self.ids.get(uid).copied()
}

/// Look up user by user id
pub fn get(&self, id: UserId) -> Option<&User> {
self.users.get(&id)
}
}
38 changes: 38 additions & 0 deletions firmware/src/vereinsflieger/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
mod proto_articles;
mod proto_auth;
mod proto_user;

use crate::article::Articles;
use crate::http::{self, Http};
use crate::user::Users;
use crate::wifi::Wifi;
use alloc::string::String;
use core::cell::RefCell;
Expand Down Expand Up @@ -166,6 +168,42 @@ impl<'a> Connection<'a> {

Ok(())
}

/// Fetch list of users and update user lookup table
pub async fn refresh_users(&mut self, users: &mut Users) -> Result<(), Error> {
use proto_user::{UserListRequest, UserListResponse};

debug!("Vereinsflieger: Refreshing users...");
let request_body = http::Connection::prepare_body(&UserListRequest {
accesstoken: &self.accesstoken,
})
.await?;
let mut rx_buf = [0; 4096];
let mut json = self
.connection
.post_json("user/list", &request_body, &mut rx_buf)
.await?;

users.clear();
let users = RefCell::new(users);

let response: UserListResponse = json
.read_object_with_context(&users)
.await
.map_err(http::Error::MalformedResponse)?;
info!(
"Vereinsflieger: Refreshed {} of {} users",
users.borrow().count(),
response.total_users
);

// Discard remaining body (needed to make the next pipelined request work)
json.discard_to_end()
.await
.map_err(http::Error::MalformedResponse)?;

Ok(())
}
}

impl<'a> Connection<'a> {
Expand Down
Loading

0 comments on commit fe1546d

Please sign in to comment.