Introduce watchdog and serialization improvements
- Added watchdog timer for improved system stability and responsiveness. - Switched save data serialization to Bincode for better efficiency. - Enhanced compatibility by supporting fallback to older JSON format. - Improved logging during flash operations for easier debugging. - Simplified SavegameManager by managing storage directly.
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
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, FlashRegion};
|
||||
use esp_bootloader_esp_idf::partitions::{Error as PartitionError, Error, FlashRegion};
|
||||
use log::{error, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::fat_error::{FatError, FatResult};
|
||||
@@ -24,7 +26,7 @@ pub struct SaveInfo {
|
||||
}
|
||||
|
||||
/// Wrapper that includes both the config data and metadata like creation timestamp.
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug, Encode, Decode)]
|
||||
struct SaveWrapper {
|
||||
/// UTC timestamp in RFC3339 format
|
||||
created_at: alloc::string::String,
|
||||
@@ -71,7 +73,21 @@ impl Flash for SavegameFlashAdapter<'_> {
|
||||
// Align end address up to erase boundary
|
||||
let end = addr + SAVEGAME_SLOT_SIZE as u32;
|
||||
let aligned_end = ((end + ERASE_SIZE - 1) / ERASE_SIZE) * ERASE_SIZE;
|
||||
NorFlash::erase(self.region, aligned_start, aligned_end).map_err(SavegameFlashError)
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,21 +104,14 @@ impl From<SavegameFlashError> for FatError {
|
||||
/// 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>,
|
||||
storage: Storage<SavegameFlashAdapter<'static>, SAVEGAME_SLOT_SIZE, SAVEGAME_SLOT_COUNT>,
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
Self {
|
||||
storage: Storage::new(SavegameFlashAdapter { region }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Persist `data` (JSON bytes) to the next available slot with a UTC timestamp.
|
||||
@@ -110,35 +119,42 @@ impl SavegameManager {
|
||||
/// `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], timestamp: &str) -> FatResult<()> {
|
||||
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 = serde_json::to_vec(&wrapper)?;
|
||||
let mut st = self.storage();
|
||||
let _slot = st.scan()?;
|
||||
st.append(&mut serialized)?;
|
||||
let mut serialized = bincode::encode_to_vec(&wrapper, bincode::config::standard())?;
|
||||
info!("Serialized config with size {}", serialized.len());
|
||||
(&mut 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 mut st = self.storage();
|
||||
let slot = st.scan()?;
|
||||
let slot = (&mut self.storage).scan()?;
|
||||
match slot {
|
||||
None => Ok(None),
|
||||
Some(slot) => {
|
||||
let mut buf = alloc::vec![0u8; SAVEGAME_SLOT_SIZE];
|
||||
match st.read(slot.idx, &mut buf)? {
|
||||
match (&mut self.storage).read(slot.idx, &mut buf)? {
|
||||
None => Ok(None),
|
||||
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())),
|
||||
// Try to deserialize as SaveWrapper (new Bincode format)
|
||||
match bincode::decode_from_slice::<SaveWrapper, _>(
|
||||
data,
|
||||
bincode::config::standard(),
|
||||
) {
|
||||
Ok((wrapper, _)) => Ok(Some(wrapper.data)),
|
||||
Err(_) => {
|
||||
// Fallback to JSON SaveWrapper (intermediate 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())),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,16 +166,24 @@ impl SavegameManager {
|
||||
/// 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)? {
|
||||
match (&mut self.storage).read(idx, &mut buf)? {
|
||||
None => Ok(None),
|
||||
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())),
|
||||
// Try to deserialize as SaveWrapper (new Bincode format)
|
||||
match bincode::decode_from_slice::<SaveWrapper, _>(
|
||||
data,
|
||||
bincode::config::standard(),
|
||||
) {
|
||||
Ok((wrapper, _)) => Ok(Some(wrapper.data)),
|
||||
Err(_) => {
|
||||
// Fallback to JSON SaveWrapper (intermediate 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())),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,8 +191,7 @@ impl SavegameManager {
|
||||
|
||||
/// 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)
|
||||
(&mut self.storage).erase(idx).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Iterate all slots and return metadata for every slot that contains a
|
||||
@@ -176,15 +199,23 @@ impl SavegameManager {
|
||||
/// 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),
|
||||
if let Some(data) = (&mut self.storage).read(idx, &mut buf)? {
|
||||
// Try to deserialize as SaveWrapper (new Bincode format)
|
||||
let (len, created_at) = match bincode::decode_from_slice::<SaveWrapper, _>(
|
||||
data,
|
||||
bincode::config::standard(),
|
||||
) {
|
||||
Ok((wrapper, _)) => (wrapper.data.len() as u32, Some(wrapper.created_at)),
|
||||
Err(_) => {
|
||||
// Fallback to JSON SaveWrapper (intermediate format)
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user