Add save timestamp support and log interceptor for enhanced debugging
- Introduced `created_at` metadata for saves, enabling timestamp tracking. - Added `InterceptorLogger` to capture logs, aiding in error diagnostics. - Updated web UI to display save creation timestamps. - Improved save/load functionality to maintain compatibility with older formats.
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
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 esp_bootloader_esp_idf::partitions::{Error as PartitionError, FlashRegion};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::fat_error::{FatError, FatResult};
|
||||
use crate::hal::shared_flash::MutexFlashStorage;
|
||||
@@ -12,13 +12,24 @@ 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
|
||||
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, Deserialize, Debug)]
|
||||
struct SaveWrapper {
|
||||
/// UTC timestamp in RFC3339 format
|
||||
created_at: alloc::string::String,
|
||||
/// Raw config JSON data
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
// ── Flash adapter ──────────────────────────────────────────────────────────────
|
||||
@@ -94,19 +105,25 @@ impl SavegameManager {
|
||||
})
|
||||
}
|
||||
|
||||
/// Persist `data` (JSON bytes) to the next available slot.
|
||||
/// 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: &mut [u8]) -> FatResult<()> {
|
||||
pub fn save(&mut self, data: &mut [u8], timestamp: &str) -> FatResult<()> {
|
||||
let wrapper = SaveWrapper {
|
||||
created_at: alloc::string::String::from(timestamp),
|
||||
data: data.to_vec(),
|
||||
};
|
||||
let mut serialized = serde_json::to_vec(&wrapper)?;
|
||||
let mut st = self.storage();
|
||||
let _slot = st.scan()?;
|
||||
st.append(data)?;
|
||||
st.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 mut st = self.storage();
|
||||
let slot = st.scan()?;
|
||||
@@ -116,7 +133,14 @@ impl SavegameManager {
|
||||
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())),
|
||||
Some(data) => {
|
||||
// Try to deserialize as SaveWrapper (new format)
|
||||
match serde_json::from_slice::<SaveWrapper>(data) {
|
||||
Ok(wrapper) => Ok(Some(wrapper.data)),
|
||||
// Fallback to raw data for backwards compatibility
|
||||
Err(_) => Ok(Some(data.to_vec())),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,12 +148,20 @@ impl SavegameManager {
|
||||
|
||||
/// 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 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())),
|
||||
Some(data) => {
|
||||
// Try to deserialize as SaveWrapper (new format)
|
||||
match serde_json::from_slice::<SaveWrapper>(data) {
|
||||
Ok(wrapper) => Ok(Some(wrapper.data)),
|
||||
// Fallback to raw data for backwards compatibility
|
||||
Err(_) => Ok(Some(data.to_vec())),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,15 +173,24 @@ impl SavegameManager {
|
||||
|
||||
/// 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 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)? {
|
||||
// Try to deserialize as SaveWrapper to extract timestamp
|
||||
let (len, created_at) = match serde_json::from_slice::<SaveWrapper>(data) {
|
||||
Ok(wrapper) => (wrapper.data.len() as u32, Some(wrapper.created_at)),
|
||||
// Old format without timestamp
|
||||
Err(_) => (data.len() as u32, None),
|
||||
};
|
||||
|
||||
saves.push(SaveInfo {
|
||||
idx,
|
||||
len: data.len() as u32,
|
||||
len,
|
||||
created_at,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user