From 07d51264f938627a236c1a873650bb26ccf9b2e9 Mon Sep 17 00:00:00 2001 From: Djeeberjr Date: Mon, 3 Nov 2025 16:13:41 +0100 Subject: [PATCH] changed storage method of ID mappings --- src/init/sd_card.rs | 175 ++++++++++++++++++++++++++---------- src/main.rs | 12 ++- src/store/id_mapping.rs | 33 ------- src/store/id_store.rs | 32 ++++--- src/store/mapping_loader.rs | 31 +++++++ src/store/mod.rs | 4 +- src/store/persistence.rs | 7 +- src/webserver/api.rs | 28 ++++-- src/webserver/app.rs | 6 +- src/webserver/mod.rs | 10 ++- web/src/lib/IDMapping.ts | 26 ++++-- 11 files changed, 241 insertions(+), 123 deletions(-) delete mode 100644 src/store/id_mapping.rs create mode 100644 src/store/mapping_loader.rs diff --git a/src/init/sd_card.rs b/src/init/sd_card.rs index dfb6714..e883360 100644 --- a/src/init/sd_card.rs +++ b/src/init/sd_card.rs @@ -1,10 +1,14 @@ -use alloc::vec::Vec; +use core::str::from_utf8; + +use alloc::{string::ToString, vec::Vec}; use embassy_time::Delay; use embedded_hal_bus::spi::ExclusiveDevice; use embedded_sdmmc::{SdCard, ShortFileName, TimeSource, Timestamp, VolumeIdx, VolumeManager}; use esp_hal::{Blocking, gpio::Output, spi::master::Spi}; -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,16 +43,46 @@ 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) -> ShortFileName { 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(".JS").unwrap(); ShortFileName::create_from_str(&filename).unwrap() } + + fn generate_path_for_id(id: TallyID) -> (ShortFileName, ShortFileName) { + 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).unwrap(); + filename.push_str(".JS").unwrap(); + + let mut dirname: heapless::String<11> = heapless::String::new(); + dirname.push_str(dir).unwrap(); + + ( + ShortFileName::create_from_str(&dirname).unwrap(), + ShortFileName::create_from_str(&filename).unwrap(), + ) + } + + fn get_tallyid_from_path(dirname: &ShortFileName, filename: &ShortFileName) -> Option { + let mut id_str: heapless::String<12> = heapless::String::new(); + + id_str.push_str(&dirname.to_string()).unwrap(); + id_str + .push_str(from_utf8(filename.base_name()).unwrap()) + .unwrap(); + + let id: TallyID = id_str.try_into().unwrap(); + + Some(id) + } } impl Persistence for SDCardPersistence { @@ -56,7 +90,7 @@ impl Persistence for SDCardPersistence { let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap(); let mut root_dir = vol_0.open_root_dir().unwrap(); - let filename = Self::generate_filename(day); + 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() { @@ -78,7 +112,7 @@ impl Persistence for SDCardPersistence { let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap(); let mut root_dir = vol_0.open_root_dir().unwrap(); - 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) @@ -90,45 +124,6 @@ impl Persistence for SDCardPersistence { file.close().unwrap(); } - async fn load_mapping(&mut self) -> Option { - let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap(); - let mut root_dir = vol_0.open_root_dir().unwrap(); - - 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 { let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap(); let mut root_dir = vol_0.open_root_dir().unwrap(); @@ -148,4 +143,92 @@ impl Persistence for SDCardPersistence { days } + + async fn load_mapping_for_id( + &mut self, + id: crate::store::tally_id::TallyID, + ) -> Option { + let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap(); + let mut root_dir = vol_0.open_root_dir().unwrap(); + let mut mapping_dir = root_dir.open_dir(Self::MAPPING_DIRNAME).unwrap(); + + let (dirname, filename) = Self::generate_path_for_id(id); + + let mut dir = mapping_dir.open_dir(dirname).unwrap(); + let mut file = dir + .open_file_in_dir(filename, embedded_sdmmc::Mode::ReadOnly) + .unwrap(); + + let mut read_buffer: [u8; 1024] = [0; 1024]; + let read_bytes = file.read(&mut read_buffer).unwrap(); + file.close().unwrap(); + + let mapping: Name = serde_json::from_slice(&read_buffer[..read_bytes]).unwrap(); + + Some(mapping) + } + + async fn save_mapping_for_id( + &mut self, + id: crate::store::tally_id::TallyID, + name: crate::store::mapping_loader::Name, + ) { + let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap(); + let mut root_dir = vol_0.open_root_dir().unwrap(); + let mut mapping_dir = root_dir.open_dir(Self::MAPPING_DIRNAME).unwrap(); + + 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).unwrap(); + mapping_dir.open_dir(&dirname).unwrap() + }; + + let mut file = dir + .open_file_in_dir(filename, embedded_sdmmc::Mode::ReadWriteCreateOrTruncate) + .unwrap(); + + file.write(&serde_json::to_vec(&name).unwrap()).unwrap(); + } + + async fn list_mappings(&mut self) -> Vec { + let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap(); + let mut root_dir = vol_0.open_root_dir().unwrap(); + let mut mapping_dir = root_dir.open_dir(Self::MAPPING_DIRNAME).unwrap(); + let mut ids: Vec = 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()); + } + }) + .unwrap(); + + 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()); + } + }) + .unwrap(); + + for filename in file_names { + let id = Self::get_tallyid_from_path(&dirname, &filename); + ids.push(id.unwrap()); + } + } + } + + ids + } } diff --git a/src/main.rs b/src/main.rs index d932310..5c7f638 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,6 @@ #![no_main] #![feature(type_alias_impl_trait)] #![feature(impl_trait_in_assoc_type)] -#![warn(clippy::unwrap_used)] use alloc::rc::Rc; use embassy_executor::Spawner; @@ -26,7 +25,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 +53,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> = + 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 +73,7 @@ async fn main(spawner: Spawner) -> ! { app_hardware.network_stack, shared_store.clone(), chan, + mapping_loader, ); /****************************** Spawning tasks ***********************************/ diff --git a/src/store/id_mapping.rs b/src/store/id_mapping.rs deleted file mode 100644 index 540a80f..0000000 --- a/src/store/id_mapping.rs +++ /dev/null @@ -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, -} - -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); - } -} diff --git a/src/store/id_store.rs b/src/store/id_store.rs index 119f6c6..ce4433d 100644 --- a/src/store/id_store.rs +++ b/src/store/id_store.rs @@ -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 { - pub current_day: AttendanceDay, - pub mapping: IDMapping, - persistence_layer: T, + current_day: AttendanceDay, + persistence_layer: Rc>, } impl IDStore { - 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>, + 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) { 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 { @@ -95,11 +93,11 @@ impl IDStore { return Some(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 { - let all_days = self.persistence_layer.list_days().await; + let all_days = self.persistence_layer.lock().await.list_days().await; all_days .into_iter() diff --git a/src/store/mapping_loader.rs b/src/store/mapping_loader.rs new file mode 100644 index 0000000..dc09441 --- /dev/null +++ b/src/store/mapping_loader.rs @@ -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(Rc>); + +impl MappingLoader { + pub fn new(persistence_layer: Rc>) -> Self { + Self(persistence_layer) + } + + pub async fn get_mapping(&self, id: TallyID) -> Option { + self.0.lock().await.load_mapping_for_id(id).await + } + + pub async fn set_mapping(&self, id: TallyID, name: Name) { + self.0.lock().await.save_mapping_for_id(id, name).await; + } + + pub async fn list_mappings(&self) -> Vec { + self.0.lock().await.list_mappings().await + } +} diff --git a/src/store/mod.rs b/src/store/mod.rs index 37d5d94..e7a37ce 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -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; diff --git a/src/store/persistence.rs b/src/store/persistence.rs index 63de781..3513eff 100644 --- a/src/store/persistence.rs +++ b/src/store/persistence.rs @@ -1,12 +1,13 @@ 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; async fn save_day(&mut self, day: Day, data: &AttendanceDay); async fn list_days(&mut self) -> Vec; - async fn load_mapping(&mut self) -> Option; - async fn save_mapping(&mut self, data: &IDMapping); + async fn load_mapping_for_id(&mut self, id:TallyID ) -> Option; + async fn save_mapping_for_id(&mut self, id:TallyID, name: Name); + async fn list_mappings(&mut self) -> Vec; } diff --git a/src/webserver/api.rs b/src/webserver/api.rs index c535b64..9ffcbae 100644 --- a/src/webserver/api.rs +++ b/src/webserver/api.rs @@ -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,25 @@ pub struct QueryDay { day: Option, } +#[derive(Deserialize)] +pub struct QueryMapping { + id: TallyID, +} + +// GET /api/mappings +pub async fn get_mappings(State(state): State) -> impl IntoResponse { + let loader = state.mapping_loader.lock().await; + response::Json(loader.list_mappings().await) +} + // GET /api/mapping -pub async fn get_mapping(State(state): State) -> impl IntoResponse { - let store = state.store.lock().await; - response::Json(store.mapping.clone()) +pub async fn get_mapping( + State(state): State, + Query(QueryMapping { id }): Query, +) -> impl IntoResponse { + let loader = state.mapping_loader.lock().await; + let mapping = loader.get_mapping(id).await; + response::Json(mapping) } // POST /api/mapping @@ -39,9 +54,8 @@ pub async fn add_mapping( State(state): State, Json(data): Json, ) -> 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; + loader.set_mapping(data.id, data.name).await; } // SSE /api/idevent diff --git a/src/webserver/app.rs b/src/webserver/app.rs index e4c24ea..06a7e3f 100644 --- a/src/webserver/app.rs +++ b/src/webserver/app.rs @@ -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>, pub chan: &'static TallyChannel, + pub mapping_loader: Rc>>, } pub struct AppProps; @@ -25,6 +28,7 @@ impl AppWithStateBuilder for AppProps { fn build_app(self) -> picoserve::Router { 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)) diff --git a/src/webserver/mod.rs b/src/webserver/mod.rs index 5a90988..816da1b 100644 --- a/src/webserver/mod.rs +++ b/src/webserver/mod.rs @@ -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>, chan: &'static TallyChannel, + mapping_loader: MappingLoader, ) { 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)), diff --git a/web/src/lib/IDMapping.ts b/web/src/lib/IDMapping.ts index a6761f2..83fc302 100644 --- a/web/src/lib/IDMapping.ts +++ b/web/src/lib/IDMapping.ts @@ -7,21 +7,29 @@ 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 { + let res = await fetch("/api/mappings"); + let data = await res.json(); + return data; +} - return map; +async function fetchID(id: string): Promise { + let res = await fetch("/api/mapping?id=" + id); + let data = await res.json(); + return data; } export async function fetchMapping(): Promise { - let res = await fetch("/api/mapping"); + let ids = await fetchIDList(); - let data = await res.json(); + let map: IDMap = {}; - return stupidSerdeFix(data); + for (const id of ids) { + let id_name = await fetchID(id); + map[id] = id_name; + } + + return map; } export async function addMapping(id: string, firstName: string, lastName: string) {