From 4d4fcbe33b70384f5ea1ec4ddaba5ebdedadaef5 Mon Sep 17 00:00:00 2001 From: Empire Date: Sun, 5 Apr 2026 13:30:11 +0200 Subject: [PATCH] store backup now in binary, and let backend serialize/deserialize --- Software/MainBoard/rust/Cargo.toml | 2 +- Software/MainBoard/rust/src/config.rs | 30 ++--- Software/MainBoard/rust/src/hal/esp.rs | 9 +- Software/MainBoard/rust/src/hal/mod.rs | 7 +- Software/MainBoard/rust/src/hal/rtc.rs | 91 +++----------- Software/MainBoard/rust/src/hal/v4_hal.rs | 61 ++++++++- Software/MainBoard/rust/src/main.rs | 4 +- Software/MainBoard/rust/src/plant_state.rs | 3 +- .../rust/src/webserver/backup_manager.rs | 118 ++---------------- 9 files changed, 121 insertions(+), 204 deletions(-) diff --git a/Software/MainBoard/rust/Cargo.toml b/Software/MainBoard/rust/Cargo.toml index 4fce08b..00c57ed 100644 --- a/Software/MainBoard/rust/Cargo.toml +++ b/Software/MainBoard/rust/Cargo.toml @@ -115,7 +115,7 @@ littlefs2-core = "0.1.2" # Serialization / codecs serde = { version = "1.0.228", features = ["derive", "alloc"], default-features = false } serde_json = { version = "1.0.145", default-features = false, features = ["alloc"] } -bincode = { version = "2.0.1", default-features = false, features = ["derive"] } +bincode = { version = "2.0.1", default-features = false, features = ["derive", "alloc"] } # Time and time zones chrono = { version = "0.4.42", default-features = false, features = ["iana-time-zone", "alloc", "serde"] } diff --git a/Software/MainBoard/rust/src/config.rs b/Software/MainBoard/rust/src/config.rs index 5dd6a7b..06c9f55 100644 --- a/Software/MainBoard/rust/src/config.rs +++ b/Software/MainBoard/rust/src/config.rs @@ -1,17 +1,17 @@ use crate::hal::PLANT_COUNT; use crate::plant_state::PlantWateringMode; -use alloc::string::String; -use core::str::FromStr; +use alloc::string::{String, ToString}; +use bincode::{Decode, Encode}; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Encode, Decode)] #[serde(default)] pub struct NetworkConfig { - pub ap_ssid: heapless::String<32>, - pub ssid: Option>, - pub password: Option>, + pub ap_ssid: String, + pub ssid: Option, + pub password: Option, pub mqtt_url: Option, - pub base_topic: Option>, + pub base_topic: Option, pub mqtt_user: Option, pub mqtt_password: Option, pub max_wait: u32, @@ -19,7 +19,7 @@ pub struct NetworkConfig { impl Default for NetworkConfig { fn default() -> Self { Self { - ap_ssid: heapless::String::from_str("PlantCtrl Init").unwrap(), + ap_ssid: "PlantCtrl Init".to_string(), ssid: None, password: None, mqtt_url: None, @@ -31,7 +31,7 @@ impl Default for NetworkConfig { } } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Encode, Decode)] #[serde(default)] pub struct NightLampConfig { pub enabled: bool, @@ -54,7 +54,7 @@ impl Default for NightLampConfig { } } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Encode, Decode)] #[serde(default)] pub struct TankConfig { pub tank_sensor_enabled: bool, @@ -79,26 +79,26 @@ impl Default for TankConfig { } } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default, Encode, Decode)] pub enum BatteryBoardVersion { #[default] Disabled, WchI2cSlave, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default, Encode, Decode)] pub enum BoardVersion { Initial, #[default] V4, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default, Encode, Decode)] pub struct BoardHardware { pub board: BoardVersion, pub battery: BatteryBoardVersion, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default, Encode, Decode)] #[serde(default)] pub struct PlantControllerConfig { pub hardware: BoardHardware, @@ -109,7 +109,7 @@ pub struct PlantControllerConfig { pub timezone: Option, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Encode, Decode)] #[serde(default)] pub struct PlantConfig { pub mode: PlantWateringMode, diff --git a/Software/MainBoard/rust/src/hal/esp.rs b/Software/MainBoard/rust/src/hal/esp.rs index 5923a53..e92f10e 100644 --- a/Software/MainBoard/rust/src/hal/esp.rs +++ b/Software/MainBoard/rust/src/hal/esp.rs @@ -749,10 +749,13 @@ impl Esp<'_> { let mut builder: McutieBuilder<'_, String, PublishDisplay, 0> = McutieBuilder::new(stack, "plant ctrl", mqtt_url); - if network_config.mqtt_user.is_some() && network_config.mqtt_password.is_some() { + if let (Some(mqtt_user), Some(mqtt_password)) = ( + network_config.mqtt_user.as_ref(), + network_config.mqtt_password.as_ref(), + ) { builder = builder.with_authentication( - network_config.mqtt_user.as_ref().unwrap().as_str(), - network_config.mqtt_password.as_ref().unwrap().as_str(), + mqtt_user, + mqtt_password, ); info!("With authentification"); } diff --git a/Software/MainBoard/rust/src/hal/mod.rs b/Software/MainBoard/rust/src/hal/mod.rs index b350fc6..4c2045a 100644 --- a/Software/MainBoard/rust/src/hal/mod.rs +++ b/Software/MainBoard/rust/src/hal/mod.rs @@ -10,7 +10,7 @@ mod v4_hal; mod water; use crate::alloc::string::ToString; -use crate::hal::rtc::{DS3231Module, RTCModuleInteraction}; +use crate::hal::rtc::{BackupHeader, DS3231Module, RTCModuleInteraction}; use esp_hal::peripherals::Peripherals; use esp_hal::peripherals::ADC1; use esp_hal::peripherals::GPIO0; @@ -150,6 +150,7 @@ pub trait BoardInteraction<'a> { async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError>; async fn deep_sleep(&mut self, duration_in_ms: u64) -> !; + fn is_day(&self) -> bool; //should be multsampled async fn light(&mut self, enable: bool) -> FatResult<()>; @@ -164,6 +165,10 @@ pub trait BoardInteraction<'a> { async fn get_mptt_current(&mut self) -> FatResult; async fn can_power(&mut self, state: bool) -> FatResult<()>; + async fn backup_config(&mut self, config: &PlantControllerConfig) -> FatResult<()>; + async fn read_backup(&mut self) -> FatResult; + async fn backup_info(&mut self) -> FatResult; + // Return JSON string with autodetected sensors per plant. Default: not supported. async fn detect_sensors(&mut self, _request: Detection) -> FatResult { bail!("Autodetection is only available on v4 HAL with CAN bus"); diff --git a/Software/MainBoard/rust/src/hal/rtc.rs b/Software/MainBoard/rust/src/hal/rtc.rs index 223f2af..11d7546 100644 --- a/Software/MainBoard/rust/src/hal/rtc.rs +++ b/Software/MainBoard/rust/src/hal/rtc.rs @@ -1,8 +1,7 @@ -use crate::fat_error::FatResult; use crate::hal::Box; +use crate::fat_error::FatResult; use async_trait::async_trait; -use bincode::config::Configuration; -use bincode::{config, Decode, Encode}; +use bincode::{Decode, Encode}; use chrono::{DateTime, Utc}; use ds323x::ic::DS3231; use ds323x::interface::I2cInterface; @@ -19,24 +18,21 @@ use esp_hal::Blocking; use serde::{Deserialize, Serialize}; pub const X25: crc::Crc = crc::Crc::::new(&crc::CRC_16_IBM_SDLC); -const CONFIG: Configuration = config::standard(); +pub const EEPROM_PAGE: usize = 32; // #[async_trait(?Send)] pub trait RTCModuleInteraction { - async fn get_backup_info(&mut self) -> FatResult; - async fn get_backup_config(&mut self, chunk: usize) -> FatResult<([u8; 32], usize, u16)>; - async fn backup_config(&mut self, offset: usize, bytes: &[u8]) -> FatResult<()>; - async fn backup_config_finalize(&mut self, crc: u16, length: usize) -> FatResult<()>; async fn get_rtc_time(&mut self) -> FatResult>; async fn set_rtc_time(&mut self, time: &DateTime) -> FatResult<()>; + + fn write(&mut self, offset: u32, data: &[u8]) -> FatResult<()>; + fn read(&mut self, offset:u32, data: &mut [u8]) -> FatResult<()>; } -// -const BACKUP_HEADER_MAX_SIZE: usize = 64; #[derive(Serialize, Deserialize, PartialEq, Debug, Default, Encode, Decode)] pub struct BackupHeader { pub timestamp: i64, - crc16: u16, + pub(crate) crc16: u16, pub size: u16, } // @@ -46,7 +42,7 @@ pub struct DS3231Module { DS3231, >, - pub(crate) storage: eeprom24x::Storage< + pub storage: eeprom24x::Storage< I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>, B32, TwoBytes, @@ -57,67 +53,6 @@ pub struct DS3231Module { #[async_trait(?Send)] impl RTCModuleInteraction for DS3231Module { - async fn get_backup_info(&mut self) -> FatResult { - let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE]; - - self.storage.read(0, &mut header_page_buffer)?; - - let (header, len): (BackupHeader, usize) = - bincode::decode_from_slice(&header_page_buffer[..], CONFIG)?; - - log::info!("Raw header is {header_page_buffer:?} with size {len}"); - Ok(header) - } - - async fn get_backup_config(&mut self, chunk: usize) -> FatResult<([u8; 32], usize, u16)> { - let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE]; - - self.storage.read(0, &mut header_page_buffer)?; - let (header, _header_size): (BackupHeader, usize) = - bincode::decode_from_slice(&header_page_buffer[..], CONFIG)?; - - let mut buf = [0_u8; 32]; - let offset = chunk * buf.len() + BACKUP_HEADER_MAX_SIZE; - - let end: usize = header.size as usize + BACKUP_HEADER_MAX_SIZE; - let current_end = offset + buf.len(); - let chunk_size = if current_end > end { - end - offset - } else { - buf.len() - }; - if chunk_size == 0 { - Ok((buf, 0, header.crc16)) - } else { - self.storage.read(offset as u32, &mut buf)?; - //&buf[..chunk_size]; - Ok((buf, chunk_size, header.crc16)) - } - } - async fn backup_config(&mut self, offset: usize, bytes: &[u8]) -> FatResult<()> { - //skip header and write after - self.storage - .write((BACKUP_HEADER_MAX_SIZE + offset) as u32, bytes)?; - - Ok(()) - } - - async fn backup_config_finalize(&mut self, crc: u16, length: usize) -> FatResult<()> { - let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE]; - - let time = self.get_rtc_time().await?.timestamp_millis(); - let header = BackupHeader { - crc16: crc, - timestamp: time, - size: length as u16, - }; - let config = config::standard(); - let encoded = bincode::encode_into_slice(&header, &mut header_page_buffer, config)?; - log::info!("Raw header is {header_page_buffer:?} with size {encoded}"); - self.storage.write(0, &header_page_buffer)?; - Ok(()) - } - async fn get_rtc_time(&mut self) -> FatResult> { Ok(self.rtc.datetime()?.and_utc()) } @@ -126,4 +61,14 @@ impl RTCModuleInteraction for DS3231Module { let naive_time = time.naive_utc(); Ok(self.rtc.set_datetime(&naive_time)?) } + + fn write(&mut self, offset: u32, data: &[u8]) -> FatResult<()> { + self.storage.write(offset, data)?; + Ok(()) + } + + fn read(&mut self, offset:u32, data: &mut [u8]) -> FatResult<()> { + self.storage.read(offset, data)?; + Ok(()) + } } diff --git a/Software/MainBoard/rust/src/hal/v4_hal.rs b/Software/MainBoard/rust/src/hal/v4_hal.rs index 9181e83..19e653c 100644 --- a/Software/MainBoard/rust/src/hal/v4_hal.rs +++ b/Software/MainBoard/rust/src/hal/v4_hal.rs @@ -3,7 +3,7 @@ use crate::config::PlantControllerConfig; use crate::fat_error::{ContextExt, FatError, FatResult}; use crate::hal::battery::BatteryInteraction; use crate::hal::esp::{hold_disable, hold_enable, Esp}; -use crate::hal::rtc::RTCModuleInteraction; +use crate::hal::rtc::{BackupHeader, RTCModuleInteraction, EEPROM_PAGE, X25}; use crate::hal::water::TankSensor; use crate::hal::{ BoardInteraction, Detection, FreePeripherals, Moistures, Sensor, I2C_DRIVER, PLANT_COUNT, @@ -13,6 +13,7 @@ use crate::log::{LogMessage, LOG_ACCESS}; use alloc::boxed::Box; use alloc::string::ToString; use async_trait::async_trait; +use bincode::config; use canapi::id::{classify, plant_id, MessageKind, IDENTIFY_CMD_OFFSET}; use canapi::SensorSlot; use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice; @@ -32,6 +33,9 @@ use measurements::Resistance; use measurements::{Current, Voltage}; use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface}; +pub const BACKUP_HEADER_MAX_SIZE: usize = 64; +const CONFIG: config::Configuration = config::standard(); + const MPPT_CURRENT_SHUNT_OHMS: f64 = 0.05_f64; const TWAI_BAUDRATE: twai::BaudRate = twai::BaudRate::Custom(twai::TimingConfig { baud_rate_prescaler: 200, // 40MHz / 200 * 2 = 100 on C6, 100 * 20 = 2000 divisor, 40MHz / 2000 = 20kHz @@ -538,6 +542,59 @@ impl<'a> BoardInteraction<'a> for V4<'a> { } Ok(()) } + async fn backup_config(&mut self, controller_config: &PlantControllerConfig) -> FatResult<()>{ + let mut buffer: [u8; 4096-BACKUP_HEADER_MAX_SIZE] = [0; 4096-BACKUP_HEADER_MAX_SIZE]; + let length = bincode::encode_into_slice(controller_config, &mut buffer, CONFIG)?; + let mut checksum = X25.digest(); + checksum.update(&buffer[..length]); + let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE]; + + let time = self.rtc_module.get_rtc_time().await?.timestamp_millis(); + let header = BackupHeader { + crc16: checksum.finalize(), + timestamp: time, + size: length as u16, + }; + bincode::encode_into_slice(&header, &mut header_page_buffer, CONFIG)?; + self.get_rtc_module().write(0, &header_page_buffer)?; + let mut to_write = length; + let mut chunk: usize = 0; + + while to_write > 0 { + self.progress(chunk as u32).await; + let start = BACKUP_HEADER_MAX_SIZE + chunk* EEPROM_PAGE; + let end = start + crate::hal::rtc::EEPROM_PAGE; + let part = &buffer[start..end]; + to_write -= part.len(); + chunk += 1; + self.get_rtc_module().write((1 + chunk) as u32, part)?; + } + self.clear_progress().await; + Ok(()) + } + + async fn read_backup(&mut self) -> FatResult { + let info = self.backup_info().await?; + let mut store = alloc::vec![0_u8; info.size as usize]; + self.rtc_module.read(BACKUP_HEADER_MAX_SIZE as u32, store.as_mut_slice())?; + let mut checksum = X25.digest(); + checksum.update(&store[..]); + let crc = checksum.finalize(); + if crc != info.crc16 { + bail!("CRC mismatch in backup data") + } + let (decoded, _) = bincode::decode_from_slice(&store[..], CONFIG)?; + Ok(decoded) + } + + async fn backup_info(&mut self) -> FatResult { + let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE]; + self.get_rtc_module().read(0, &mut header_page_buffer)?; + + let info: Result<(BackupHeader, usize), bincode::error::DecodeError> = + bincode::decode_from_slice(&header_page_buffer[..], CONFIG); + info.map(|(header, _)| header).map_err(|e| FatError::String {error:"Could not read backup header: ".to_string() + &e.to_string()}) + } } async fn wait_for_can_measurements( @@ -597,6 +654,8 @@ async fn wait_for_can_measurements( } } } + + } impl From for Detection { diff --git a/Software/MainBoard/rust/src/main.rs b/Software/MainBoard/rust/src/main.rs index 0f65180..337011f 100644 --- a/Software/MainBoard/rust/src/main.rs +++ b/Software/MainBoard/rust/src/main.rs @@ -1049,8 +1049,8 @@ async fn wait_infinity( exit_hold_blink = !exit_hold_blink; let progress = core::cmp::min(elapsed, exit_hold_duration); - let lit = ((progress.as_millis() as u64 * 8) - / exit_hold_duration.as_millis() as u64) + let lit = ((progress.as_millis() * 8) + / exit_hold_duration.as_millis()) .saturating_add(1) .min(8) as usize; diff --git a/Software/MainBoard/rust/src/plant_state.rs b/Software/MainBoard/rust/src/plant_state.rs index 26f914b..560c920 100644 --- a/Software/MainBoard/rust/src/plant_state.rs +++ b/Software/MainBoard/rust/src/plant_state.rs @@ -1,3 +1,4 @@ +use bincode::{Decode, Encode}; use crate::hal::Moistures; use crate::{config::PlantConfig, hal::HAL, in_time_range}; use chrono::{DateTime, TimeDelta, Utc}; @@ -70,7 +71,7 @@ impl PumpState { } } -#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Encode, Decode)] pub enum PlantWateringMode { Off, TargetMoisture, diff --git a/Software/MainBoard/rust/src/webserver/backup_manager.rs b/Software/MainBoard/rust/src/webserver/backup_manager.rs index ef6c4b0..f567fee 100644 --- a/Software/MainBoard/rust/src/webserver/backup_manager.rs +++ b/Software/MainBoard/rust/src/webserver/backup_manager.rs @@ -1,13 +1,11 @@ use crate::fat_error::{FatError, FatResult}; -use crate::hal::rtc::X25; +use crate::webserver::read_up_to_bytes_from_request; use crate::BOARD_ACCESS; use alloc::borrow::ToOwned; -use alloc::format; use alloc::string::{String, ToString}; use chrono::DateTime; use edge_http::io::server::Connection; use edge_nal::io::{Read, Write}; -use log::info; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, PartialEq, Debug)] @@ -21,48 +19,9 @@ pub(crate) async fn get_backup_config( where T: Read + Write, { - // First pass: verify checksum without sending data - let mut checksum = X25.digest(); - let mut chunk = 0_usize; - loop { - let mut board = BOARD_ACCESS.get().await.lock().await; - board.board_hal.progress(chunk as u32).await; - let (buf, len, expected_crc) = board - .board_hal - .get_rtc_module() - .get_backup_config(chunk) - .await?; + let mut board = BOARD_ACCESS.get().await.lock().await; + let backup = board.board_hal.read_backup().await?; - // Update checksum with the actual data bytes of this chunk - checksum.update(&buf[..len]); - - let is_last = len == 0 || len < buf.len(); - if is_last { - let actual_crc = checksum.finalize(); - if actual_crc != expected_crc { - BOARD_ACCESS - .get() - .await - .lock() - .await - .board_hal - .clear_progress() - .await; - conn.initiate_response( - 409, - Some( - format!("Checksum mismatch expected {expected_crc} got {actual_crc}") - .as_str(), - ), - &[], - ) - .await?; - return Ok(Some(409)); - } - break; - } - chunk += 1; - } // Second pass: stream data conn.initiate_response( 200, @@ -75,35 +34,7 @@ where ) .await?; - let mut chunk = 0_usize; - loop { - let mut board = BOARD_ACCESS.get().await.lock().await; - board.board_hal.progress(chunk as u32).await; - let (buf, len, _expected_crc) = board - .board_hal - .get_rtc_module() - .get_backup_config(chunk) - .await?; - - if len == 0 { - break; - } - - conn.write_all(&buf[..len]).await?; - - if len < buf.len() { - break; - } - chunk += 1; - } - BOARD_ACCESS - .get() - .await - .lock() - .await - .board_hal - .clear_progress() - .await; + conn.write_all(serde_json::to_string(&backup)?.as_bytes()).await?; Ok(Some(200)) } @@ -113,39 +44,10 @@ pub(crate) async fn backup_config( where T: Read + Write, { - let mut offset = 0_usize; - let mut buf = [0_u8; 32]; - - let mut checksum = X25.digest(); - - let mut counter = 0; - loop { - let to_write = conn.read(&mut buf).await?; - if to_write == 0 { - info!("backup finished"); - break; - } else { - let mut board = BOARD_ACCESS.get().await.lock().await; - board.board_hal.progress(counter).await; - - counter += 1; - board - .board_hal - .get_rtc_module() - .backup_config(offset, &buf[0..to_write]) - .await?; - checksum.update(&buf[0..to_write]); - } - offset += to_write; - } - + let input = read_up_to_bytes_from_request(conn, Option::None).await?; let mut board = BOARD_ACCESS.get().await.lock().await; - board - .board_hal - .get_rtc_module() - .backup_config_finalize(checksum.finalize(), offset) - .await?; - board.board_hal.clear_progress().await; + let config_to_backup = serde_json::from_slice(&input)?; + board.board_hal.backup_config(&config_to_backup).await?; conn.initiate_response( 200, Some("OK"), @@ -166,8 +68,10 @@ where T: Read + Write, { let mut board = BOARD_ACCESS.get().await.lock().await; - let header = board.board_hal.get_rtc_module().get_backup_info().await; - let json = match header { + let info = board.board_hal.backup_info().await; + + + let json = match info { Ok(h) => { let timestamp = DateTime::from_timestamp_millis(h.timestamp).unwrap(); let wbh = WebBackupHeader {