diff --git a/board/PlantCtrlESP32.kicad_prl b/board/PlantCtrlESP32.kicad_prl index 9375f7c..78641cf 100644 --- a/board/PlantCtrlESP32.kicad_prl +++ b/board/PlantCtrlESP32.kicad_prl @@ -1,6 +1,6 @@ { "board": { - "active_layer": 2, + "active_layer": 31, "active_layer_preset": "", "auto_track_width": false, "hidden_netclasses": [], diff --git a/rust/src/main.rs b/rust/src/main.rs index c472b9a..850da95 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -51,7 +51,8 @@ mod webserver { #[derive(Serialize, Deserialize, Debug, PartialEq)] enum WaitType { MissingConfig, - Config, + ConfigButton, + MqttConfig, } #[derive(Serialize, Deserialize, Debug, PartialEq, Default)] @@ -345,7 +346,7 @@ fn safe_main() -> anyhow::Result<()> { drop(board); let reboot_now = Arc::new(AtomicBool::new(false)); let _webserver = httpd(reboot_now.clone()); - wait_infinity(WaitType::Config, reboot_now.clone()); + wait_infinity(WaitType::ConfigButton, reboot_now.clone()); } let tank_state = determine_tank_state(&mut board, &config); @@ -528,7 +529,7 @@ fn safe_main() -> anyhow::Result<()> { drop(board); let reboot_now = Arc::new(AtomicBool::new(false)); let _webserver = httpd(reboot_now.clone()); - wait_infinity(WaitType::Config, reboot_now.clone()); + wait_infinity(WaitType::MqttConfig, reboot_now.clone()); } unsafe { esp_deep_sleep(1000 * 1000 * 60 * deep_sleep_duration_minutes as u64) }; @@ -895,7 +896,8 @@ fn update_plant_state( fn wait_infinity(wait_type: WaitType, reboot_now: Arc) -> ! { let delay = match wait_type { WaitType::MissingConfig => 500_u32, - WaitType::Config => 100_u32, + WaitType::ConfigButton => 100_u32, + WaitType::MqttConfig => 200_u32, }; let mut led_count = 8; loop { @@ -919,7 +921,8 @@ fn wait_infinity(wait_type: WaitType, reboot_now: Arc) -> ! { vTaskDelay(delay); match wait_type { WaitType::MissingConfig => {} - WaitType::Config => { + WaitType::ConfigButton => {} + WaitType::MqttConfig => { if !STAY_ALIVE.load(std::sync::atomic::Ordering::Relaxed) { reboot_now.store(true, std::sync::atomic::Ordering::Relaxed); } diff --git a/rust/src/plant_hal.rs b/rust/src/plant_hal.rs index 866ebcd..450964e 100644 --- a/rust/src/plant_hal.rs +++ b/rust/src/plant_hal.rs @@ -56,7 +56,7 @@ use crate::{plant_hal, STAY_ALIVE}; //Only support for 8 right now! pub const PLANT_COUNT: usize = 8; -const REPEAT_MOIST_MEASURE: usize = 3; +const REPEAT_MOIST_MEASURE: usize = 1; const SPIFFS_PARTITION_NAME: &str = "storage"; const CONFIG_FILE: &str = "/spiffs/config.cfg"; @@ -387,7 +387,7 @@ impl PlantCtrlBoard<'_> { .unwrap(); let delay = Delay::new_default(); - let measurement = 10; + let measurement = 5000; let factor = 1000 as f32 / measurement as f32; //give some time to stabilize @@ -402,6 +402,7 @@ impl PlantCtrlBoard<'_> { delay.delay_ms(10); let unscaled = self.signal_counter.get_counter_value()? as i32; let hz = (unscaled as f32 * factor) as i32; + println!("raw measure unscaled {} hz {}, plant {} sensor {:?}",unscaled, hz, plant, sensor); results[repeat] = hz; //println!("Measuring {:?} @ {} with {}", sensor, plant, hz); } diff --git a/rust/src/webserver/config.html b/rust/src/webserver/config.html index 1be93a1..0bea802 100644 --- a/rust/src/webserver/config.html +++ b/rust/src/webserver/config.html @@ -6,6 +6,14 @@
Buildtime loading
Build githash loading
+

Time

+
+
Esp time
+
Rtc time
+
Rtc time
+
Store Browser time into esp and rtc
+ +

firmeware OTA v3

diff --git a/rust/src/webserver/webserver.rs b/rust/src/webserver/webserver.rs index 603ecd8..782b573 100644 --- a/rust/src/webserver/webserver.rs +++ b/rust/src/webserver/webserver.rs @@ -7,11 +7,12 @@ use std::{ sync::{atomic::AtomicBool, Arc}, }; -use crate::{espota::OtaUpdate, BOARD_ACCESS}; +use crate::{espota::OtaUpdate, map_range_moisture, plant_hal::PLANT_COUNT, BOARD_ACCESS}; +use chrono::DateTime; use core::result::Result::Ok; use embedded_svc::http::Method; use esp_idf_hal::delay::Delay; -use esp_idf_svc::http::server::{Configuration, EspHttpServer}; +use esp_idf_svc::http::server::{Configuration, EspHttpConnection, EspHttpServer, Request}; use heapless::String; use serde::{Deserialize, Serialize}; @@ -28,12 +29,180 @@ struct VersionInfo<'a> { build_time: &'a str, } +#[derive(Serialize, Debug)] +struct LoadData<'a> { + rtc: &'a str, + native: &'a str, + moisture_a: Vec, + moisture_b: Vec, +} + +#[derive(Deserialize, Debug)] +struct SetTime<'a> { + time: &'a str, +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct TestPump { pump: usize, } -pub fn httpd(reboot_now: Arc) -> Box> { +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 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())?; + anyhow::Ok(None) +} + +fn get_data( + _request: &mut Request<&mut EspHttpConnection>, +) -> Result, anyhow::Error> { + let mut board = BOARD_ACCESS.lock().unwrap(); + let native = board + .time() + .and_then(|t| Ok(t.to_rfc3339())) + .unwrap_or("error".to_string()); + let rtc = board + .get_rtc_time() + .and_then(|t| Ok(t.to_rfc3339())) + .unwrap_or("error".to_string()); + + let mut a: Vec = Vec::new(); + let mut b: Vec = Vec::new(); + for plant in 0..2 { + let a_hz = board.measure_moisture_hz(plant, crate::plant_hal::Sensor::A)?; + let b_hz = board.measure_moisture_hz(plant, crate::plant_hal::Sensor::B)?; + let a_pct = map_range_moisture(a_hz as f32); + + match a_pct { + Ok(result) => { + a.push(result); + }, + Err(err) => { + a.push(200); + } + } + + let b_pct = map_range_moisture(b_hz as f32); + match b_pct { + Ok(result) => { + b.push(result); + }, + Err(err) => { + b.push(200); + } + } + } + + + let data = LoadData { + rtc: rtc.as_str(), + native: native.as_str(), + moisture_a: a, + moisture_b: b + }; + let json = serde_json::to_string(&data)?; + + anyhow::Ok(Some(json)) +} + +fn get_config( + _request: &mut Request<&mut EspHttpConnection>, +) -> Result, anyhow::Error> { + let mut board = BOARD_ACCESS.lock().unwrap(); + let json = match board.get_config() { + Ok(config) => serde_json::to_string(&config)?, + Err(_) => serde_json::to_string(&Config::default())?, + }; + anyhow::Ok(Some(json)) +} + +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: Config = serde_json::from_slice(actual_data)?; + let mut board = BOARD_ACCESS.lock().unwrap(); + board.set_config(&config)?; + anyhow::Ok(Some("saved".to_owned())) +} + +fn get_version( + _request: &mut Request<&mut EspHttpConnection>, +) -> Result, anyhow::Error> { + let version_info = VersionInfo { + git_hash: env!("VERGEN_GIT_DESCRIBE"), + build_time: env!("VERGEN_BUILD_TIMESTAMP"), + }; + anyhow::Ok(Some(serde_json::to_string(&version_info)?)) +} + +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 mut board = BOARD_ACCESS.lock().unwrap(); + board.test_pump(pump_test.pump)?; + anyhow::Ok(None) +} + +fn wifi_scan( + _request: &mut Request<&mut EspHttpConnection>, +) -> Result, anyhow::Error> { + let mut board = BOARD_ACCESS.lock().unwrap(); + let scan_result = board.wifi_scan()?; + let mut ssids: Vec<&String<32>> = Vec::new(); + scan_result.iter().for_each(|s| ssids.push(&s.ssid)); + let ssid_json = serde_json::to_string(&SSIDList { ssids })?; + println!("Sending ssid list {}", &ssid_json); + anyhow::Ok(Some(ssid_json)) +} + +fn ota( + request: &mut Request<&mut EspHttpConnection>, +) -> Result, anyhow::Error> { + let mut ota = OtaUpdate::begin()?; + println!("start ota"); + + //having a larger buffer is not really faster, requires more stack and prevents the progress bar from working ;) + const BUFFER_SIZE: usize = 512; + let mut buffer: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; + let mut total_read: usize = 0; + loop { + let read = request.read(&mut buffer)?; + total_read += read; + println!("received {read} bytes ota {total_read}"); + let to_write = &buffer[0..read]; + + ota.write(to_write)?; + println!("wrote {read} bytes ota {total_read}"); + if read == 0 { + break; + } + } + println!("finish ota"); + let partition = ota.raw_partition(); + println!("finalizing and changing boot partition to {partition:?}"); + + let mut finalizer = ota.finalize()?; + println!("changing boot partition"); + finalizer.set_as_boot_partition()?; + finalizer.restart(); +} + +pub fn httpd(_reboot_now: Arc) -> Box> { let server_config = Configuration { stack_size: 32768, ..Default::default() @@ -42,121 +211,49 @@ pub fn httpd(reboot_now: Arc) -> Box> { Box::new(EspHttpServer::new(&server_config).unwrap()); server .fn_handler("/version", Method::Get, |request| { - let mut response = request.into_ok_response()?; - let git_hash = env!("VERGEN_GIT_DESCRIBE"); - let build_time = env!("VERGEN_BUILD_TIMESTAMP"); - let version_info = VersionInfo { - git_hash, - build_time, - }; - let version_info_json = serde_json::to_string(&version_info)?; - response.write(version_info_json.as_bytes())?; - anyhow::Ok(()) + handle_error_to500(request, get_version) + }) + .unwrap(); + + server + .fn_handler("/data", Method::Get, |request| { + handle_error_to500(request, get_data) }) .unwrap(); server - .fn_handler("/bundle.js", Method::Get, |request| { - let mut response = request.into_ok_response()?; - response.write(include_bytes!("bundle.js"))?; - anyhow::Ok(()) + .fn_handler("/time", Method::Post, |request| { + handle_error_to500(request, write_time) }) .unwrap(); server - .fn_handler("/favicon.ico", Method::Get, |request| { - let mut response = request.into_ok_response()?; - response.write(include_bytes!("favicon.ico"))?; - anyhow::Ok(()) - }) - .unwrap(); - server - .fn_handler("/ota", Method::Post, |mut request| { - let ota = OtaUpdate::begin(); - if ota.is_err() { - let error_text = ota.unwrap_err().to_string(); - request - .into_status_response(500)? - .write(error_text.as_bytes())?; - return anyhow::Ok(()); - } - let mut ota = ota.unwrap(); - println!("start ota"); - - //having a larger buffer is not really faster, requires more stack and prevents the progress bar from working ;) - const BUFFER_SIZE: usize = 512; - let mut buffer: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; - let mut total_read: usize = 0; - loop { - let read = request.read(&mut buffer).unwrap(); - total_read += read; - println!("received {read} bytes ota {total_read}"); - let to_write = &buffer[0..read]; - - let write_result = ota.write(to_write); - if write_result.is_err() { - let error_text = write_result.unwrap_err().to_string(); - request - .into_status_response(500)? - .write(error_text.as_bytes())?; - return Ok(()); - } - println!("wrote {read} bytes ota {total_read}"); - if read == 0 { - break; - } - } - println!("finish ota"); - let partition = ota.raw_partition(); - println!("finalizing and changing boot partition to {partition:?}"); - - let finalizer = ota.finalize(); - if finalizer.is_err() { - let error_text = finalizer.err().unwrap().to_string(); - request - .into_status_response(500)? - .write(error_text.as_bytes())?; - return Ok(()); - } - let mut finalizer = finalizer.unwrap(); - - println!("changing boot partition"); - finalizer.set_as_boot_partition().unwrap(); - finalizer.restart(); + .fn_handler("/pumptest", Method::Post, |request| { + handle_error_to500(request, pump_test) }) .unwrap(); server .fn_handler("/boardtest", Method::Post, move |_| { - let mut board = BOARD_ACCESS.lock().unwrap(); - board.test()?; - anyhow::Ok(()) + BOARD_ACCESS.lock().unwrap().test() }) .unwrap(); server - .fn_handler("/pumptest", Method::Post, |mut request| { - let mut buf = [0_u8; 3072]; - let read = request.read(&mut buf); - if read.is_err() { - let error_text = read.unwrap_err().to_string(); - println!("Could not parse testrequest {}", error_text); - request - .into_status_response(500)? - .write(error_text.as_bytes())?; - return anyhow::Ok(()); - } - let actual_data = &buf[0..read.unwrap()]; - println!("Raw data {}", from_utf8(actual_data).unwrap()); - let pump_test: Result = - serde_json::from_slice(actual_data); - if pump_test.is_err() { - let error_text = pump_test.unwrap_err().to_string(); - println!("Could not parse TestPump {}", error_text); - request - .into_status_response(500)? - .write(error_text.as_bytes())?; - return Ok(()); - } - let mut board = BOARD_ACCESS.lock().unwrap(); - board.test_pump(pump_test.unwrap().pump)?; - anyhow::Ok(()) + .fn_handler("/wifiscan", Method::Post, move |request| { + handle_error_to500(request, wifi_scan) + }) + .unwrap(); + server + .fn_handler("/ota", Method::Post, |mut request| { + handle_error_to500(request, ota) + }) + .unwrap(); + server + .fn_handler("/get_config", Method::Get, move |request| { + handle_error_to500(request, get_config) + }) + .unwrap(); + + server + .fn_handler("/set_config", Method::Post, move |mut request| { + handle_error_to500(request, set_config) }) .unwrap(); server @@ -227,25 +324,6 @@ pub fn httpd(reboot_now: Arc) -> Box> { }) .unwrap(); - server - .fn_handler("/wifiscan", Method::Post, move |request| { - let mut response = request.into_ok_response()?; - let mut board = BOARD_ACCESS.lock().unwrap(); - match board.wifi_scan() { - Err(error) => { - response.write(format!("Error scanning wifi: {}", error).as_bytes())?; - } - Ok(scan_result) => { - let mut ssids: Vec<&String<32>> = Vec::new(); - scan_result.iter().for_each(|s| ssids.push(&s.ssid)); - let ssid_json = serde_json::to_string(&SSIDList { ssids })?; - println!("Sending ssid list {}", &ssid_json); - response.write(ssid_json.as_bytes())?; - } - } - anyhow::Ok(()) - }) - .unwrap(); server .fn_handler("/", Method::Get, move |request| { let mut response = request.into_ok_response()?; @@ -253,55 +331,49 @@ pub fn httpd(reboot_now: Arc) -> Box> { anyhow::Ok(()) }) .unwrap(); - server - .fn_handler("/get_config", Method::Get, move |request| { - let mut response = request.into_ok_response()?; - let mut board = BOARD_ACCESS.lock().unwrap(); - match board.get_config() { - Ok(config) => { - let config_json = serde_json::to_string(&config)?; - response.write(config_json.as_bytes())?; - } - Err(_) => { - let config_json = serde_json::to_string(&Config::default())?; - response.write(config_json.as_bytes())?; - } - } + .fn_handler("/bundle.js", Method::Get, |request| { + request + .into_ok_response()? + .write(include_bytes!("bundle.js"))?; anyhow::Ok(()) }) .unwrap(); - server - .fn_handler("/set_config", Method::Post, move |mut request| { - let mut buf = [0_u8; 3072]; - let read = request.read(&mut buf); - if read.is_err() { - let error_text = read.unwrap_err().to_string(); - println!("Could not parse config {}", error_text); - request - .into_status_response(500)? - .write(error_text.as_bytes())?; - return anyhow::Ok(()); - } - let actual_data = &buf[0..read.unwrap()]; - println!("Raw data {}", from_utf8(actual_data).unwrap()); - let config: Result = serde_json::from_slice(actual_data); - if config.is_err() { - let error_text = config.unwrap_err().to_string(); - println!("Could not parse config {}", error_text); - request - .into_status_response(500)? - .write(error_text.as_bytes())?; - return Ok(()); - } - let mut board = BOARD_ACCESS.lock().unwrap(); - board.set_config(&config.unwrap())?; - let mut response = request.into_status_response(202)?; - response.write("saved".as_bytes())?; - reboot_now.store(true, std::sync::atomic::Ordering::Relaxed); - Ok(()) + .fn_handler("/favicon.ico", Method::Get, |request| { + request + .into_ok_response()? + .write(include_bytes!("favicon.ico"))?; + anyhow::Ok(()) }) .unwrap(); server } + + +type AnyhowHandler = + fn(&mut Request<&mut EspHttpConnection>) -> Result, anyhow::Error>; +fn handle_error_to500( + mut request: Request<&mut EspHttpConnection>, + chain: AnyhowHandler, +) -> Result<(), anyhow::Error> { + let error = chain(&mut request); + match error { + Ok(answer) => match answer { + Some(json) => { + let mut response = request.into_ok_response()?; + response.write(json.as_bytes())?; + response.flush()?; + } + None => {} + }, + Err(err) => { + let error_text = err.to_string(); + println!("error handling process {}", error_text); + request + .into_status_response(500)? + .write(error_text.as_bytes())?; + } + } + return anyhow::Ok(()); +} \ No newline at end of file diff --git a/rust/src_webpack/src/form.ts b/rust/src_webpack/src/form.ts index 908e6aa..a809811 100644 --- a/rust/src_webpack/src/form.ts +++ b/rust/src_webpack/src/form.ts @@ -35,6 +35,17 @@ interface TestPump{ pump: number } +interface SetTime{ + time: string +} + +interface GetData{ + rtc: string, + native: string, + moisture_a: [number], + moisture_b: [number], +} + let plants = document.getElementById("plants") as HTMLInputElement; let scanWifiBtn = document.getElementById("scan") as HTMLButtonElement; @@ -42,6 +53,56 @@ if(scanWifiBtn){ scanWifiBtn.onclick = scanWifi; } +let esp_time = document.getElementById("esp_time") as HTMLDivElement; +let rtc_time = document.getElementById("rtc_time") as HTMLDivElement; +let browser_time = document.getElementById("browser_time") as HTMLDivElement; +let sync = document.getElementById("time_upload") as HTMLButtonElement; +sync.onclick = setTime + +function setTime(){ + var value: SetTime = { + time : new Date().toISOString() + } + var pretty = JSON.stringify(value, undefined, 1); + fetch("/time", { + method :"POST", + body: pretty + }) +} + +function updateTime(){ + fetch("/data") + .then(response => response.json()) + .then(json => json as GetData) + .then(time => { + esp_time.innerText = time.native; + rtc_time.innerText = time.rtc; + var date = new Date(); + browser_time.innerText = date.toISOString(); + + + time.moisture_a.forEach((a, index) => { + var id = "plant_" + index + "_moisture_a"; + console.log("id is " + id + "index is " + index) + var target = document.getElementById(id) as HTMLDivElement; + target.innerText = a+""; + }) + time.moisture_b.forEach((b, index) => { + var id = "plant_" + index + "_moisture_b"; + var target = document.getElementById(id) as HTMLDivElement; + target.innerText = b+""; + }) + + setTimeout(updateTime,1000); + }) + .catch(error => { + console.log(error); + setTimeout(updateTime,10000); + }); +} +setTimeout(updateTime,1000); + + export function scanWifi(){ var scanButton = (document.getElementById("scan") as HTMLButtonElement); scanButton.disabled = true; @@ -154,6 +215,16 @@ let fromWrapper = (() => { }; holder.appendChild(testButton); + let moisture_a = document.createElement("div"); + moisture_a.innerText = "N/A"; + moisture_a.id = "plant_" + i + "_moisture_a"; + holder.appendChild(moisture_a); + + let moisture_b = document.createElement("div"); + moisture_b.innerText = "N/A"; + moisture_b.id = "plant_" + i + "_moisture_b"; + holder.appendChild(moisture_b); + let br = document.createElement("br"); holder.appendChild(br);