remove: eliminate file management and LittleFS-based filesystem, implement savegame management for JSON config slots with wear-leveling
This commit is contained in:
@@ -1,18 +1,16 @@
|
||||
use crate::bail;
|
||||
use crate::config::{NetworkConfig, PlantControllerConfig};
|
||||
use crate::hal::savegame_manager::SavegameManager;
|
||||
use crate::hal::{PLANT_COUNT, TIME_ACCESS};
|
||||
use crate::log::{LogMessage, LOG_ACCESS};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::fat_error::{ContextExt, FatError, FatResult};
|
||||
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
|
||||
use crate::hal::shared_flash::MutexFlashStorage;
|
||||
use alloc::string::ToString;
|
||||
use alloc::sync::Arc;
|
||||
use alloc::{format, string::String, vec, vec::Vec};
|
||||
use core::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use core::str::FromStr;
|
||||
use core::sync::atomic::Ordering;
|
||||
use embassy_executor::Spawner;
|
||||
use embassy_net::udp::UdpSocket;
|
||||
@@ -39,8 +37,6 @@ use esp_radio::wifi::{
|
||||
AccessPointConfig, AccessPointInfo, AuthMethod, ClientConfig, ModeConfig, ScanConfig,
|
||||
ScanTypeConfig, WifiController, WifiDevice, WifiStaState,
|
||||
};
|
||||
use littlefs2::fs::Filesystem;
|
||||
use littlefs2_core::{FileType, PathBuf, SeekFrom};
|
||||
use log::{error, info, warn};
|
||||
use mcutie::{
|
||||
Error, McutieBuilder, McutieReceiver, McutieTask, MqttMessage, PublishDisplay, Publishable,
|
||||
@@ -60,7 +56,6 @@ static mut LOW_VOLTAGE_DETECTED: i8 = 0;
|
||||
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
|
||||
static mut RESTART_TO_CONF: i8 = 0;
|
||||
|
||||
const CONFIG_FILE: &str = "config.json";
|
||||
const NTP_SERVER: &str = "pool.ntp.org";
|
||||
|
||||
static MQTT_CONNECTED_EVENT_RECEIVED: AtomicBool = AtomicBool::new(false);
|
||||
@@ -68,19 +63,6 @@ static MQTT_ROUND_TRIP_RECEIVED: AtomicBool = AtomicBool::new(false);
|
||||
pub static MQTT_STAY_ALIVE: AtomicBool = AtomicBool::new(false);
|
||||
static MQTT_BASE_TOPIC: OnceLock<String> = OnceLock::new();
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct FileInfo {
|
||||
filename: String,
|
||||
size: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct FileList {
|
||||
total: usize,
|
||||
used: usize,
|
||||
files: Vec<FileInfo>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default)]
|
||||
struct Timestamp {
|
||||
stamp: DateTime<Utc>,
|
||||
@@ -117,7 +99,7 @@ impl NtpTimestampGenerator for Timestamp {
|
||||
}
|
||||
|
||||
pub struct Esp<'a> {
|
||||
pub fs: Arc<Mutex<CriticalSectionRawMutex, Filesystem<'static, LittleFs2Filesystem>>>,
|
||||
pub savegame: SavegameManager,
|
||||
pub rng: Rng,
|
||||
//first starter (ap or sta will take these)
|
||||
pub interface_sta: Option<WifiDevice<'static>>,
|
||||
@@ -185,69 +167,6 @@ impl Esp<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
pub(crate) async fn delete_file(&self, filename: String) -> FatResult<()> {
|
||||
let file = PathBuf::try_from(filename.as_str())?;
|
||||
let access = self.fs.lock().await;
|
||||
access.remove(&file)?;
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) async fn write_file(
|
||||
&mut self,
|
||||
filename: String,
|
||||
offset: u32,
|
||||
buf: &[u8],
|
||||
) -> Result<(), FatError> {
|
||||
let file = PathBuf::try_from(filename.as_str())?;
|
||||
let access = self.fs.lock().await;
|
||||
access.open_file_with_options_and_then(
|
||||
|options| options.read(true).write(true).create(true),
|
||||
&file,
|
||||
|file| {
|
||||
file.seek(SeekFrom::Start(offset))?;
|
||||
file.write(buf)?;
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_size(&mut self, filename: String) -> FatResult<usize> {
|
||||
let file = PathBuf::try_from(filename.as_str())?;
|
||||
let access = self.fs.lock().await;
|
||||
let data = access.metadata(&file)?;
|
||||
Ok(data.len())
|
||||
}
|
||||
pub(crate) async fn get_file(
|
||||
&mut self,
|
||||
filename: String,
|
||||
chunk: u32,
|
||||
) -> FatResult<([u8; 512], usize)> {
|
||||
use littlefs2::io::Error as lfs2Error;
|
||||
|
||||
let file = PathBuf::try_from(filename.as_str())?;
|
||||
let access = self.fs.lock().await;
|
||||
let mut buf = [0_u8; 512];
|
||||
let mut read = 0;
|
||||
let offset = chunk * buf.len() as u32;
|
||||
access.open_file_with_options_and_then(
|
||||
|options| options.read(true),
|
||||
&file,
|
||||
|file| {
|
||||
let length = file.len()? as u32;
|
||||
if length == 0 {
|
||||
Err(lfs2Error::IO)
|
||||
} else if length > offset {
|
||||
file.seek(SeekFrom::Start(offset))?;
|
||||
read = file.read(&mut buf)?;
|
||||
Ok(())
|
||||
} else {
|
||||
//exactly at end, do nothing
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
)?;
|
||||
Ok((buf, read))
|
||||
}
|
||||
|
||||
pub(crate) async fn write_ota(&mut self, offset: u32, buf: &[u8]) -> Result<(), FatError> {
|
||||
let _ = check_erase(self.ota_target, offset, offset + 4096);
|
||||
@@ -422,10 +341,7 @@ impl Esp<'_> {
|
||||
.interface_ap
|
||||
.take()
|
||||
.context("AP interface already taken")?;
|
||||
let gw_ip_addr_str = "192.168.71.1";
|
||||
let gw_ip_addr = Ipv4Addr::from_str(gw_ip_addr_str).map_err(|_| FatError::String {
|
||||
error: "failed to parse gateway ip".to_string(),
|
||||
})?;
|
||||
let gw_ip_addr = Ipv4Addr::new(192, 168, 71, 1);
|
||||
|
||||
let config = embassy_net::Config::ipv4_static(StaticConfigV4 {
|
||||
address: Ipv4Cidr::new(gw_ip_addr, 24),
|
||||
@@ -454,7 +370,7 @@ impl Esp<'_> {
|
||||
println!("start net task");
|
||||
spawner.spawn(net_task(runner)).ok();
|
||||
println!("run dhcp");
|
||||
spawner.spawn(run_dhcp(*stack, gw_ip_addr_str)).ok();
|
||||
spawner.spawn(run_dhcp(*stack, gw_ip_addr)).ok();
|
||||
|
||||
loop {
|
||||
if stack.is_link_up() {
|
||||
@@ -465,7 +381,7 @@ impl Esp<'_> {
|
||||
while !stack.is_config_up() {
|
||||
Timer::after(Duration::from_millis(100)).await
|
||||
}
|
||||
println!("Connect to the AP `${ssid}` and point your browser to http://{gw_ip_addr_str}/");
|
||||
println!("Connect to the AP `${ssid}` and point your browser to http://{gw_ip_addr}/");
|
||||
stack
|
||||
.config_v4()
|
||||
.inspect(|c| println!("ipv4 config: {c:?}"));
|
||||
@@ -624,48 +540,46 @@ impl Esp<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the most recently saved config from flash.
|
||||
pub(crate) async fn load_config(&mut self) -> FatResult<PlantControllerConfig> {
|
||||
let cfg = PathBuf::try_from(CONFIG_FILE)?;
|
||||
let config_exist = self.fs.lock().await.exists(&cfg);
|
||||
if !config_exist {
|
||||
bail!("No config file stored")
|
||||
match self.savegame.load_latest()? {
|
||||
None => bail!("No config stored"),
|
||||
Some(data) => {
|
||||
let config: PlantControllerConfig = serde_json::from_slice(&data)?;
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
let data = self.fs.lock().await.read::<4096>(&cfg)?;
|
||||
let config: PlantControllerConfig = serde_json::from_slice(&data)?;
|
||||
Ok(config)
|
||||
}
|
||||
pub(crate) async fn save_config(&mut self, config: Vec<u8>) -> FatResult<()> {
|
||||
let filesystem = self.fs.lock().await;
|
||||
let cfg = PathBuf::try_from(CONFIG_FILE)?;
|
||||
filesystem.write(&cfg, &config)?;
|
||||
|
||||
/// Load a config from a specific save slot.
|
||||
pub(crate) async fn load_config_slot(
|
||||
&mut self,
|
||||
idx: usize,
|
||||
) -> FatResult<String> {
|
||||
match self.savegame.load_slot(idx)? {
|
||||
None => bail!("Slot {idx} is empty or invalid"),
|
||||
Some(data) => {
|
||||
Ok(String::from_utf8_lossy(&*data).to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Persist a JSON config blob to the next wear-leveling slot.
|
||||
pub(crate) async fn save_config(&mut self, mut config: Vec<u8>) -> FatResult<()> {
|
||||
self.savegame.save(config.as_mut_slice())?;
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) async fn list_files(&self) -> FatResult<FileList> {
|
||||
let path = PathBuf::new();
|
||||
|
||||
let fs = self.fs.lock().await;
|
||||
let free_size = fs.available_space()?;
|
||||
let total_size = fs.total_space();
|
||||
/// Delete a specific save slot by erasing it on flash.
|
||||
pub(crate) async fn delete_save_slot(&mut self, idx: usize) -> FatResult<()> {
|
||||
self.savegame.delete_slot(idx)
|
||||
}
|
||||
|
||||
let mut result = FileList {
|
||||
total: total_size,
|
||||
used: total_size - free_size,
|
||||
files: Vec::new(),
|
||||
};
|
||||
|
||||
fs.read_dir_and_then(&path, |dir| {
|
||||
for entry in dir {
|
||||
let e = entry?;
|
||||
if e.file_type() == FileType::File {
|
||||
result.files.push(FileInfo {
|
||||
filename: e.path().to_string(),
|
||||
size: e.metadata().len(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(result)
|
||||
/// Return metadata about all valid save slots.
|
||||
pub(crate) async fn list_saves(
|
||||
&mut self,
|
||||
) -> FatResult<alloc::vec::Vec<crate::hal::savegame_manager::SaveInfo>> {
|
||||
self.savegame.list_saves()
|
||||
}
|
||||
|
||||
pub(crate) async fn init_rtc_deepsleep_memory(
|
||||
@@ -969,8 +883,8 @@ async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) {
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn run_dhcp(stack: Stack<'static>, gw_ip_addr: &'static str) {
|
||||
use core::net::{Ipv4Addr, SocketAddrV4};
|
||||
async fn run_dhcp(stack: Stack<'static>, ip: Ipv4Addr) {
|
||||
use core::net::SocketAddrV4;
|
||||
|
||||
use edge_dhcp::{
|
||||
io::{self, DEFAULT_SERVER_PORT},
|
||||
@@ -979,14 +893,6 @@ async fn run_dhcp(stack: Stack<'static>, gw_ip_addr: &'static str) {
|
||||
use edge_nal::UdpBind;
|
||||
use edge_nal_embassy::{Udp, UdpBuffers};
|
||||
|
||||
let ip = match Ipv4Addr::from_str(gw_ip_addr) {
|
||||
Ok(ip) => ip,
|
||||
Err(_) => {
|
||||
error!("dhcp task failed to parse gw ip");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut buf = [0u8; 1500];
|
||||
|
||||
let mut gw_buf = [Ipv4Addr::UNSPECIFIED];
|
||||
|
||||
Reference in New Issue
Block a user