use alloc::vec::Vec; use bincode::{Decode, Encode}; use embedded_savegame::storage::{Flash, Storage}; use embedded_storage::nor_flash::{NorFlash, ReadNorFlash}; use esp_bootloader_esp_idf::partitions::{Error as PartitionError, Error, FlashRegion}; use log::{error, info}; use serde::Serialize; use crate::fat_error::{FatError, FatResult}; use crate::hal::shared_flash::MutexFlashStorage; /// Size of each save slot in bytes (16 KB). pub const SAVEGAME_SLOT_SIZE: usize = 16384; //keep a little of space at the end due to partition table offsets const SAFETY: usize = 5; /// Number of slots in the 8 MB storage partition. pub const SAVEGAME_SLOT_COUNT: usize = (8 * 1024 * 1024) / SAVEGAME_SLOT_SIZE - SAFETY; // 507 /// Metadata about a single existing save slot, returned by [`SavegameManager::list_saves`]. #[derive(Serialize, Debug, Clone)] pub struct SaveInfo { pub idx: usize, pub len: u32, /// UTC timestamp in RFC3339 format when the save was created pub created_at: Option, } /// Wrapper that includes both the config data and metadata like creation timestamp. #[derive(Serialize, Debug, Encode, Decode)] struct SaveWrapper { /// UTC timestamp in RFC3339 format created_at: alloc::string::String, /// Raw config JSON data data: Vec, } // ── Flash adapter ────────────────────────────────────────────────────────────── /// Newtype wrapper around a [`PartitionError`] so we can implement the /// [`core::fmt::Debug`] bound required by [`embedded_savegame::storage::Flash`]. #[derive(Debug)] pub struct SavegameFlashError(#[allow(dead_code)] PartitionError); /// Adapts a `&mut FlashRegion<'static, MutexFlashStorage>` to the /// [`embedded_savegame::storage::Flash`] trait. /// /// `erase(addr)` erases exactly one slot (`SAVEGAME_SLOT_SIZE` bytes) starting /// at `addr`, which is what embedded-savegame expects for NOR flash. pub struct SavegameFlashAdapter<'a> { region: &'a mut FlashRegion<'static, MutexFlashStorage>, } impl Flash for SavegameFlashAdapter<'_> { type Error = SavegameFlashError; fn read(&mut self, addr: u32, buf: &mut [u8]) -> Result<(), Self::Error> { ReadNorFlash::read(self.region, addr, buf).map_err(SavegameFlashError) } fn write(&mut self, addr: u32, data: &mut [u8]) -> Result<(), Self::Error> { info!( "Relative writing to flash at 0x{:x} with length {}", addr, data.len() ); let error = NorFlash::write(self.region, addr, data); if error.is_err() { error!("error {:?}", error.unwrap_err()) } error.map_err(SavegameFlashError) } /// Erase one full slot at `addr`. /// embedded-savegame calls this before writing to a slot, so we erase /// the entire `SAVEGAME_SLOT_SIZE` bytes so subsequent writes land on /// pre-erased (0xFF) pages. /// Ensures addresses are aligned to ERASE_SIZE (4KB) boundaries. fn erase(&mut self, addr: u32) -> Result<(), Self::Error> { const ERASE_SIZE: u32 = 4096; // Align start address down to erase boundary let aligned_start = (addr / ERASE_SIZE) * ERASE_SIZE; // Align end address up to erase boundary let end = addr + SAVEGAME_SLOT_SIZE as u32; let aligned_end = end.div_ceil(ERASE_SIZE) * ERASE_SIZE; info!( "Relative erasing flash at 0x{:x} (aligned to 0x{:x}-0x{:x})", addr, aligned_start, aligned_end ); if aligned_start != addr || aligned_end != end { log::warn!("Flash erase address not aligned: addr=0x{:x}, slot_size=0x{:x}. Aligned to 0x{:x}-0x{:x}", addr, SAVEGAME_SLOT_SIZE, aligned_start, aligned_end); } match NorFlash::erase(self.region, aligned_start, aligned_end) { Ok(_) => Ok(()), Err(err) => { error!( "Flash erase failed: {:?}. 0x{:x}-0x{:x}", err, aligned_start, aligned_end ); Err(SavegameFlashError(err)) } } } } impl From for FatError { fn from(e: SavegameFlashError) -> Self { FatError::String { error: alloc::format!("Savegame flash error: {:?}", e), } } } // ── SavegameManager ──────────────────────────────────────────────────────────── /// High-level save-game manager that stores JSON config blobs on the storage /// partition using [`embedded_savegame`] for wear leveling and power-fail safety. pub struct SavegameManager { storage: Storage, SAVEGAME_SLOT_SIZE, SAVEGAME_SLOT_COUNT>, } impl SavegameManager { pub fn new(region: &'static mut FlashRegion<'static, MutexFlashStorage>) -> Self { Self { storage: Storage::new(SavegameFlashAdapter { region }), } } /// Persist `data` (JSON bytes) to the next available slot with a UTC timestamp. /// /// `scan()` advances the internal wear-leveling pointer to the latest valid /// slot before `append()` writes to the next free one. /// Both operations are performed atomically on the same Storage instance. pub fn save(&mut self, data: &[u8], timestamp: &str) -> FatResult<()> { let wrapper = SaveWrapper { created_at: alloc::string::String::from(timestamp), data: data.to_vec(), }; let mut serialized = bincode::encode_to_vec(&wrapper, bincode::config::standard())?; // Flash storage often requires length to be a multiple of 4. let padding = (4 - (serialized.len() % 4)) % 4; if padding > 0 { serialized.extend_from_slice(&[0u8; 4][..padding]); } info!("Serialized config with size {} (padded)", serialized.len()); self.storage.append(&mut serialized)?; Ok(()) } /// Load the most recently saved data. Returns `None` if no valid save exists. /// Unwraps the SaveWrapper and returns only the config data. pub fn load_latest(&mut self) -> FatResult>> { let slot = self.storage.scan()?; match slot { None => Ok(None), Some(slot) => self.load_slot(slot.idx), } } /// Load a specific save by slot index. Returns `None` if the slot is /// empty or contains an invalid checksum. /// Unwraps the SaveWrapper and returns only the config data. pub fn load_slot(&mut self, idx: usize) -> FatResult>> { let mut buf = alloc::vec![0u8; SAVEGAME_SLOT_SIZE]; match self.storage.read(idx, &mut buf)? { None => Ok(None), Some(data) => { // Try to deserialize as SaveWrapper (new Bincode format) let (wrapper, _) = bincode::decode_from_slice::( data, bincode::config::standard(), )?; Ok(Some(wrapper.data)) } } } /// Erase a specific slot by index, effectively deleting it. pub fn delete_slot(&mut self, idx: usize) -> FatResult<()> { self.storage.erase(idx).map_err(Into::into) } /// Iterate all slots and return metadata for every slot that contains a /// valid save, using the Storage read API to avoid assuming internal slot structure. /// Extracts timestamps from SaveWrapper if available. pub fn list_saves(&mut self) -> FatResult> { let mut saves = Vec::new(); let mut buf = alloc::vec![0u8; SAVEGAME_SLOT_SIZE]; for idx in 0..SAVEGAME_SLOT_COUNT { if let Some(data) = self.storage.read(idx, &mut buf)? { // Try to deserialize as SaveWrapper (new Bincode format) let (wrapper, _) = bincode::decode_from_slice::( data, bincode::config::standard(), )?; saves.push(SaveInfo { idx, len: wrapper.data.len() as u32, created_at: Some(wrapper.created_at), }); } } Ok(saves) } }