remove: eliminate file management and LittleFS-based filesystem, implement savegame management for JSON config slots with wear-leveling

This commit is contained in:
2026-04-08 22:12:55 +02:00
parent 1da6d54d7a
commit 301298522b
17 changed files with 318 additions and 622 deletions

View File

@@ -0,0 +1,150 @@
use alloc::vec::Vec;
use embedded_savegame::storage::{Flash, Storage};
use embedded_storage::nor_flash::{NorFlash, ReadNorFlash};
use esp_bootloader_esp_idf::partitions::{FlashRegion, Error as PartitionError};
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;
/// Number of slots in the 8 MB storage partition.
pub const SAVEGAME_SLOT_COUNT: usize = 8 * 1024 * 1024 / SAVEGAME_SLOT_SIZE; // 512
/// 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,
}
// ── 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> {
NorFlash::write(self.region, addr, data).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.
fn erase(&mut self, addr: u32) -> Result<(), Self::Error> {
let end = addr + SAVEGAME_SLOT_SIZE as u32;
NorFlash::erase(self.region, addr, end).map_err(SavegameFlashError)
}
}
impl From<SavegameFlashError> 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 {
region: &'static mut FlashRegion<'static, MutexFlashStorage>,
}
impl SavegameManager {
pub fn new(region: &'static mut FlashRegion<'static, MutexFlashStorage>) -> Self {
Self { region }
}
/// Build a short-lived [`Storage`] that borrows our flash region.
fn storage(
&mut self,
) -> Storage<SavegameFlashAdapter<'_>, SAVEGAME_SLOT_SIZE, SAVEGAME_SLOT_COUNT> {
Storage::new(SavegameFlashAdapter {
region: &mut *self.region,
})
}
/// Persist `data` (JSON bytes) to the next available slot.
///
/// `scan()` advances the internal wear-leveling pointer to the latest valid
/// slot before `append()` writes to the next free one.
pub fn save(&mut self, data: &mut [u8]) -> FatResult<()> {
let mut st = self.storage();
st.scan()?;
st.append(data)?;
Ok(())
}
/// Load the most recently saved data. Returns `None` if no valid save exists.
pub fn load_latest(&mut self) -> FatResult<Option<Vec<u8>>> {
let mut st = self.storage();
let slot = st.scan()?;
match slot {
None => Ok(None),
Some(slot) => {
let mut buf = alloc::vec![0u8; SAVEGAME_SLOT_SIZE];
match st.read(slot.idx, &mut buf)? {
None => Ok(None),
Some(data) => Ok(Some(data.to_vec())),
}
}
}
}
/// Load a specific save by slot index. Returns `None` if the slot is
/// empty or contains an invalid checksum.
pub fn load_slot(&mut self, idx: usize) -> FatResult<Option<Vec<u8>>> {
let mut st = self.storage();
let mut buf = alloc::vec![0u8; SAVEGAME_SLOT_SIZE];
match st.read(idx, &mut buf)? {
None => Ok(None),
Some(data) => Ok(Some(data.to_vec())),
}
}
/// Erase a specific slot by index, effectively deleting it.
pub fn delete_slot(&mut self, idx: usize) -> FatResult<()> {
let mut st = self.storage();
st.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.
pub fn list_saves(&mut self) -> FatResult<Vec<SaveInfo>> {
let mut saves = Vec::new();
let mut st = self.storage();
let mut buf = alloc::vec![0u8; SAVEGAME_SLOT_SIZE];
for idx in 0..SAVEGAME_SLOT_COUNT {
if let Some(data) = st.read(idx, &mut buf)? {
saves.push(SaveInfo {
idx,
len: data.len() as u32,
});
}
}
Ok(saves)
}
}