diff --git a/rust/src/main.rs b/rust/src/main.rs index ad750e7..6dcb9c1 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -10,7 +10,17 @@ use chrono_tz::{Europe::Berlin, Tz}; use config::Mode; use esp_idf_hal::delay::Delay; use esp_idf_sys::{ - esp_deep_sleep, esp_ota_get_app_partition_count, esp_ota_get_running_partition, esp_ota_get_state_partition, esp_ota_img_states_t, esp_ota_img_states_t_ESP_OTA_IMG_ABORTED, esp_ota_img_states_t_ESP_OTA_IMG_INVALID, esp_ota_img_states_t_ESP_OTA_IMG_NEW, esp_ota_img_states_t_ESP_OTA_IMG_PENDING_VERIFY, esp_ota_img_states_t_ESP_OTA_IMG_UNDEFINED, esp_ota_img_states_t_ESP_OTA_IMG_VALID, esp_restart, esp_sleep_enable_ext1_wakeup, esp_sleep_ext1_wakeup_mode_t_ESP_EXT1_WAKEUP_ANY_LOW, vTaskDelay, CONFIG_FREERTOS_HZ + esp_ota_get_app_partition_count, + esp_ota_get_running_partition, + esp_ota_get_state_partition, + esp_ota_img_states_t, + esp_ota_img_states_t_ESP_OTA_IMG_ABORTED, + esp_ota_img_states_t_ESP_OTA_IMG_INVALID, + esp_ota_img_states_t_ESP_OTA_IMG_NEW, + esp_ota_img_states_t_ESP_OTA_IMG_PENDING_VERIFY, + esp_ota_img_states_t_ESP_OTA_IMG_UNDEFINED, + esp_ota_img_states_t_ESP_OTA_IMG_VALID, + vTaskDelay }; use log::error; use once_cell::sync::Lazy; @@ -460,8 +470,7 @@ fn safe_main() -> anyhow::Result<()> { board.last_pump_time(plant); state.active = true; for _ in 0..plant_config.pump_time_s { - unsafe { vTaskDelay(CONFIG_FREERTOS_HZ) }; - //info message or something? + Delay::new_default().delay_ms(1000); } board.pump(plant, false)?; @@ -557,10 +566,7 @@ fn safe_main() -> anyhow::Result<()> { let _webserver = httpd(reboot_now.clone()); wait_infinity(WaitType::MqttConfig, reboot_now.clone()); } - unsafe { - esp_sleep_enable_ext1_wakeup(0b10u64, esp_sleep_ext1_wakeup_mode_t_ESP_EXT1_WAKEUP_ANY_LOW); - esp_deep_sleep(1000 * 1000 * 60 * deep_sleep_duration_minutes as u64) - }; + board.deep_sleep(1000 * 1000 * 60 * deep_sleep_duration_minutes as u64); } fn publish_battery_state( @@ -949,8 +955,7 @@ fn wait_infinity(wait_type: WaitType, reboot_now: Arc) -> ! { } } if reboot_now.load(std::sync::atomic::Ordering::Relaxed) { - println!("Rebooting"); - esp_restart(); + BOARD_ACCESS.lock().unwrap().deep_sleep( 1); } } } @@ -961,7 +966,7 @@ fn main() { match result { Ok(_) => { println!("Main app finished, restarting"); - unsafe { esp_restart() }; + BOARD_ACCESS.lock().unwrap().deep_sleep(1); } Err(err) => { println!("Failed main {}", err); diff --git a/rust/src/plant_hal.rs b/rust/src/plant_hal.rs index c80782d..bfbbdff 100644 --- a/rust/src/plant_hal.rs +++ b/rust/src/plant_hal.rs @@ -7,6 +7,7 @@ use embedded_hal_bus::i2c::MutexDevice; use embedded_svc::wifi::{ AccessPointConfiguration, AccessPointInfo, AuthMethod, ClientConfiguration, Configuration, }; + use esp_idf_hal::adc::oneshot::config::AdcChannelConfig; use esp_idf_hal::adc::oneshot::{AdcChannelDriver, AdcDriver}; use esp_idf_hal::adc::{attenuation, Resolution}; @@ -23,6 +24,8 @@ use esp_idf_svc::wifi::EspWifi; use measurements::Temperature; use once_cell::sync::Lazy; use plant_ctrl2::sipo::ShiftRegister40; +use esp_idf_sys::esp_deep_sleep; +use esp_idf_sys::esp_restart; use anyhow::{anyhow, Context}; use anyhow::{bail, Ok, Result}; @@ -53,6 +56,7 @@ use esp_idf_sys::{esp, esp_spiffs_check, gpio_hold_dis, gpio_hold_en, vTaskDelay use one_wire_bus::OneWire; use crate::config::{self, PlantControllerConfig}; +use crate::espota::mark_app_valid; use crate::{plant_hal, to_string, STAY_ALIVE}; //Only support for 8 right now! @@ -196,6 +200,21 @@ pub struct BatteryState { } impl PlantCtrlBoard<'_> { + pub fn deep_sleep(&mut self, duration_in_ms:u64) -> !{ + unsafe { + //if we dont do this here, we might just revert a newly flashed firmeware + mark_app_valid(); + //allow early wakup by pressing the boot button + if duration_in_ms == 0 { + esp_restart(); + } else { + // 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_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 661027f..26c0565 100644 --- a/rust/src/webserver/webserver.rs +++ b/rust/src/webserver/webserver.rs @@ -4,8 +4,6 @@ use std::{ str::from_utf8, sync::{atomic::AtomicBool, Arc}, }; - -use esp_idf_svc::io::BufRead; use crate::{ espota::OtaUpdate, get_version, map_range_moisture, plant_hal::FileInfo, BOARD_ACCESS, }; @@ -56,11 +54,8 @@ pub struct TestPump { fn write_time( request: &mut Request<&mut EspHttpConnection>, ) -> Result, anyhow::Error> { - let mut buf = [0_u8; 3072]; - let read = request.read(&mut buf)?; - let actual_data = &buf[0..read]; - println!("Raw data {}", from_utf8(actual_data)?); - let time: SetTime = serde_json::from_slice(actual_data)?; + let actual_data = read_up_to_bytes_from_request(request, None)?; + let time: SetTime = serde_json::from_slice(&actual_data)?; let parsed = DateTime::parse_from_rfc3339(time.time).map_err(|err| anyhow::anyhow!(err))?; let mut board = BOARD_ACCESS.lock().unwrap(); board.set_rtc_time(&parsed.to_utc())?; @@ -144,16 +139,14 @@ fn get_config( fn set_config( request: &mut Request<&mut EspHttpConnection>, ) -> Result, anyhow::Error> { - let mut buf = [0_u8; 3072]; - let read = request.read(&mut buf)?; - let actual_data = &buf[0..read]; - println!("Raw data {}", from_utf8(actual_data).unwrap()); - let config: PlantControllerConfig = serde_json::from_slice(actual_data)?; + let all = read_up_to_bytes_from_request(request, Some(3072))?; + let config: PlantControllerConfig = serde_json::from_slice(&all)?; let mut board = BOARD_ACCESS.lock().unwrap(); board.set_config(&config)?; anyhow::Ok(Some("saved".to_owned())) } + fn get_battery_state( _request: &mut Request<&mut EspHttpConnection>, ) -> Result, anyhow::Error> { @@ -172,9 +165,8 @@ fn get_version_web( fn pump_test( request: &mut Request<&mut EspHttpConnection>, ) -> Result, anyhow::Error> { - let mut buf = [0_u8; 3072]; - let read = request.read(&mut buf)?; - let pump_test: TestPump = serde_json::from_slice(&buf[0..read])?; + let actual_data = read_up_to_bytes_from_request(request, None)?; + let pump_test: TestPump = serde_json::from_slice(&actual_data)?; let mut board = BOARD_ACCESS.lock().unwrap(); board.test_pump(pump_test.pump)?; anyhow::Ok(None) @@ -285,7 +277,7 @@ fn query_param(uri: &str, param_name: &str) -> Option { } } -pub fn httpd(_reboot_now: Arc) -> Box> { +pub fn httpd(reboot_now: Arc) -> Box> { let server_config = Configuration { stack_size: 32768, ..Default::default() @@ -358,6 +350,25 @@ pub fn httpd(_reboot_now: Arc) -> Box> { handle_error_to500(request, list_files) }) .unwrap(); + let reboot_now_for_reboot = reboot_now.clone(); + server + .fn_handler("/reboot", Method::Post, move |_| { + BOARD_ACCESS + .lock() + .unwrap() + .set_restart_to_conf(true); + reboot_now_for_reboot.store(true, std::sync::atomic::Ordering::Relaxed); + anyhow::Ok(()) + }) + .unwrap(); + + let reboot_now_for_exit = reboot_now.clone(); + server + .fn_handler("/exit", Method::Post, move |_| { + reboot_now_for_exit.store(true, std::sync::atomic::Ordering::Relaxed); + anyhow::Ok(()) + }) + .unwrap(); server .fn_handler("/file", Method::Get, move |request| { let filename = query_param(request.uri(), "filename").unwrap(); @@ -407,6 +418,8 @@ pub fn httpd(_reboot_now: Arc) -> Box> { .unwrap() .get_file_handle(&filename, true); match file_handle { + //TODO get free filesystem size, check against during write if not to large + Ok(mut file_handle) => { const BUFFER_SIZE: usize = 512; let mut buffer: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; @@ -482,7 +495,6 @@ pub fn httpd(_reboot_now: Arc) -> Box> { anyhow::Ok(()) }) .unwrap(); - server .fn_handler("/", Method::Get, move |request| { let mut response = request.into_ok_response()?; @@ -559,3 +571,25 @@ fn handle_error_to500( } return anyhow::Ok(()); } + +fn read_up_to_bytes_from_request(request: &mut Request<&mut EspHttpConnection<'_>>, limit: Option) -> Result, anyhow::Error> { + let max_read = limit.unwrap_or(1024); + let mut data_store = Vec::new(); + let mut total_read = 0; + loop{ + let mut buf = [0_u8; 64]; + let read = request.read(&mut buf)?; + if read == 0 { + break; + } + let actual_data = &buf[0..read]; + total_read += read; + if total_read > max_read{ + bail!("Request too large {total_read} > {max_read}"); + } + data_store.push(actual_data.to_owned()); + } + let allvec = data_store.concat(); + println!("Raw data {}", from_utf8(&allvec)?); + Ok(allvec) +} diff --git a/rust/src_webpack/src/main.html b/rust/src_webpack/src/main.html index 74290ef..ec13db4 100644 --- a/rust/src_webpack/src/main.html +++ b/rust/src_webpack/src/main.html @@ -116,6 +116,9 @@
+ + +
diff --git a/rust/src_webpack/src/main.ts b/rust/src_webpack/src/main.ts index 7f7043a..25728d3 100644 --- a/rust/src_webpack/src/main.ts +++ b/rust/src_webpack/src/main.ts @@ -18,37 +18,37 @@ import { BatteryView } from "./batteryview"; export class Controller { updateRTCData() { fetch(PUBLIC_URL + "/time") - .then(response => response.json()) - .then(json => json as GetTime) - .then(time => { - controller.timeView.update(time.native, time.rtc) - }) - .catch(error => { - controller.timeView.update("n/a","n/a") - console.log(error); - }); + .then(response => response.json()) + .then(json => json as GetTime) + .then(time => { + controller.timeView.update(time.native, time.rtc) + }) + .catch(error => { + controller.timeView.update("n/a", "n/a") + console.log(error); + }); } updateBatteryData() { fetch(PUBLIC_URL + "/battery") - .then(response => response.json()) - .then(json => json as BatteryState) - .then(battery => { - controller.batteryView.update(battery) - }) - .catch(error => { - controller.batteryView.update(null) - console.log(error); - }); + .then(response => response.json()) + .then(json => json as BatteryState) + .then(battery => { + controller.batteryView.update(battery) + }) + .catch(error => { + controller.batteryView.update(null) + console.log(error); + }); } uploadNewFirmware(file: File) { var current = 0; var max = 100; - controller.progressview.addProgress("ota_upload", (current/max) *100 , "Uploading firmeware ("+current+"/" + max+")") + controller.progressview.addProgress("ota_upload", (current / max) * 100, "Uploading firmeware (" + current + "/" + max + ")") var ajax = new XMLHttpRequest(); ajax.upload.addEventListener("progress", event => { - current = event.loaded/1000; - max = event.total/1000; - controller.progressview.addProgress("ota_upload", (current/max) *100 , "Uploading firmeware ("+current+"/" + max+")") + current = event.loaded / 1000; + max = event.total / 1000; + controller.progressview.addProgress("ota_upload", (current / max) * 100, "Uploading firmeware (" + current + "/" + max + ")") }, false); ajax.addEventListener("load", () => { //TODO wait for reboot here! @@ -68,24 +68,24 @@ export class Controller { version() { controller.progressview.addIndeterminate("version", "Getting buildVersion") fetch(PUBLIC_URL + "/version") - .then(response => response.json()) - .then(json => json as VersionInfo) - .then(versionInfo => { - controller.progressview.removeProgress("version") - controller.firmWareView.setVersion(versionInfo); - }) + .then(response => response.json()) + .then(json => json as VersionInfo) + .then(versionInfo => { + controller.progressview.removeProgress("version") + controller.firmWareView.setVersion(versionInfo); + }) } downloadConfig() { controller.progressview.addIndeterminate("get_config", "Downloading Config") fetch(PUBLIC_URL + "/get_config") - .then(response => response.json()) - .then(loaded => { - var currentConfig = loaded as PlantControllerConfig; - this.setConfig(currentConfig); - //sync json view initially - this.configChanged(); - controller.progressview.removeProgress("get_config") - }) + .then(response => response.json()) + .then(loaded => { + var currentConfig = loaded as PlantControllerConfig; + this.setConfig(currentConfig); + //sync json view initially + this.configChanged(); + controller.progressview.removeProgress("get_config") + }) } uploadConfig(json: string, statusCallback: (status: string) => void) { controller.progressview.addIndeterminate("set_config", "Uploading Config") @@ -95,9 +95,9 @@ export class Controller { }) .then(response => response.text()) .then(text => statusCallback(text)) - controller.progressview.removeProgress("set_config") + controller.progressview.removeProgress("set_config") } - syncRTCFromBrowser(){ + syncRTCFromBrowser() { controller.progressview.addIndeterminate("write_rtc", "Writing RTC") var value: SetTime = { time: new Date().toISOString() @@ -117,17 +117,17 @@ export class Controller { console.log(pretty) controller.submitView.setJson(pretty); } - + testPlant(plantId: number) { let counter = 0 let limit = 30 - controller.progressview.addProgress("test_pump", counter/limit*100, "Testing pump " + (plantId+1) + " for " + (limit-counter)+"s") + controller.progressview.addProgress("test_pump", counter / limit * 100, "Testing pump " + (plantId + 1) + " for " + (limit - counter) + "s") let timerId: string | number | NodeJS.Timeout | undefined - function updateProgress(){ + function updateProgress() { counter++; - controller.progressview.addProgress("test_pump", counter/limit*100, "Testing pump " + (plantId+1) + " for " + (limit-counter)+"s") + controller.progressview.addProgress("test_pump", counter / limit * 100, "Testing pump " + (plantId + 1) + " for " + (limit - counter) + "s") timerId = setTimeout(updateProgress, 1000); } @@ -137,21 +137,21 @@ export class Controller { pump: plantId } var pretty = JSON.stringify(body, undefined, 1); - + fetch(PUBLIC_URL + "/pumptest", { method: "POST", body: pretty }) .then(response => response.text()) - .then( + .then( text => { clearTimeout(timerId); controller.progressview.removeProgress("test_pump"); } - ) + ) } - getConfig(): PlantControllerConfig{ + getConfig(): PlantControllerConfig { return { network: controller.networkView.getConfig(), tank: controller.tankView.getConfig(), @@ -163,12 +163,12 @@ export class Controller { scanWifi() { let counter = 0 let limit = 5 - controller.progressview.addProgress("scan_ssid", counter/limit*100, "Scanning for SSIDs for " + (limit-counter)+"s") + controller.progressview.addProgress("scan_ssid", counter / limit * 100, "Scanning for SSIDs for " + (limit - counter) + "s") let timerId: string | number | NodeJS.Timeout | undefined - function updateProgress(){ + function updateProgress() { counter++; - controller.progressview.addProgress("scan_ssid", counter/limit*100, "Scanning for SSIDs for " + (limit-counter)+"s") + controller.progressview.addProgress("scan_ssid", counter / limit * 100, "Scanning for SSIDs for " + (limit - counter) + "s") timerId = setTimeout(updateProgress, 1000); } @@ -200,15 +200,15 @@ export class Controller { this.plantViews.setConfig(current.plants); } - measure_moisture (){ + measure_moisture() { let counter = 0 let limit = 2 - controller.progressview.addProgress("measure_moisture", counter/limit*100, "Measure Moisture " + (limit-counter)+"s") + controller.progressview.addProgress("measure_moisture", counter / limit * 100, "Measure Moisture " + (limit - counter) + "s") let timerId: string | number | NodeJS.Timeout | undefined - function updateProgress(){ + function updateProgress() { counter++; - controller.progressview.addProgress("measure_moisture", counter/limit*100, "Measure Moisture " + (limit-counter)+"s") + controller.progressview.addProgress("measure_moisture", counter / limit * 100, "Measure Moisture " + (limit - counter) + "s") timerId = setTimeout(updateProgress, 1000); } @@ -216,28 +216,62 @@ export class Controller { fetch(PUBLIC_URL + "/moisture") - .then(response => response.json()) - .then(json => json as Moistures) - .then(time => { - controller.plantViews.update(time.moisture_a, time.moisture_b) - clearTimeout(timerId); - controller.progressview.removeProgress("measure_moisture"); + .then(response => response.json()) + .then(json => json as Moistures) + .then(time => { + controller.plantViews.update(time.moisture_a, time.moisture_b) + clearTimeout(timerId); + controller.progressview.removeProgress("measure_moisture"); + }) + .catch(error => { + clearTimeout(timerId); + controller.progressview.removeProgress("measure_moisture"); + console.log(error); + }); + } + + exit() { + fetch(PUBLIC_URL + "/exit", { + method: "POST", }) - .catch(error => { - clearTimeout(timerId); - controller.progressview.removeProgress("measure_moisture"); - console.log(error); - }); + controller.progressview.addIndeterminate("rebooting", "Returned to normal mode, you can close this site now") + + } + + waitForReboot() { + console.log("Check if controller online again") + fetch(PUBLIC_URL + "/version", { + method: "POST", + signal: AbortSignal.timeout(5000) + }).then(response => { + console.log("Reached controller, reloading") + window.location.reload(); + }) + .catch(err => { + console.log("Not reached yet, retrying") + setTimeout(controller.waitForReboot, 1000) + }) + } + + reboot() { + fetch(PUBLIC_URL + "/reboot", { + method: "POST", + }) + controller.progressview.addIndeterminate("rebooting", "Rebooting") + setTimeout(this.waitForReboot, 1000) } + + readonly rebootBtn: HTMLButtonElement + readonly exitBtn: HTMLButtonElement readonly timeView: TimeView; readonly plantViews: PlantViews; readonly networkView: NetworkConfigView; readonly tankView: TankConfigView; readonly nightLampView: NightLampView; readonly submitView: SubmitView; - readonly firmWareView : OTAView; + readonly firmWareView: OTAView; readonly progressview: ProgressView; readonly batteryView: BatteryView; constructor() { @@ -250,6 +284,14 @@ export class Controller { this.submitView = new SubmitView(this) this.firmWareView = new OTAView(this) this.progressview = new ProgressView(this) + this.rebootBtn = document.getElementById("reboot") as HTMLButtonElement + this.rebootBtn.onclick = () => { + controller.reboot(); + } + this.exitBtn = document.getElementById("exit") as HTMLButtonElement + this.exitBtn.onclick = () => { + controller.exit(); + } } } const controller = new Controller(); @@ -258,4 +300,5 @@ controller.updateBatteryData(); controller.downloadConfig(); //controller.measure_moisture(); controller.version(); +controller.progressview.removeProgress("rebooting");