changed storage method of ID mappings

This commit is contained in:
Djeeberjr 2025-11-03 16:13:41 +01:00
parent 6cc6df6298
commit 07d51264f9
11 changed files with 241 additions and 123 deletions

View File

@ -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<TallyID> {
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<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 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();
@ -148,4 +143,92 @@ impl Persistence for SDCardPersistence {
days
}
async fn load_mapping_for_id(
&mut self,
id: crate::store::tally_id::TallyID,
) -> Option<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 = 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<TallyID> {
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<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());
}
})
.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
}
}

View File

@ -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<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 +73,7 @@ async fn main(spawner: Spawner) -> ! {
app_hardware.network_stack,
shared_store.clone(),
chan,
mapping_loader,
);
/****************************** Spawning tasks ***********************************/

View File

@ -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);
}
}

View File

@ -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) {
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<T: Persistence> IDStore<T> {
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<Day> {
let all_days = self.persistence_layer.list_days().await;
let all_days = self.persistence_layer.lock().await.list_days().await;
all_days
.into_iter()

View 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) -> Option<Name> {
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<TallyID> {
self.0.lock().await.list_mappings().await
}
}

View File

@ -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;

View File

@ -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<AttendanceDay>;
async fn save_day(&mut self, day: Day, data: &AttendanceDay);
async fn list_days(&mut self) -> Vec<Day>;
async fn load_mapping(&mut self) -> Option<IDMapping>;
async fn save_mapping(&mut self, data: &IDMapping);
async fn load_mapping_for_id(&mut self, id:TallyID ) -> Option<Name>;
async fn save_mapping_for_id(&mut self, id:TallyID, name: Name);
async fn list_mappings(&mut self) -> Vec<TallyID>;
}

View File

@ -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<u32>,
}
#[derive(Deserialize)]
pub struct QueryMapping {
id: TallyID,
}
// GET /api/mappings
pub async fn get_mappings(State(state): State<AppState>) -> 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<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>,
) -> 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<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;
loader.set_mapping(data.id, data.name).await;
}
// SSE /api/idevent

View File

@ -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))

View File

@ -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)),

View File

@ -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<string[]> {
let res = await fetch("/api/mappings");
let data = await res.json();
return data;
}
return map;
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 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) {