mirror of
https://github.com/Djeeberjr/fw-anwesenheit.git
synced 2026-05-01 02:59:09 +00:00
Compare commits
8 Commits
fb8d98da28
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1731cff9c9 | |||
| 336249fd12 | |||
| 5ffeb969e2 | |||
| a4ab0bad48 | |||
| 02798d90c4 | |||
| 07d51264f9 | |||
| 6cc6df6298 | |||
| 7d78aa8c4c |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/pcb/bom/ibom.html linguist-vendored
|
||||
@@ -19,7 +19,7 @@ esp-radio = { version = "0.16.0", features = ["esp32c6","esp-alloc", "wifi", "lo
|
||||
esp-rtos = { version = "0.1.1", features = ["esp32c6", "embassy", "esp-radio", "esp-alloc"] }
|
||||
|
||||
critical-section = "1.2.0"
|
||||
log = { version = "0.4" }
|
||||
log = { version = "0.4", features = ["release_max_level_info"]}
|
||||
static_cell = { version = "2.1.1", features = ["nightly"] }
|
||||
heapless = { version = "0.8.0", default-features = false }
|
||||
chrono = { version = "0.4.41", default-features = false }
|
||||
|
||||
@@ -52,7 +52,7 @@ pub async fn rtc_config(i2c: I2c<'static, Async>) -> DS3231<I2c<'static, Async>>
|
||||
square_wave_frequency: SquareWaveFrequency::Hz1,
|
||||
interrupt_control: InterruptControl::Interrupt, // Enable interrupt mode
|
||||
battery_backed_square_wave: false,
|
||||
oscillator_enable: Oscillator::Disabled,
|
||||
oscillator_enable: Oscillator::Enabled,
|
||||
};
|
||||
|
||||
match rtc.configure(&rtc_config).await {
|
||||
@@ -83,5 +83,8 @@ pub async fn rtc_config(i2c: I2c<'static, Async>) -> DS3231<I2c<'static, Async>>
|
||||
}
|
||||
Err(e) => info!("Failed to read status: {:?}", e),
|
||||
}
|
||||
|
||||
info!("RTC time is: {:?}", rtc.datetime().await.unwrap());
|
||||
|
||||
rtc
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
use alloc::vec::Vec;
|
||||
use alloc::{string::ToString, vec::Vec};
|
||||
use core::str::from_utf8;
|
||||
use embassy_time::Delay;
|
||||
use embedded_hal_bus::spi::ExclusiveDevice;
|
||||
use embedded_sdmmc::{SdCard, ShortFileName, TimeSource, Timestamp, VolumeIdx, VolumeManager};
|
||||
use embedded_sdmmc::{
|
||||
SdCard, SdCardError, ShortFileName, TimeSource, Timestamp, VolumeIdx, VolumeManager,
|
||||
};
|
||||
use esp_hal::{Blocking, gpio::Output, spi::master::Spi};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::store::{AttendanceDay, IDMapping, day::Day, persistence::Persistence};
|
||||
use crate::store::{
|
||||
AttendanceDay, day::Day, mapping_loader::Name, persistence::Persistence, tally_id::TallyID,
|
||||
};
|
||||
|
||||
pub struct DummyTimesource;
|
||||
|
||||
@@ -39,113 +45,231 @@ pub struct SDCardPersistence {
|
||||
}
|
||||
|
||||
impl SDCardPersistence {
|
||||
const MAPPING_FILENAME: &'static str = "MAPPING.JS";
|
||||
const MAPPING_DIRNAME: &'static str = "MAPPINGS";
|
||||
|
||||
fn generate_filename(day: Day) -> ShortFileName {
|
||||
fn generate_filename_for_day(day: Day) -> Result<ShortFileName, PersistenceError> {
|
||||
let basename = day.to_string();
|
||||
let mut filename: heapless::String<11> = heapless::String::new();
|
||||
filename.push_str(&basename).unwrap();
|
||||
filename.push_str(".js").unwrap();
|
||||
filename
|
||||
.push_str(&basename)
|
||||
.map_err(|_| PersistenceError::DayFilename)?;
|
||||
filename
|
||||
.push_str(".JS")
|
||||
.map_err(|_| PersistenceError::DayFilename)?;
|
||||
|
||||
ShortFileName::create_from_str(&filename).unwrap()
|
||||
ShortFileName::create_from_str(&filename).map_err(|_| PersistenceError::DayFilename)
|
||||
}
|
||||
|
||||
fn generate_path_for_id(
|
||||
id: TallyID,
|
||||
) -> Result<(ShortFileName, ShortFileName), PersistenceError> {
|
||||
let basename: heapless::String<12> = id.into();
|
||||
let (dir, file) = basename.split_at(6);
|
||||
|
||||
let mut filename: heapless::String<11> = heapless::String::new();
|
||||
filename
|
||||
.push_str(file)
|
||||
.map_err(|_| PersistenceError::IDFilename)?;
|
||||
filename
|
||||
.push_str(".JS")
|
||||
.map_err(|_| PersistenceError::IDFilename)?;
|
||||
|
||||
let mut dirname: heapless::String<11> = heapless::String::new();
|
||||
dirname
|
||||
.push_str(dir)
|
||||
.map_err(|_| PersistenceError::IDFilename)?;
|
||||
|
||||
Ok((
|
||||
ShortFileName::create_from_str(&dirname).map_err(|_| PersistenceError::IDFilename)?,
|
||||
ShortFileName::create_from_str(&filename).map_err(|_| PersistenceError::IDFilename)?,
|
||||
))
|
||||
}
|
||||
|
||||
fn get_tallyid_from_path(
|
||||
dirname: &ShortFileName,
|
||||
filename: &ShortFileName,
|
||||
) -> Result<TallyID, PersistenceError> {
|
||||
let mut id_str: heapless::String<12> = heapless::String::new();
|
||||
|
||||
id_str
|
||||
.push_str(&dirname.to_string())
|
||||
.map_err(|_| PersistenceError::IDFilename)?;
|
||||
id_str
|
||||
.push_str(from_utf8(filename.base_name()).map_err(|_| PersistenceError::IDFilename)?)
|
||||
.map_err(|_| PersistenceError::IDFilename)?;
|
||||
|
||||
let id: TallyID = id_str
|
||||
.try_into()
|
||||
.map_err(|_| PersistenceError::IDFilename)?;
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum PersistenceError {
|
||||
#[error("Failed to interact with SD card")]
|
||||
SdCard(embedded_sdmmc::Error<SdCardError>),
|
||||
|
||||
#[error("Failed to parse data")]
|
||||
Parseing(#[from] serde_json::Error),
|
||||
|
||||
#[error("Failed to parse Day and Filename")]
|
||||
DayFilename,
|
||||
|
||||
#[error("Failed to parse TallyID for file path")]
|
||||
IDFilename,
|
||||
|
||||
#[error("Item not found")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl From<embedded_sdmmc::Error<SdCardError>> for PersistenceError {
|
||||
fn from(err: embedded_sdmmc::Error<SdCardError>) -> Self {
|
||||
PersistenceError::SdCard(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl Persistence for SDCardPersistence {
|
||||
async fn load_day(&mut self, day: Day) -> Option<AttendanceDay> {
|
||||
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
|
||||
let mut root_dir = vol_0.open_root_dir().unwrap();
|
||||
type Error = PersistenceError;
|
||||
|
||||
let filename = Self::generate_filename(day);
|
||||
async fn load_day(&mut self, day: Day) -> Result<AttendanceDay, Self::Error> {
|
||||
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0))?;
|
||||
let mut root_dir = vol_0.open_root_dir()?;
|
||||
|
||||
let filename = Self::generate_filename_for_day(day)?;
|
||||
let file = root_dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadOnly);
|
||||
|
||||
if file.is_err() {
|
||||
return None;
|
||||
return Err(PersistenceError::NotFound);
|
||||
}
|
||||
|
||||
let mut open_file = file.unwrap();
|
||||
let mut open_file = file?;
|
||||
|
||||
let mut read_buffer: [u8; 1024] = [0; 1024];
|
||||
let read = open_file.read(&mut read_buffer).unwrap();
|
||||
open_file.close().unwrap();
|
||||
let read = open_file.read(&mut read_buffer)?;
|
||||
open_file.close()?;
|
||||
|
||||
let day: AttendanceDay = serde_json::from_slice(&read_buffer[..read]).unwrap();
|
||||
let day: AttendanceDay = serde_json::from_slice(&read_buffer[..read])?;
|
||||
|
||||
Some(day)
|
||||
Ok(day)
|
||||
}
|
||||
|
||||
async fn save_day(&mut self, day: Day, data: &AttendanceDay) {
|
||||
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
|
||||
let mut root_dir = vol_0.open_root_dir().unwrap();
|
||||
async fn save_day(&mut self, day: Day, data: &AttendanceDay) -> Result<(), Self::Error> {
|
||||
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0))?;
|
||||
let mut root_dir = vol_0.open_root_dir()?;
|
||||
|
||||
let filename = Self::generate_filename(day);
|
||||
let filename = Self::generate_filename_for_day(day)?;
|
||||
|
||||
let mut file = root_dir
|
||||
.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadWriteCreateOrTruncate)
|
||||
.unwrap();
|
||||
let mut file =
|
||||
root_dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadWriteCreateOrTruncate)?;
|
||||
|
||||
file.write(&serde_json::to_vec(data).unwrap()).unwrap();
|
||||
file.write(&serde_json::to_vec(data)?)?;
|
||||
|
||||
file.flush().unwrap();
|
||||
file.close().unwrap();
|
||||
file.flush()?;
|
||||
file.close()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_mapping(&mut self) -> Option<crate::store::IDMapping> {
|
||||
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
|
||||
let mut root_dir = vol_0.open_root_dir().unwrap();
|
||||
async fn list_days(&mut self) -> Result<Vec<Day>, Self::Error> {
|
||||
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0))?;
|
||||
let mut root_dir = vol_0.open_root_dir()?;
|
||||
|
||||
let file =
|
||||
root_dir.open_file_in_dir(Self::MAPPING_FILENAME, embedded_sdmmc::Mode::ReadOnly);
|
||||
|
||||
if file.is_err() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut open_file = file.unwrap();
|
||||
|
||||
let mut read_buffer: [u8; 1024] = [0; 1024];
|
||||
let read = open_file.read(&mut read_buffer).unwrap();
|
||||
open_file.close().unwrap();
|
||||
|
||||
let mapping: IDMapping = serde_json::from_slice(&read_buffer[..read]).unwrap();
|
||||
|
||||
Some(mapping)
|
||||
}
|
||||
|
||||
async fn save_mapping(&mut self, data: &crate::store::IDMapping) {
|
||||
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
|
||||
let mut root_dir = vol_0.open_root_dir().unwrap();
|
||||
|
||||
let mut file = root_dir
|
||||
.open_file_in_dir(
|
||||
Self::MAPPING_FILENAME,
|
||||
embedded_sdmmc::Mode::ReadWriteCreateOrTruncate,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
file.write(&serde_json::to_vec(data).unwrap()).unwrap();
|
||||
|
||||
file.flush().unwrap();
|
||||
file.close().unwrap();
|
||||
}
|
||||
|
||||
async fn list_days(&mut self) -> Vec<Day> {
|
||||
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
|
||||
let mut root_dir = vol_0.open_root_dir().unwrap();
|
||||
|
||||
let mut days_dir = root_dir.open_dir(".").unwrap();
|
||||
let mut days_dir = root_dir.open_dir(".")?;
|
||||
|
||||
let mut days: Vec<Day> = Vec::new();
|
||||
days_dir
|
||||
.iterate_dir(|e| {
|
||||
days_dir.iterate_dir(|e| {
|
||||
let filename = e.name.clone();
|
||||
|
||||
if let Ok(day) = filename.try_into() {
|
||||
days.push(day);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
})?;
|
||||
|
||||
days
|
||||
Ok(days)
|
||||
}
|
||||
|
||||
async fn load_mapping_for_id(
|
||||
&mut self,
|
||||
id: crate::store::tally_id::TallyID,
|
||||
) -> Result<crate::store::mapping_loader::Name, Self::Error> {
|
||||
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0))?;
|
||||
let mut root_dir = vol_0.open_root_dir()?;
|
||||
let mut mapping_dir = root_dir.open_dir(Self::MAPPING_DIRNAME)?;
|
||||
|
||||
let (dirname, filename) = Self::generate_path_for_id(id)?;
|
||||
|
||||
let mut dir = mapping_dir.open_dir(dirname)?;
|
||||
let mut file = dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadOnly)?;
|
||||
|
||||
let mut read_buffer: [u8; 1024] = [0; 1024];
|
||||
let read_bytes = file.read(&mut read_buffer)?;
|
||||
file.close()?;
|
||||
|
||||
let mapping: Name = serde_json::from_slice(&read_buffer[..read_bytes])?;
|
||||
|
||||
Ok(mapping)
|
||||
}
|
||||
|
||||
async fn save_mapping_for_id(
|
||||
&mut self,
|
||||
id: crate::store::tally_id::TallyID,
|
||||
name: crate::store::mapping_loader::Name,
|
||||
) -> Result<(), Self::Error> {
|
||||
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0))?;
|
||||
let mut root_dir = vol_0.open_root_dir()?;
|
||||
let mut mapping_dir = root_dir.open_dir(Self::MAPPING_DIRNAME)?;
|
||||
|
||||
let (dirname, filename) = Self::generate_path_for_id(id)?;
|
||||
|
||||
let mut dir = if let Ok(dir) = mapping_dir.open_dir(&dirname) {
|
||||
dir
|
||||
} else {
|
||||
mapping_dir.make_dir_in_dir(&dirname)?;
|
||||
mapping_dir.open_dir(&dirname)?
|
||||
};
|
||||
|
||||
let mut file =
|
||||
dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadWriteCreateOrTruncate)?;
|
||||
|
||||
file.write(&serde_json::to_vec(&name)?)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_mappings(&mut self) -> Result<Vec<TallyID>, Self::Error> {
|
||||
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0))?;
|
||||
let mut root_dir = vol_0.open_root_dir()?;
|
||||
let mut mapping_dir = root_dir.open_dir(Self::MAPPING_DIRNAME)?;
|
||||
let mut ids: Vec<TallyID> = Vec::new();
|
||||
|
||||
let mut dir_names = Vec::new();
|
||||
mapping_dir.iterate_dir(|entry| {
|
||||
if entry.attributes.is_directory()
|
||||
&& entry.name.to_string() != "."
|
||||
&& entry.name.to_string() != ".."
|
||||
{
|
||||
dir_names.push(entry.name.clone());
|
||||
}
|
||||
})?;
|
||||
|
||||
for dirname in dir_names {
|
||||
if let Ok(mut subdir) = mapping_dir.open_dir(&dirname) {
|
||||
let mut file_names = Vec::new();
|
||||
subdir.iterate_dir(|file_entry| {
|
||||
if !file_entry.attributes.is_directory() {
|
||||
file_names.push(file_entry.name.clone());
|
||||
}
|
||||
})?;
|
||||
|
||||
for filename in file_names {
|
||||
let id = Self::get_tallyid_from_path(&dirname, &filename)?;
|
||||
ids.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ids)
|
||||
}
|
||||
}
|
||||
|
||||
11
src/main.rs
11
src/main.rs
@@ -26,7 +26,7 @@ extern crate alloc;
|
||||
|
||||
use crate::{
|
||||
init::{hardware::AppHardware, sd_card::SDCardPersistence},
|
||||
store::{IDStore, day::Day, tally_id::TallyID},
|
||||
store::{IDStore, day::Day, mapping_loader::MappingLoader, tally_id::TallyID},
|
||||
webserver::start_webserver,
|
||||
};
|
||||
|
||||
@@ -54,9 +54,15 @@ async fn main(spawner: Spawner) -> ! {
|
||||
let mut rtc = drivers::rtc::RTCClock::new(app_hardware.i2c).await;
|
||||
|
||||
let current_day: Day = rtc.get_time().await.into();
|
||||
let store: UsedStore = IDStore::new_from_storage(app_hardware.sdcard, current_day).await;
|
||||
|
||||
let shared_sdcard: Rc<Mutex<CriticalSectionRawMutex, SDCardPersistence>> =
|
||||
Rc::new(Mutex::new(app_hardware.sdcard));
|
||||
|
||||
let store: UsedStore = IDStore::new_from_storage(shared_sdcard.clone(), current_day).await;
|
||||
let shared_store = Rc::new(Mutex::new(store));
|
||||
|
||||
let mapping_loader = MappingLoader::new(shared_sdcard.clone());
|
||||
|
||||
let chan: &'static mut TallyChannel = CHAN.init(PubSubChannel::new());
|
||||
let publisher: TallyPublisher = chan.publisher().unwrap();
|
||||
let mut sub: TallySubscriber = chan.subscriber().unwrap();
|
||||
@@ -68,6 +74,7 @@ async fn main(spawner: Spawner) -> ! {
|
||||
app_hardware.network_stack,
|
||||
shared_store.clone(),
|
||||
chan,
|
||||
mapping_loader,
|
||||
);
|
||||
|
||||
/****************************** Spawning tasks ***********************************/
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::string::String;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::store::tally_id::TallyID;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Name {
|
||||
pub first: String,
|
||||
pub last: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct IDMapping {
|
||||
#[serde(flatten)]
|
||||
id_map: BTreeMap<TallyID, Name>,
|
||||
}
|
||||
|
||||
impl IDMapping {
|
||||
pub fn new() -> Self {
|
||||
IDMapping {
|
||||
id_map: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map(&self, id: &TallyID) -> Option<&Name> {
|
||||
self.id_map.get(id)
|
||||
}
|
||||
|
||||
pub fn add_mapping(&mut self, id: TallyID, name: Name) {
|
||||
self.id_map.insert(id, name);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
use alloc::rc::Rc;
|
||||
use alloc::vec::Vec;
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_sync::mutex::Mutex;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::IDMapping;
|
||||
use crate::store::day::Day;
|
||||
use crate::store::persistence::Persistence;
|
||||
use crate::store::tally_id::TallyID;
|
||||
@@ -34,40 +36,36 @@ impl AttendanceDay {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IDStore<T: Persistence> {
|
||||
pub current_day: AttendanceDay,
|
||||
pub mapping: IDMapping,
|
||||
persistence_layer: T,
|
||||
current_day: AttendanceDay,
|
||||
persistence_layer: Rc<Mutex<CriticalSectionRawMutex, T>>,
|
||||
}
|
||||
|
||||
impl<T: Persistence> IDStore<T> {
|
||||
pub async fn new_from_storage(mut persistence_layer: T, current_date: Day) -> Self {
|
||||
let mapping = match persistence_layer.load_mapping().await {
|
||||
Some(map) => map,
|
||||
None => IDMapping::new(),
|
||||
};
|
||||
|
||||
pub async fn new_from_storage(
|
||||
persistence_layer: Rc<Mutex<CriticalSectionRawMutex, T>>,
|
||||
current_date: Day,
|
||||
) -> Self {
|
||||
let day = persistence_layer
|
||||
.lock()
|
||||
.await
|
||||
.load_day(current_date)
|
||||
.await
|
||||
.unwrap_or(AttendanceDay::new(current_date));
|
||||
|
||||
Self {
|
||||
current_day: day,
|
||||
mapping,
|
||||
persistence_layer,
|
||||
}
|
||||
}
|
||||
|
||||
async fn persist_day(&mut self) {
|
||||
async fn persist_day(&mut self) -> Result<(), T::Error> {
|
||||
self.persistence_layer
|
||||
.lock()
|
||||
.await
|
||||
.save_day(self.current_day.date, &self.current_day)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn persist_mapping(&mut self) {
|
||||
self.persistence_layer.save_mapping(&self.mapping).await
|
||||
}
|
||||
|
||||
/// Add a new id for the current day
|
||||
/// Returns false if ID is already present at the current day.
|
||||
pub async fn add_id(&mut self, id: TallyID, current_date: Day) -> bool {
|
||||
@@ -90,21 +88,25 @@ impl<T: Persistence> IDStore<T> {
|
||||
}
|
||||
|
||||
/// Load and return a AttendanceDay. Nothing more. Nothing less.
|
||||
pub async fn load_day(&mut self, day: Day) -> Option<AttendanceDay> {
|
||||
pub async fn load_day(&mut self, day: Day) -> Result<AttendanceDay, T::Error> {
|
||||
if day == self.current_day.date {
|
||||
return Some(self.current_day.clone());
|
||||
return Ok(self.current_day.clone());
|
||||
}
|
||||
|
||||
self.persistence_layer.load_day(day).await
|
||||
self.persistence_layer.lock().await.load_day(day).await
|
||||
}
|
||||
|
||||
pub async fn list_days_in_timespan(&mut self, from: Day, to: Day) -> Vec<Day> {
|
||||
let all_days = self.persistence_layer.list_days().await;
|
||||
pub async fn list_days_in_timespan(
|
||||
&mut self,
|
||||
from: Day,
|
||||
to: Day,
|
||||
) -> Result<Vec<Day>, T::Error> {
|
||||
let all_days = self.persistence_layer.lock().await.list_days().await?;
|
||||
|
||||
all_days
|
||||
Ok(all_days
|
||||
.into_iter()
|
||||
.filter(|e| *e >= from)
|
||||
.filter(|e| *e <= to)
|
||||
.collect()
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
31
src/store/mapping_loader.rs
Normal file
31
src/store/mapping_loader.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use alloc::{rc::Rc, string::String, vec::Vec};
|
||||
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::store::{persistence::Persistence, tally_id::TallyID};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Name {
|
||||
pub first: String,
|
||||
pub last: String,
|
||||
}
|
||||
|
||||
pub struct MappingLoader<T: Persistence>(Rc<Mutex<CriticalSectionRawMutex, T>>);
|
||||
|
||||
impl<T: Persistence> MappingLoader<T> {
|
||||
pub fn new(persistence_layer: Rc<Mutex<CriticalSectionRawMutex, T>>) -> Self {
|
||||
Self(persistence_layer)
|
||||
}
|
||||
|
||||
pub async fn get_mapping(&self, id: TallyID) -> Result<Name, T::Error> {
|
||||
self.0.lock().await.load_mapping_for_id(id).await
|
||||
}
|
||||
|
||||
pub async fn set_mapping(&self, id: TallyID, name: Name) -> Result<(), T::Error> {
|
||||
self.0.lock().await.save_mapping_for_id(id, name).await
|
||||
}
|
||||
|
||||
pub async fn list_mappings(&self) -> Result<Vec<TallyID>,T::Error> {
|
||||
self.0.lock().await.list_mappings().await
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
pub use id_mapping::{IDMapping, Name};
|
||||
pub use id_store::{IDStore,AttendanceDay};
|
||||
|
||||
mod id_mapping;
|
||||
pub mod persistence;
|
||||
mod id_store;
|
||||
pub mod tally_id;
|
||||
pub mod day;
|
||||
|
||||
pub mod mapping_loader;
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::store::{IDMapping, day::Day, id_store::AttendanceDay};
|
||||
use crate::store::{day::Day, id_store::AttendanceDay, mapping_loader::Name, tally_id::TallyID};
|
||||
|
||||
pub trait Persistence {
|
||||
async fn load_day(&mut self, day: Day) -> Option<AttendanceDay>;
|
||||
async fn save_day(&mut self, day: Day, data: &AttendanceDay);
|
||||
async fn list_days(&mut self) -> Vec<Day>;
|
||||
type Error: core::error::Error;
|
||||
|
||||
async fn load_mapping(&mut self) -> Option<IDMapping>;
|
||||
async fn save_mapping(&mut self, data: &IDMapping);
|
||||
async fn load_day(&mut self, day: Day) -> Result<AttendanceDay, Self::Error>;
|
||||
async fn save_day(&mut self, day: Day, data: &AttendanceDay) -> Result<(), Self::Error>;
|
||||
async fn list_days(&mut self) -> Result<Vec<Day>, Self::Error>;
|
||||
|
||||
async fn load_mapping_for_id(&mut self, id: TallyID) -> Result<Name, Self::Error>;
|
||||
async fn save_mapping_for_id(&mut self, id: TallyID, name: Name) -> Result<(), Self::Error>;
|
||||
async fn list_mappings(&mut self) -> Result<Vec<TallyID>, Self::Error>;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use alloc::string::String;
|
||||
use core::{fmt::Display, str::FromStr};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
|
||||
|
||||
@@ -102,7 +103,7 @@ impl<'de> Deserialize<'de> for TallyID {
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = <&str>::deserialize(deserializer)?;
|
||||
TallyID::from_str(s).map_err(|_| de::Error::custom("Failed to parse Tally ID"))
|
||||
let s = <String>::deserialize(deserializer)?;
|
||||
TallyID::from_str(&s).map_err(|_| de::Error::custom("Failed to parse Tally ID"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use picoserve::{
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
store::{Name, day::Day, tally_id::TallyID},
|
||||
store::{day::Day, mapping_loader::Name, tally_id::TallyID},
|
||||
webserver::{app::AppState, sse::IDEvents},
|
||||
};
|
||||
|
||||
@@ -28,10 +28,40 @@ pub struct QueryDay {
|
||||
day: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct QueryMapping {
|
||||
id: TallyID,
|
||||
}
|
||||
|
||||
// GET /api/mappings
|
||||
pub async fn get_mappings(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<impl IntoResponse, impl IntoResponse> {
|
||||
let loader = state.mapping_loader.lock().await;
|
||||
|
||||
match loader.list_mappings().await {
|
||||
Ok(ids) => Ok(response::Json(ids)),
|
||||
Err(_) => Err((
|
||||
response::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"INTERNAL_SERVER_ERROR",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/mapping
|
||||
pub async fn get_mapping(State(state): State<AppState>) -> impl IntoResponse {
|
||||
let store = state.store.lock().await;
|
||||
response::Json(store.mapping.clone())
|
||||
pub async fn get_mapping(
|
||||
State(state): State<AppState>,
|
||||
Query(QueryMapping { id }): Query<QueryMapping>,
|
||||
) -> Result<impl IntoResponse, impl IntoResponse> {
|
||||
let loader = state.mapping_loader.lock().await;
|
||||
|
||||
match loader.get_mapping(id).await {
|
||||
Ok(name) => Ok(response::Json(name)),
|
||||
Err(_) => Err((
|
||||
response::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"INTERNAL_SERVER_ERROR",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/mapping
|
||||
@@ -39,9 +69,14 @@ pub async fn add_mapping(
|
||||
State(state): State<AppState>,
|
||||
Json(data): Json<NewMapping>,
|
||||
) -> impl IntoResponse {
|
||||
let mut store = state.store.lock().await;
|
||||
store.mapping.add_mapping(data.id, data.name);
|
||||
store.persist_mapping().await;
|
||||
let loader = state.mapping_loader.lock().await;
|
||||
match loader.set_mapping(data.id, data.name).await {
|
||||
Ok(_) => (response::StatusCode::CREATED, ""),
|
||||
Err(_) => (
|
||||
response::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"INTERNAL_SERVER_ERROR",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// SSE /api/idevent
|
||||
@@ -70,9 +105,13 @@ pub async fn get_days(
|
||||
|
||||
let mut store = state.store.lock().await;
|
||||
|
||||
let days = store.list_days_in_timespan(from_day, to_day).await;
|
||||
|
||||
response::Json(days)
|
||||
match store.list_days_in_timespan(from_day, to_day).await {
|
||||
Ok(days) => Ok(response::Json(days)),
|
||||
Err(_) => Err((
|
||||
response::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Internal server error",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/day
|
||||
@@ -88,7 +127,10 @@ pub async fn get_day(
|
||||
let mut store = state.store.lock().await;
|
||||
|
||||
match store.load_day(parsed_day).await {
|
||||
Some(att_day) => Ok(response::Json(att_day)),
|
||||
None => Err((response::StatusCode::NOT_FOUND, "Not found")),
|
||||
Ok(att_day) => Ok(response::Json(att_day)),
|
||||
Err(_) => Err((
|
||||
response::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Internal server error",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ use picoserve::{AppWithStateBuilder, routing::get};
|
||||
|
||||
use crate::{
|
||||
TallyChannel, UsedStore,
|
||||
init::sd_card::SDCardPersistence,
|
||||
store::mapping_loader::MappingLoader,
|
||||
webserver::{
|
||||
api::{add_mapping, get_day, get_days, get_idevent, get_mapping},
|
||||
api::{add_mapping, get_day, get_days, get_idevent, get_mapping, get_mappings},
|
||||
assets::Assets,
|
||||
},
|
||||
};
|
||||
@@ -14,6 +16,7 @@ use crate::{
|
||||
pub struct AppState {
|
||||
pub store: Rc<Mutex<CriticalSectionRawMutex, UsedStore>>,
|
||||
pub chan: &'static TallyChannel,
|
||||
pub mapping_loader: Rc<Mutex<CriticalSectionRawMutex, MappingLoader<SDCardPersistence>>>,
|
||||
}
|
||||
|
||||
pub struct AppProps;
|
||||
@@ -25,6 +28,7 @@ impl AppWithStateBuilder for AppProps {
|
||||
fn build_app(self) -> picoserve::Router<Self::PathRouter, AppState> {
|
||||
picoserve::Router::from_service(Assets)
|
||||
.route("/api/mapping", get(get_mapping).post(add_mapping))
|
||||
.route("/api/mappings", get(get_mappings))
|
||||
.route("/api/idevent", get(get_idevent))
|
||||
.route("/api/days", get(get_days))
|
||||
.route("/api/day", get(get_day))
|
||||
|
||||
@@ -8,6 +8,8 @@ use static_cell::make_static;
|
||||
|
||||
use crate::{
|
||||
TallyChannel, UsedStore,
|
||||
init::sd_card::SDCardPersistence,
|
||||
store::mapping_loader::{self, MappingLoader},
|
||||
webserver::app::{AppProps, AppState},
|
||||
};
|
||||
|
||||
@@ -23,10 +25,16 @@ pub fn start_webserver(
|
||||
stack: Stack<'static>,
|
||||
store: Rc<Mutex<CriticalSectionRawMutex, UsedStore>>,
|
||||
chan: &'static TallyChannel,
|
||||
mapping_loader: MappingLoader<SDCardPersistence>,
|
||||
) {
|
||||
let app = make_static!(AppProps.build_app());
|
||||
|
||||
let state = make_static!(AppState { store, chan });
|
||||
let shared_mapping_loader = Rc::new(Mutex::new(mapping_loader));
|
||||
let state = make_static!(AppState {
|
||||
store,
|
||||
chan,
|
||||
mapping_loader: shared_mapping_loader
|
||||
});
|
||||
|
||||
let config = make_static!(picoserve::Config::new(picoserve::Timeouts {
|
||||
start_read_request: Some(Duration::from_secs(5)),
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
import AddIDModal from "./lib/AddIDModal.svelte";
|
||||
import ExportModal from "./lib/ExportModal.svelte";
|
||||
import { generateCSVFile } from "./lib/exporting";
|
||||
import { fetchMapping, type IDMap } from "./lib/IDMapping";
|
||||
import {
|
||||
cacheMappingInLocalstore,
|
||||
fetchMapping,
|
||||
loadCachedMappingFromLocalstore,
|
||||
type IDMap,
|
||||
} from "./lib/IDMapping";
|
||||
import { downloadBlob } from "./lib/downloadBlob";
|
||||
|
||||
let lastID: string = $state("");
|
||||
@@ -15,7 +20,11 @@
|
||||
let exportModal: ExportModal;
|
||||
|
||||
onMount(async () => {
|
||||
mapping = await fetchMapping();
|
||||
mapping = loadCachedMappingFromLocalstore();
|
||||
|
||||
let fetchedMapping = await fetchMapping();
|
||||
mapping = fetchedMapping;
|
||||
cacheMappingInLocalstore(fetchedMapping);
|
||||
|
||||
let sse = new EventSource("/api/idevent");
|
||||
sse.addEventListener("msg", function (e) {
|
||||
@@ -56,16 +65,23 @@
|
||||
addModal.open(id, firstName, lastName);
|
||||
}}
|
||||
/>
|
||||
<span>Gesammmte einträge: { Object.keys(mapping).length}</span>
|
||||
<span>Gesammmte einträge: {Object.keys(mapping).length}</span>
|
||||
{:else}
|
||||
Lade ...
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<AddIDModal
|
||||
bind:this={addModal}
|
||||
onSubmitted={async () => {
|
||||
mapping = await fetchMapping();
|
||||
onSubmitted={async (id, firstName, lastname) => {
|
||||
if (mapping == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mapping[id] = {
|
||||
first: firstName,
|
||||
last: lastname,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<script lang="ts">
|
||||
import Modal from "./Modal.svelte";
|
||||
|
||||
let { onSubmitted }: { onSubmitted?: () => void } = $props();
|
||||
let {
|
||||
onSubmitted,
|
||||
}: {
|
||||
onSubmitted?: (id: string, firstName: string, lastName: string) => void;
|
||||
} = $props();
|
||||
|
||||
let displayID = $state("");
|
||||
let firstName = $state("");
|
||||
@@ -9,7 +13,11 @@
|
||||
|
||||
let modal: Modal;
|
||||
|
||||
export function open(presetID: string, presetFirstName?: string, presetLastName?: string) {
|
||||
export function open(
|
||||
presetID: string,
|
||||
presetFirstName?: string,
|
||||
presetLastName?: string,
|
||||
) {
|
||||
displayID = presetID;
|
||||
|
||||
firstName = presetFirstName ?? "";
|
||||
@@ -33,13 +41,14 @@
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
}).then(() => {
|
||||
onSubmitted?.();
|
||||
});
|
||||
|
||||
}).then((res) => {
|
||||
if (res.status == 201) {
|
||||
onSubmitted?.(displayID, firstName, lastName);
|
||||
}
|
||||
firstName = "";
|
||||
lastName = "";
|
||||
displayID = "";
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -7,21 +7,55 @@ export interface Name {
|
||||
last: string,
|
||||
}
|
||||
|
||||
function stupidSerdeFix(pairs: [string, Name][]): IDMap {
|
||||
const map: IDMap = {};
|
||||
for (const [key, value] of pairs) {
|
||||
map[key] = value;
|
||||
async function fetchIDList(): Promise<string[]> {
|
||||
let res = await fetch("/api/mappings");
|
||||
let data = await res.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
async function fetchID(id: string): Promise<Name> {
|
||||
let res = await fetch("/api/mapping?id=" + id);
|
||||
let data = await res.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchMapping(): Promise<IDMap> {
|
||||
let ids = await fetchIDList();
|
||||
|
||||
let map: IDMap = {};
|
||||
|
||||
for (const id of ids) {
|
||||
let id_name = await fetchID(id);
|
||||
map[id] = id_name;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
export async function fetchMapping(): Promise<IDMap> {
|
||||
let res = await fetch("/api/mapping");
|
||||
const CACHE_KEY = "idmap";
|
||||
|
||||
let data = await res.json();
|
||||
export function cacheMappingInLocalstore(mapping: IDMap) {
|
||||
if (!localStorage) {
|
||||
console.error("localStorage is not available");
|
||||
return;
|
||||
}
|
||||
|
||||
return stupidSerdeFix(data);
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(mapping));
|
||||
}
|
||||
|
||||
export function loadCachedMappingFromLocalstore(): IDMap | null {
|
||||
if (!localStorage) {
|
||||
console.error("localStorage is not available");
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = localStorage.getItem(CACHE_KEY);
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mapping = JSON.parse(data);
|
||||
return mapping;
|
||||
}
|
||||
|
||||
export async function addMapping(id: string, firstName: string, lastName: string) {
|
||||
|
||||
Reference in New Issue
Block a user