- Added detailed logging for flash write and erase operations. - Ensured serialized save data is aligned to 4-byte boundaries.
211 lines
8.2 KiB
Rust
211 lines
8.2 KiB
Rust
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<alloc::string::String>,
|
|
}
|
|
|
|
/// 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<u8>,
|
|
}
|
|
|
|
// ── 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<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 {
|
|
storage: Storage<SavegameFlashAdapter<'static>, 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<Option<Vec<u8>>> {
|
|
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<Option<Vec<u8>>> {
|
|
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::<SaveWrapper, _>(
|
|
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<Vec<SaveInfo>> {
|
|
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::<SaveWrapper, _>(
|
|
data,
|
|
bincode::config::standard(),
|
|
)?;
|
|
|
|
saves.push(SaveInfo {
|
|
idx,
|
|
len: wrapper.data.len() as u32,
|
|
created_at: Some(wrapper.created_at),
|
|
});
|
|
}
|
|
}
|
|
Ok(saves)
|
|
}
|
|
}
|