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
Software/MainBoard/rust/.idea/vcs.xml
generated
1
Software/MainBoard/rust/.idea/vcs.xml
generated
@@ -2,5 +2,6 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$/../../.." vcs="Git" />
|
<mapping directory="$PROJECT_DIR$/../../.." vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$/../../../website/themes/blowfish" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -140,7 +140,8 @@ macro_rules! mk_static {
|
|||||||
|
|
||||||
impl Esp<'_> {
|
impl Esp<'_> {
|
||||||
pub fn get_time(&self) -> DateTime<Utc> {
|
pub fn get_time(&self) -> DateTime<Utc> {
|
||||||
DateTime::from_timestamp_micros(self.rtc.current_time_us() as i64).unwrap_or(DateTime::UNIX_EPOCH)
|
DateTime::from_timestamp_micros(self.rtc.current_time_us() as i64)
|
||||||
|
.unwrap_or(DateTime::UNIX_EPOCH)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_time(&mut self, time: DateTime<Utc>) {
|
pub fn set_time(&mut self, time: DateTime<Utc>) {
|
||||||
@@ -517,10 +518,7 @@ impl Esp<'_> {
|
|||||||
Ok(*stack)
|
Ok(*stack)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deep_sleep(
|
pub fn deep_sleep(&mut self, duration_in_ms: u64) -> ! {
|
||||||
&mut self,
|
|
||||||
duration_in_ms: u64,
|
|
||||||
) -> ! {
|
|
||||||
// Mark the current OTA image as valid if we reached here while in pending verify.
|
// Mark the current OTA image as valid if we reached here while in pending verify.
|
||||||
if let Ok(cur) = self.ota.current_ota_state() {
|
if let Ok(cur) = self.ota.current_ota_state() {
|
||||||
if cur == OtaImageState::PendingVerify {
|
if cur == OtaImageState::PendingVerify {
|
||||||
@@ -556,23 +554,24 @@ impl Esp<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Load a config from a specific save slot.
|
/// Load a config from a specific save slot.
|
||||||
pub(crate) async fn load_config_slot(
|
pub(crate) async fn load_config_slot(&mut self, idx: usize) -> FatResult<String> {
|
||||||
&mut self,
|
|
||||||
idx: usize,
|
|
||||||
) -> FatResult<String> {
|
|
||||||
match self.savegame.load_slot(idx)? {
|
match self.savegame.load_slot(idx)? {
|
||||||
None => bail!("Slot {idx} is empty or invalid"),
|
None => bail!("Slot {idx} is empty or invalid"),
|
||||||
Some(data) => {
|
Some(data) => Ok(String::from_utf8_lossy(&data).to_string()),
|
||||||
Ok(String::from_utf8_lossy(&data).to_string())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Persist a JSON config blob to the next wear-leveling slot.
|
/// Persist a JSON config blob to the next wear-leveling slot.
|
||||||
pub(crate) async fn save_config(&mut self, mut config: Vec<u8>) -> FatResult<()> {
|
/// Retries once on flash error.
|
||||||
|
pub(crate) async fn save_config(&mut self, config: Vec<u8>) -> FatResult<()> {
|
||||||
let timestamp = self.get_time().to_rfc3339();
|
let timestamp = self.get_time().to_rfc3339();
|
||||||
self.savegame.save(config.as_mut_slice(), ×tamp)?;
|
match self.savegame.save(config.as_slice(), ×tamp) {
|
||||||
Ok(())
|
Ok(()) => Ok(()),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("First save attempt failed: {e:?}. Retrying...");
|
||||||
|
self.savegame.save(config.as_slice(), ×tamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete a specific save slot by erasing it on flash.
|
/// Delete a specific save slot by erasing it on flash.
|
||||||
@@ -608,7 +607,14 @@ impl Esp<'_> {
|
|||||||
if to_config_mode {
|
if to_config_mode {
|
||||||
RESTART_TO_CONF = 1;
|
RESTART_TO_CONF = 1;
|
||||||
}
|
}
|
||||||
log(LogMessage::RestartToConfig, RESTART_TO_CONF as u32, 0, "", "").await;
|
log(
|
||||||
|
LogMessage::RestartToConfig,
|
||||||
|
RESTART_TO_CONF as u32,
|
||||||
|
0,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
log(
|
log(
|
||||||
LogMessage::LowVoltage,
|
LogMessage::LowVoltage,
|
||||||
LOW_VOLTAGE_DETECTED as u32,
|
LOW_VOLTAGE_DETECTED as u32,
|
||||||
@@ -702,6 +708,7 @@ impl Esp<'_> {
|
|||||||
let mqtt_timeout = 15000;
|
let mqtt_timeout = 15000;
|
||||||
let res = async {
|
let res = async {
|
||||||
while !MQTT_CONNECTED_EVENT_RECEIVED.load(Ordering::Relaxed) {
|
while !MQTT_CONNECTED_EVENT_RECEIVED.load(Ordering::Relaxed) {
|
||||||
|
crate::hal::PlantHal::feed_watchdog();
|
||||||
Timer::after(Duration::from_millis(100)).await;
|
Timer::after(Duration::from_millis(100)).await;
|
||||||
}
|
}
|
||||||
Ok::<(), FatError>(())
|
Ok::<(), FatError>(())
|
||||||
@@ -720,6 +727,7 @@ impl Esp<'_> {
|
|||||||
|
|
||||||
let res = async {
|
let res = async {
|
||||||
while !MQTT_ROUND_TRIP_RECEIVED.load(Ordering::Relaxed) {
|
while !MQTT_ROUND_TRIP_RECEIVED.load(Ordering::Relaxed) {
|
||||||
|
crate::hal::PlantHal::feed_watchdog();
|
||||||
Timer::after(Duration::from_millis(100)).await;
|
Timer::after(Duration::from_millis(100)).await;
|
||||||
}
|
}
|
||||||
Ok::<(), FatError>(())
|
Ok::<(), FatError>(())
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ use lib_bms_protocol::BmsReadable;
|
|||||||
pub(crate) mod battery;
|
pub(crate) mod battery;
|
||||||
// mod can_api; // replaced by external canapi crate
|
// mod can_api; // replaced by external canapi crate
|
||||||
pub mod esp;
|
pub mod esp;
|
||||||
pub(crate) mod savegame_manager;
|
|
||||||
pub(crate) mod rtc;
|
pub(crate) mod rtc;
|
||||||
|
pub(crate) mod savegame_manager;
|
||||||
mod shared_flash;
|
mod shared_flash;
|
||||||
mod v4_hal;
|
mod v4_hal;
|
||||||
mod water;
|
mod water;
|
||||||
@@ -85,7 +85,7 @@ use esp_alloc as _;
|
|||||||
use esp_backtrace as _;
|
use esp_backtrace as _;
|
||||||
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState};
|
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState};
|
||||||
use esp_hal::delay::Delay;
|
use esp_hal::delay::Delay;
|
||||||
use esp_hal::i2c::master::{BusTimeout, Config, I2c};
|
use esp_hal::i2c::master::{BusTimeout, Config, FsmTimeout, I2c, SoftwareTimeout};
|
||||||
use esp_hal::interrupt::software::SoftwareInterruptControl;
|
use esp_hal::interrupt::software::SoftwareInterruptControl;
|
||||||
use esp_hal::pcnt::unit::Unit;
|
use esp_hal::pcnt::unit::Unit;
|
||||||
use esp_hal::pcnt::Pcnt;
|
use esp_hal::pcnt::Pcnt;
|
||||||
@@ -93,7 +93,7 @@ use esp_hal::rng::Rng;
|
|||||||
use esp_hal::rtc_cntl::{Rtc, SocResetReason};
|
use esp_hal::rtc_cntl::{Rtc, SocResetReason};
|
||||||
use esp_hal::system::reset_reason;
|
use esp_hal::system::reset_reason;
|
||||||
use esp_hal::time::Rate;
|
use esp_hal::time::Rate;
|
||||||
use esp_hal::timer::timg::TimerGroup;
|
use esp_hal::timer::timg::{MwdtStage, TimerGroup, Wdt};
|
||||||
use esp_hal::uart::Uart;
|
use esp_hal::uart::Uart;
|
||||||
use esp_hal::Blocking;
|
use esp_hal::Blocking;
|
||||||
use esp_radio::{init, Controller};
|
use esp_radio::{init, Controller};
|
||||||
@@ -108,6 +108,13 @@ pub const PLANT_COUNT: usize = 8;
|
|||||||
|
|
||||||
pub static PROGRESS_ACTIVE: AtomicBool = AtomicBool::new(false);
|
pub static PROGRESS_ACTIVE: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
pub static WATCHDOG: OnceLock<
|
||||||
|
embassy_sync::blocking_mutex::Mutex<
|
||||||
|
CriticalSectionRawMutex,
|
||||||
|
RefCell<Wdt<esp_hal::peripherals::TIMG0>>,
|
||||||
|
>,
|
||||||
|
> = OnceLock::new();
|
||||||
|
|
||||||
const TANK_MULTI_SAMPLE: usize = 11;
|
const TANK_MULTI_SAMPLE: usize = 11;
|
||||||
pub static I2C_DRIVER: OnceLock<
|
pub static I2C_DRIVER: OnceLock<
|
||||||
embassy_sync::blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<I2c<Blocking>>>,
|
embassy_sync::blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<I2c<Blocking>>>,
|
||||||
@@ -174,6 +181,9 @@ pub trait BoardInteraction<'a> {
|
|||||||
// Indicate progress is active to suppress default wait_infinity blinking
|
// Indicate progress is active to suppress default wait_infinity blinking
|
||||||
PROGRESS_ACTIVE.store(true, core::sync::atomic::Ordering::Relaxed);
|
PROGRESS_ACTIVE.store(true, core::sync::atomic::Ordering::Relaxed);
|
||||||
|
|
||||||
|
// Feed watchdog during long-running webserver operations
|
||||||
|
PlantHal::feed_watchdog();
|
||||||
|
|
||||||
let current = counter % PLANT_COUNT as u32;
|
let current = counter % PLANT_COUNT as u32;
|
||||||
for led in 0..PLANT_COUNT {
|
for led in 0..PLANT_COUNT {
|
||||||
if let Err(err) = self.fault(led, current == led as u32).await {
|
if let Err(err) = self.fault(led, current == led as u32).await {
|
||||||
@@ -251,6 +261,16 @@ impl PlantHal {
|
|||||||
let sw_int = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
|
let sw_int = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
|
||||||
esp_rtos::start(timg0.timer0, sw_int.software_interrupt0);
|
esp_rtos::start(timg0.timer0, sw_int.software_interrupt0);
|
||||||
|
|
||||||
|
// Initialize and enable the watchdog with 30 second timeout
|
||||||
|
let mut wdt = timg0.wdt;
|
||||||
|
wdt.set_timeout(MwdtStage::Stage0, esp_hal::time::Duration::from_secs(30));
|
||||||
|
wdt.enable();
|
||||||
|
WATCHDOG
|
||||||
|
.init(embassy_sync::blocking_mutex::Mutex::new(RefCell::new(wdt)))
|
||||||
|
.map_err(|_| FatError::String {
|
||||||
|
error: "Watchdog already initialized".to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
let boot_button = Input::new(
|
let boot_button = Input::new(
|
||||||
peripherals.GPIO9,
|
peripherals.GPIO9,
|
||||||
InputConfig::default().with_pull(Pull::None),
|
InputConfig::default().with_pull(Pull::None),
|
||||||
@@ -380,7 +400,11 @@ impl PlantHal {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let savegame = SavegameManager::new(data);
|
let savegame = SavegameManager::new(data);
|
||||||
info!("Savegame storage initialized ({} slots × {} KB)", savegame_manager::SAVEGAME_SLOT_COUNT, savegame_manager::SAVEGAME_SLOT_SIZE / 1024);
|
info!(
|
||||||
|
"Savegame storage initialized ({} slots × {} KB)",
|
||||||
|
savegame_manager::SAVEGAME_SLOT_COUNT,
|
||||||
|
savegame_manager::SAVEGAME_SLOT_SIZE / 1024
|
||||||
|
);
|
||||||
|
|
||||||
let uart0 =
|
let uart0 =
|
||||||
Uart::new(peripherals.UART0, UartConfig::default()).map_err(|_| FatError::String {
|
Uart::new(peripherals.UART0, UartConfig::default()).map_err(|_| FatError::String {
|
||||||
@@ -458,11 +482,16 @@ impl PlantHal {
|
|||||||
let sda = peripherals.GPIO20;
|
let sda = peripherals.GPIO20;
|
||||||
let scl = peripherals.GPIO19;
|
let scl = peripherals.GPIO19;
|
||||||
|
|
||||||
|
// Configure I2C with 1-second timeout
|
||||||
|
// At 100 Hz I2C clock, one bus cycle = 10ms
|
||||||
|
// For 1 second timeout: 100 bus cycles
|
||||||
let i2c = I2c::new(
|
let i2c = I2c::new(
|
||||||
peripherals.I2C0,
|
peripherals.I2C0,
|
||||||
Config::default()
|
Config::default()
|
||||||
.with_frequency(Rate::from_hz(100))
|
//.with_frequency(Rate::from_hz(100))
|
||||||
.with_timeout(BusTimeout::Maximum),
|
//1s at 100khz
|
||||||
|
.with_timeout(BusTimeout::BusCycles(100_000))
|
||||||
|
.with_scl_main_st_timeout(FsmTimeout::new(21)?),
|
||||||
)?
|
)?
|
||||||
.with_scl(scl)
|
.with_scl(scl)
|
||||||
.with_sda(sda);
|
.with_sda(sda);
|
||||||
@@ -563,6 +592,15 @@ impl PlantHal {
|
|||||||
|
|
||||||
Ok(Mutex::new(hal))
|
Ok(Mutex::new(hal))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Feed the watchdog timer to prevent system reset
|
||||||
|
pub fn feed_watchdog() {
|
||||||
|
if let Some(wdt_mutex) = WATCHDOG.try_get() {
|
||||||
|
wdt_mutex.lock(|cell| {
|
||||||
|
cell.borrow_mut().feed();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ota_state(
|
fn ota_state(
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
|
use bincode::{Decode, Encode};
|
||||||
use embedded_savegame::storage::{Flash, Storage};
|
use embedded_savegame::storage::{Flash, Storage};
|
||||||
use embedded_storage::nor_flash::{NorFlash, ReadNorFlash};
|
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 serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::fat_error::{FatError, FatResult};
|
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.
|
/// Wrapper that includes both the config data and metadata like creation timestamp.
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug, Encode, Decode)]
|
||||||
struct SaveWrapper {
|
struct SaveWrapper {
|
||||||
/// UTC timestamp in RFC3339 format
|
/// UTC timestamp in RFC3339 format
|
||||||
created_at: alloc::string::String,
|
created_at: alloc::string::String,
|
||||||
@@ -71,7 +73,21 @@ impl Flash for SavegameFlashAdapter<'_> {
|
|||||||
// Align end address up to erase boundary
|
// Align end address up to erase boundary
|
||||||
let end = addr + SAVEGAME_SLOT_SIZE as u32;
|
let end = addr + SAVEGAME_SLOT_SIZE as u32;
|
||||||
let aligned_end = ((end + ERASE_SIZE - 1) / ERASE_SIZE) * ERASE_SIZE;
|
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
|
/// High-level save-game manager that stores JSON config blobs on the storage
|
||||||
/// partition using [`embedded_savegame`] for wear leveling and power-fail safety.
|
/// partition using [`embedded_savegame`] for wear leveling and power-fail safety.
|
||||||
pub struct SavegameManager {
|
pub struct SavegameManager {
|
||||||
region: &'static mut FlashRegion<'static, MutexFlashStorage>,
|
storage: Storage<SavegameFlashAdapter<'static>, SAVEGAME_SLOT_SIZE, SAVEGAME_SLOT_COUNT>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SavegameManager {
|
impl SavegameManager {
|
||||||
pub fn new(region: &'static mut FlashRegion<'static, MutexFlashStorage>) -> Self {
|
pub fn new(region: &'static mut FlashRegion<'static, MutexFlashStorage>) -> Self {
|
||||||
Self { region }
|
Self {
|
||||||
|
storage: Storage::new(SavegameFlashAdapter { 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 with a UTC timestamp.
|
/// Persist `data` (JSON bytes) to the next available slot with a UTC timestamp.
|
||||||
@@ -110,31 +119,36 @@ impl SavegameManager {
|
|||||||
/// `scan()` advances the internal wear-leveling pointer to the latest valid
|
/// `scan()` advances the internal wear-leveling pointer to the latest valid
|
||||||
/// slot before `append()` writes to the next free one.
|
/// slot before `append()` writes to the next free one.
|
||||||
/// Both operations are performed atomically on the same Storage instance.
|
/// 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 {
|
let wrapper = SaveWrapper {
|
||||||
created_at: alloc::string::String::from(timestamp),
|
created_at: alloc::string::String::from(timestamp),
|
||||||
data: data.to_vec(),
|
data: data.to_vec(),
|
||||||
};
|
};
|
||||||
let mut serialized = serde_json::to_vec(&wrapper)?;
|
let mut serialized = bincode::encode_to_vec(&wrapper, bincode::config::standard())?;
|
||||||
let mut st = self.storage();
|
info!("Serialized config with size {}", serialized.len());
|
||||||
let _slot = st.scan()?;
|
(&mut self.storage).append(&mut serialized)?;
|
||||||
st.append(&mut serialized)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the most recently saved data. Returns `None` if no valid save exists.
|
/// Load the most recently saved data. Returns `None` if no valid save exists.
|
||||||
/// Unwraps the SaveWrapper and returns only the config data.
|
/// Unwraps the SaveWrapper and returns only the config data.
|
||||||
pub fn load_latest(&mut self) -> FatResult<Option<Vec<u8>>> {
|
pub fn load_latest(&mut self) -> FatResult<Option<Vec<u8>>> {
|
||||||
let mut st = self.storage();
|
let slot = (&mut self.storage).scan()?;
|
||||||
let slot = st.scan()?;
|
|
||||||
match slot {
|
match slot {
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
Some(slot) => {
|
Some(slot) => {
|
||||||
let mut buf = alloc::vec![0u8; SAVEGAME_SLOT_SIZE];
|
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),
|
None => Ok(None),
|
||||||
Some(data) => {
|
Some(data) => {
|
||||||
// Try to deserialize as SaveWrapper (new format)
|
// 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) {
|
match serde_json::from_slice::<SaveWrapper>(data) {
|
||||||
Ok(wrapper) => Ok(Some(wrapper.data)),
|
Ok(wrapper) => Ok(Some(wrapper.data)),
|
||||||
// Fallback to raw data for backwards compatibility
|
// Fallback to raw data for backwards compatibility
|
||||||
@@ -145,17 +159,25 @@ impl SavegameManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Load a specific save by slot index. Returns `None` if the slot is
|
/// Load a specific save by slot index. Returns `None` if the slot is
|
||||||
/// empty or contains an invalid checksum.
|
/// empty or contains an invalid checksum.
|
||||||
/// Unwraps the SaveWrapper and returns only the config data.
|
/// Unwraps the SaveWrapper and returns only the config data.
|
||||||
pub fn load_slot(&mut self, idx: usize) -> FatResult<Option<Vec<u8>>> {
|
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];
|
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),
|
None => Ok(None),
|
||||||
Some(data) => {
|
Some(data) => {
|
||||||
// Try to deserialize as SaveWrapper (new format)
|
// 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) {
|
match serde_json::from_slice::<SaveWrapper>(data) {
|
||||||
Ok(wrapper) => Ok(Some(wrapper.data)),
|
Ok(wrapper) => Ok(Some(wrapper.data)),
|
||||||
// Fallback to raw data for backwards compatibility
|
// Fallback to raw data for backwards compatibility
|
||||||
@@ -164,11 +186,12 @@ impl SavegameManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Erase a specific slot by index, effectively deleting it.
|
/// Erase a specific slot by index, effectively deleting it.
|
||||||
pub fn delete_slot(&mut self, idx: usize) -> FatResult<()> {
|
pub fn delete_slot(&mut self, idx: usize) -> FatResult<()> {
|
||||||
let mut st = self.storage();
|
(&mut self.storage).erase(idx).map_err(Into::into)
|
||||||
st.erase(idx).map_err(Into::into)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterate all slots and return metadata for every slot that contains a
|
/// Iterate all slots and return metadata for every slot that contains a
|
||||||
@@ -176,15 +199,23 @@ impl SavegameManager {
|
|||||||
/// Extracts timestamps from SaveWrapper if available.
|
/// Extracts timestamps from SaveWrapper if available.
|
||||||
pub fn list_saves(&mut self) -> FatResult<Vec<SaveInfo>> {
|
pub fn list_saves(&mut self) -> FatResult<Vec<SaveInfo>> {
|
||||||
let mut saves = Vec::new();
|
let mut saves = Vec::new();
|
||||||
let mut st = self.storage();
|
|
||||||
let mut buf = alloc::vec![0u8; SAVEGAME_SLOT_SIZE];
|
let mut buf = alloc::vec![0u8; SAVEGAME_SLOT_SIZE];
|
||||||
for idx in 0..SAVEGAME_SLOT_COUNT {
|
for idx in 0..SAVEGAME_SLOT_COUNT {
|
||||||
if let Some(data) = st.read(idx, &mut buf)? {
|
if let Some(data) = (&mut self.storage).read(idx, &mut buf)? {
|
||||||
// Try to deserialize as SaveWrapper to extract timestamp
|
// Try to deserialize as SaveWrapper (new Bincode format)
|
||||||
let (len, created_at) = match serde_json::from_slice::<SaveWrapper>(data) {
|
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)),
|
Ok(wrapper) => (wrapper.data.len() as u32, Some(wrapper.created_at)),
|
||||||
// Old format without timestamp
|
// Old format without timestamp
|
||||||
Err(_) => (data.len() as u32, None),
|
Err(_) => (data.len() as u32, None),
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
saves.push(SaveInfo {
|
saves.push(SaveInfo {
|
||||||
|
|||||||
@@ -749,6 +749,7 @@ pub async fn do_secure_pump(
|
|||||||
}
|
}
|
||||||
None => Duration::from_millis(1),
|
None => Duration::from_millis(1),
|
||||||
};
|
};
|
||||||
|
hal::PlantHal::feed_watchdog();
|
||||||
Timer::after(sleep_time).await;
|
Timer::after(sleep_time).await;
|
||||||
pump_time_ms += 50;
|
pump_time_ms += 50;
|
||||||
}
|
}
|
||||||
@@ -1119,6 +1120,8 @@ async fn wait_infinity(
|
|||||||
|
|
||||||
Timer::after_millis(delay).await;
|
Timer::after_millis(delay).await;
|
||||||
|
|
||||||
|
hal::PlantHal::feed_watchdog();
|
||||||
|
|
||||||
if wait_type == WaitType::MqttConfig && !MQTT_STAY_ALIVE.load(Ordering::Relaxed) {
|
if wait_type == WaitType::MqttConfig && !MQTT_STAY_ALIVE.load(Ordering::Relaxed) {
|
||||||
reboot_now.store(true, Ordering::Relaxed);
|
reboot_now.store(true, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -240,7 +240,10 @@ export class Controller {
|
|||||||
.then(_ => {
|
.then(_ => {
|
||||||
controller.progressview.removeProgress("set_config");
|
controller.progressview.removeProgress("set_config");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
controller.downloadConfig()
|
controller.downloadConfig().then(r => {
|
||||||
|
controller.updateSaveList().then(r => {
|
||||||
|
});
|
||||||
|
});
|
||||||
}, 250)
|
}, 250)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user