remove: eliminate file management and LittleFS-based filesystem, implement savegame management for JSON config slots with wear-leveling

This commit is contained in:
2026-04-08 22:12:55 +02:00
parent 1da6d54d7a
commit 301298522b
17 changed files with 318 additions and 622 deletions

View File

@@ -1,160 +0,0 @@
use crate::fat_error::{FatError, FatResult};
use crate::webserver::read_up_to_bytes_from_request;
use crate::BOARD_ACCESS;
use alloc::borrow::ToOwned;
use alloc::format;
use alloc::string::String;
use edge_http::io::server::Connection;
use edge_http::Method;
use edge_nal::io::{Read, Write};
use log::info;
pub(crate) async fn list_files<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await;
let result = board.board_hal.get_esp().list_files().await?;
let file_list_json = serde_json::to_string(&result)?;
Ok(Some(file_list_json))
}
pub(crate) async fn file_operations<T, const N: usize>(
conn: &mut Connection<'_, T, { N }>,
method: Method,
path: &&str,
prefix: &&str,
) -> Result<Option<u32>, FatError>
where
T: Read + Write,
{
let filename = &path[prefix.len()..];
info!("file request for {filename} with method {method}");
Ok(match method {
Method::Delete => {
let mut board = BOARD_ACCESS.get().await.lock().await;
board
.board_hal
.get_esp()
.delete_file(filename.to_owned())
.await?;
conn.initiate_response(
200,
Some("OK"),
&[
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
],
)
.await?;
Some(200)
}
Method::Get => {
let disposition = format!("attachment; filename=\"{filename}\"");
let size = {
let mut board = BOARD_ACCESS.get().await.lock().await;
board
.board_hal
.get_esp()
.get_size(filename.to_owned())
.await?
};
conn.initiate_response(
200,
Some("OK"),
&[
("Content-Type", "application/octet-stream"),
("Content-Disposition", disposition.as_str()),
("Content-Length", &format!("{size}")),
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
],
)
.await?;
let mut chunk = 0;
loop {
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.progress(chunk).await;
let read_chunk = board
.board_hal
.get_esp()
.get_file(filename.to_owned(), chunk)
.await?;
let length = read_chunk.1;
if length == 0 {
info!("file request for {filename} finished");
break;
}
let data = &read_chunk.0[0..length];
conn.write_all(data).await?;
if length < read_chunk.0.len() {
info!("file request for {filename} finished");
break;
}
chunk += 1;
}
BOARD_ACCESS
.get()
.await
.lock()
.await
.board_hal
.clear_progress()
.await;
Some(200)
}
Method::Post => {
{
let mut board = BOARD_ACCESS.get().await.lock().await;
//ensure the file is deleted first; otherwise we would need to truncate the file which will not work with streaming
let _ = board
.board_hal
.get_esp()
.delete_file(filename.to_owned())
.await;
}
let mut offset = 0_usize;
let mut chunk = 0;
loop {
let buf = read_up_to_bytes_from_request(conn, Some(4096)).await?;
if buf.is_empty() {
info!("file request for {filename} finished");
break;
} else {
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.progress(chunk as u32).await;
board
.board_hal
.get_esp()
.write_file(filename.to_owned(), offset as u32, &buf)
.await?;
}
offset += buf.len();
chunk += 1;
}
BOARD_ACCESS
.get()
.await
.lock()
.await
.board_hal
.clear_progress()
.await;
conn.initiate_response(
200,
Some("OK"),
&[
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
],
)
.await?;
Some(200)
}
_ => None,
})
}

View File

@@ -114,12 +114,39 @@ pub(crate) async fn get_version_web<T, const N: usize>(
Ok(Some(serde_json::to_string(&get_version(&mut board).await)?))
}
/// Return the current in-memory config, or — when `saveidx` is `Some(idx)` —
/// the JSON stored in save slot `idx`.
pub(crate) async fn get_config<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
saveidx: Option<usize>,
) -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await;
let json = match saveidx {
None => serde_json::to_string(board.board_hal.get_config())?,
Some(idx) => {
board.board_hal.get_esp().load_config_slot(idx).await?
}
};
Ok(Some(json))
}
/// Return a JSON array describing every valid save slot on flash.
pub(crate) async fn list_saves<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await;
let json = serde_json::to_string(&board.board_hal.get_config())?;
Ok(Some(json))
let saves = board.board_hal.get_esp().list_saves().await?;
Ok(Some(serde_json::to_string(&saves)?))
}
/// Erase (delete) a single save slot by index.
pub(crate) async fn delete_save<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
idx: usize,
) -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.get_esp().delete_save_slot(idx).await?;
Ok(None)
}
pub(crate) async fn get_battery_state<T, const N: usize>(

View File

@@ -1,7 +1,6 @@
//offer ota and config mode
mod backup_manager;
mod file_manager;
mod get_json;
mod get_log;
mod get_static;
@@ -10,10 +9,9 @@ mod post_json;
use crate::fat_error::{FatError, FatResult};
use crate::webserver::backup_manager::{backup_config, backup_info, get_backup_config};
use crate::webserver::file_manager::{file_operations, list_files};
use crate::webserver::get_json::{
get_battery_state, get_config, get_live_moisture, get_log_localization_config, get_solar_state,
get_time, get_timezones, get_version_web, tank_info,
delete_save, get_time, get_timezones, get_version_web, list_saves, tank_info,
};
use crate::webserver::get_log::get_log;
use crate::webserver::get_static::{serve_bundle, serve_favicon, serve_index};
@@ -60,10 +58,7 @@ impl Handler for HTTPRequestRouter {
let method = headers.method;
let path = headers.path;
let prefix = "/file?filename=";
let status = if path.starts_with(prefix) {
file_operations(conn, method, &path, &prefix).await?
} else if path == "/ota" {
let status = if path == "/ota" {
ota_operations(conn, method).await.map_err(|e| {
error!("Error handling ota: {e}");
e
@@ -82,13 +77,20 @@ impl Handler for HTTPRequestRouter {
"/time" => Some(get_time(conn).await),
"/battery" => Some(get_battery_state(conn).await),
"/solar" => Some(get_solar_state(conn).await),
"/get_config" => Some(get_config(conn).await),
"/files" => Some(list_files(conn).await),
"/log_localization" => Some(get_log_localization_config(conn).await),
"/tank" => Some(tank_info(conn).await),
"/backup_info" => Some(backup_info(conn).await),
"/timezones" => Some(get_timezones().await),
"/moisture" => Some(get_live_moisture(conn).await),
"/list_saves" => Some(list_saves(conn).await),
// /get_config accepts an optional ?saveidx=N query parameter
p if p == "/get_config" || p.starts_with("/get_config?") => {
let saveidx: Option<usize> = p
.find("saveidx=")
.and_then(|pos| p[pos + 8..].split('&').next())
.and_then(|s| s.parse().ok());
Some(get_config(conn, saveidx).await)
}
_ => None,
};
match json {
@@ -127,7 +129,26 @@ impl Handler for HTTPRequestRouter {
Some(json) => Some(handle_json(conn, json).await?),
}
}
Method::Options | Method::Delete | Method::Head | Method::Put => None,
Method::Delete => {
// DELETE /delete_save?idx=N
let json = if path == "/delete_save" || path.starts_with("/delete_save?") {
let idx: Option<usize> = path
.find("idx=")
.and_then(|pos| path[pos + 4..].split('&').next())
.and_then(|s| s.parse().ok());
match idx {
Some(idx) => Some(delete_save(conn, idx).await),
None => Some(Err(FatError::String { error: "missing idx parameter".into() })),
}
} else {
None
};
match json {
None => None,
Some(json) => Some(handle_json(conn, json).await?),
}
}
Method::Options | Method::Head | Method::Put => None,
_ => None,
}
};