diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 88bddf6..062344c 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -79,6 +79,8 @@ chrono = { version = "0.4.23", default-features = false , features = ["iana-time chrono-tz = {version="0.8.0", default-features = false , features = [ "filter-by-regex" ]} eeprom24x = "0.7.2" url = "2.5.3" +crc = "3.2.1" +bincode = "1.3.3" [patch.crates-io] diff --git a/rust/src/plant_hal.rs b/rust/src/plant_hal.rs index fee7f41..b5eb4cc 100644 --- a/rust/src/plant_hal.rs +++ b/rust/src/plant_hal.rs @@ -1,8 +1,9 @@ use bq34z100::{Bq34Z100Error, Bq34z100g1, Bq34z100g1Driver}; +use chrono_tz::Tz; use ds323x::{DateTimeAccess, Ds323x}; -use eeprom24x::{Eeprom24x, SlaveAddr}; +use eeprom24x::{Eeprom24x, Eeprom24xTrait, SlaveAddr}; use embedded_hal_bus::i2c::MutexDevice; use embedded_svc::wifi::{ AccessPointConfiguration, AccessPointInfo, AuthMethod, ClientConfiguration, Configuration, @@ -29,7 +30,7 @@ use esp_idf_sys::esp_restart; use anyhow::{anyhow, Context}; use anyhow::{bail, Ok, Result}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use std::ffi::CString; use std::fs::{self, File}; use std::path::Path; @@ -114,6 +115,8 @@ const SENSOR_B_6: u8 = 13; const SENSOR_B_7: u8 = 14; const SENSOR_B_8: u8 = 15; +const X25: crc::Crc = crc::Crc::::new(&crc::CRC_16_IBM_SDLC); + #[link_section = ".rtc.data"] static mut LAST_WATERING_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT]; #[link_section = ".rtc.data"] @@ -201,6 +204,13 @@ pub struct BatteryState { temperature: String, } +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct BackupHeader{ + timestamp: i64, + crc16: u16, + size: usize +} + impl PlantCtrlBoard<'_> { pub fn deep_sleep(&mut self, duration_in_ms:u64) -> !{ unsafe { @@ -214,10 +224,95 @@ impl PlantCtrlBoard<'_> { esp_sleep_enable_ext1_wakeup(0b10u64, esp_sleep_ext1_wakeup_mode_t_ESP_EXT1_WAKEUP_ANY_LOW); esp_deep_sleep(duration_in_ms); } - }; } + pub fn get_backup_config(&mut self) -> Result> { + let dummy = BackupHeader{ + timestamp: 0, + crc16: 0, + size: 0, + }; + let store = bincode::serialize(&dummy)?.len(); + let mut header_page_buffer = vec![0_u8; store]; + + match self.eeprom.read_data(0, &mut header_page_buffer) { + OkStd(_) => {}, + Err(err) => bail!("Error reading eeprom header {:?}", err), + }; + println!("Raw header is {:?} with size {}", header_page_buffer , store); + let header:BackupHeader = bincode::deserialize(&header_page_buffer)?; + println!("Reading eeprom header {header:?}"); + + let data_start_address = 1*self.eeprom.page_size() as u32; + let mut data_buffer = vec![0_u8; header.size]; + match self.eeprom.read_data(data_start_address, &mut data_buffer) { + OkStd(_) => {}, + Err(err) => bail!("Error reading eeprom data {:?}", err), + }; + + let checksum = X25.checksum(&data_buffer); + if checksum != header.crc16 { + bail!("Invalid checksum, got {} but expected {}", checksum, header.crc16 ); + } + Ok(data_buffer) + } + + pub fn backup_config(&mut self, bytes: &[u8]) -> Result<()>{ + let delay = Delay::new_default(); + + let checksum = X25.checksum(bytes); + let page_size = self.eeprom.page_size(); + + let time = self.get_rtc_time()?.timestamp_millis(); + + let header = BackupHeader{ + crc16 : checksum, + timestamp : time, + size: bytes.len(), + }; + + let encoded = bincode::serialize(&header)?; + if encoded.len() > page_size { + bail!("Size limit reached header is {}, but firest page is only {}",encoded.len(), page_size) + } + let as_u8:&[u8] = &encoded; + + println!("Raw header is {:?} with size {}", as_u8 , as_u8.len()); + + match self.eeprom.write_page(0, as_u8) { + OkStd(_) => {}, + Err(err) => bail!("Error writing eeprom {:?}", err), + }; + delay.delay_ms(5); + + let to_write= bytes.chunks(page_size); + + let mut lastiter = 0; + let mut current_page = 1; + for chunk in to_write { + let address = current_page*page_size as u32; + match self.eeprom.write_page(address, chunk) { + OkStd(_) => {}, + Err(err) => bail!("Error writing eeprom {:?}", err), + }; + current_page = current_page+1; + + let iter = ((current_page/1)%8 ) as usize; + if iter != lastiter { + for i in 0..PLANT_COUNT { + self.fault(i, iter==i); + } + lastiter = iter; + } + + //update led here? + delay.delay_ms(5); + } + return Ok(()) + + } + pub fn get_battery_state(&mut self) -> BatteryState { let bat = BatteryState { voltage_milli_volt: to_string(self.voltage_milli_volt()), diff --git a/rust/src/webserver/webserver.rs b/rust/src/webserver/webserver.rs index e2e8f06..086078d 100644 --- a/rust/src/webserver/webserver.rs +++ b/rust/src/webserver/webserver.rs @@ -136,6 +136,29 @@ fn get_config( anyhow::Ok(Some(json)) } +fn backup_config( + request: &mut Request<&mut EspHttpConnection>, +) -> Result, anyhow::Error> { + let all = read_up_to_bytes_from_request(request, Some(3072))?; + let mut board = BOARD_ACCESS.lock().unwrap(); + board.backup_config(&all)?; + anyhow::Ok(Some("saved".to_owned())) +} + +fn get_backup_config( + _request: &mut Request<&mut EspHttpConnection>, +) -> Result, anyhow::Error> { + let mut board = BOARD_ACCESS.lock().unwrap(); + let json = match board.get_backup_config() { + Ok(config) => std::str::from_utf8(&config)?.to_owned(), + Err(err) => { + println!("Error get backup config {:?}", err); + err.to_string() + } + }; + anyhow::Ok(Some(json)) +} + fn set_config( request: &mut Request<&mut EspHttpConnection>, ) -> Result, anyhow::Error> { @@ -353,12 +376,22 @@ pub fn httpd(reboot_now: Arc) -> Box> { handle_error_to500(request, get_config) }) .unwrap(); + server + .fn_handler("/get_backup_config", Method::Get, move |request| { + handle_error_to500(request, get_backup_config) + }) + .unwrap(); server .fn_handler("/set_config", Method::Post, move |request| { handle_error_to500(request, set_config) }) .unwrap(); + server + .fn_handler("/backup_config", Method::Post, move |request| { + handle_error_to500(request, backup_config) + }) + .unwrap(); server .fn_handler("/files", Method::Get, move |request| { handle_error_to500(request, list_files) diff --git a/rust/src_webpack/src/main.html b/rust/src_webpack/src/main.html index 1413ea6..f8bd1bd 100644 --- a/rust/src_webpack/src/main.html +++ b/rust/src_webpack/src/main.html @@ -163,6 +163,8 @@
+ +
diff --git a/rust/src_webpack/src/main.ts b/rust/src_webpack/src/main.ts index ca10441..4135445 100644 --- a/rust/src_webpack/src/main.ts +++ b/rust/src_webpack/src/main.ts @@ -138,6 +138,17 @@ export class Controller { controller.firmWareView.setVersion(versionInfo); }) } + + getBackupConfig() { + controller.progressview.addIndeterminate("get_backup_config", "Downloading Backup") + fetch(PUBLIC_URL + "/get_backup_config") + .then(response => response.json()) + .then(loaded => { + controller.progressview.removeProgress("get_config") + alert(loaded) + }) + } + downloadConfig() { controller.progressview.addIndeterminate("get_config", "Downloading Config") fetch(PUBLIC_URL + "/get_config") @@ -166,6 +177,16 @@ export class Controller { //load from remote to be clean controller.downloadConfig() } + backupConfig(json: string, statusCallback: (status: string) => void) { + controller.progressview.addIndeterminate("backup_config", "Backingup Config") + fetch(PUBLIC_URL + "/backup_config", { + method: "POST", + body: json, + }) + .then(response => response.text()) + .then(text => statusCallback(text)) + controller.progressview.removeProgress("backup_config") + } syncRTCFromBrowser() { controller.progressview.addIndeterminate("write_rtc", "Writing RTC") var value: SetTime = { diff --git a/rust/src_webpack/src/submitView.ts b/rust/src_webpack/src/submitView.ts index 9fa63d8..f449d13 100644 --- a/rust/src_webpack/src/submitView.ts +++ b/rust/src_webpack/src/submitView.ts @@ -1,22 +1,34 @@ import { Controller } from "./main"; -export class SubmitView{ - json: HTMLInputElement; - submitFormBtn: HTMLButtonElement; - submit_status: HTMLElement; - - constructor(controller: Controller){ - this.json = document.getElementById('json') as HTMLInputElement - this.submitFormBtn = document.getElementById("submit") as HTMLButtonElement - this.submit_status = document.getElementById("submit_status") as HTMLElement - this.submitFormBtn.onclick = () => { - controller.uploadConfig(this.json.value, (status:string) => { - this.submit_status.innerHTML = status; - }); - } +export class SubmitView { + json: HTMLInputElement; + submitFormBtn: HTMLButtonElement; + submit_status: HTMLElement; + backupBtn: HTMLButtonElement; + restoreBackupBtn: HTMLButtonElement; + + constructor(controller: Controller) { + this.json = document.getElementById('json') as HTMLInputElement + this.submitFormBtn = document.getElementById("submit") as HTMLButtonElement + this.backupBtn = document.getElementById("backup") as HTMLButtonElement + this.restoreBackupBtn = document.getElementById("restorebackup") as HTMLButtonElement + this.submit_status = document.getElementById("submit_status") as HTMLElement + this.submitFormBtn.onclick = () => { + controller.uploadConfig(this.json.value, (status: string) => { + this.submit_status.innerHTML = status; + }); } - - setJson(pretty: string) { - this.json.value = pretty + this.backupBtn.onclick = () => { + controller.backupConfig(this.json.value, (status: string) => { + this.submit_status.innerHTML = status; + }); + this.restoreBackupBtn.onclick = () => { + controller.getBackupConfig(); + } } - } \ No newline at end of file + } + + setJson(pretty: string) { + this.json.value = pretty + } +} \ No newline at end of file