//offer ota and config mode use std::{ str::from_utf8, sync::{atomic::AtomicBool, Arc}, }; use crate::{ espota::OtaUpdate, get_version, log::LogMessage, map_range_moisture, plant_hal::{FileInfo, PLANT_COUNT}, BOARD_ACCESS }; use anyhow::bail; use chrono::DateTime; use esp_idf_sys::{esp_set_time_from_rtc, settimeofday, timeval, vTaskDelay}; use core::result::Result::Ok; use embedded_svc::http::Method; use esp_idf_hal::delay::Delay; use esp_idf_svc::http::server::{Configuration, EspHttpConnection, EspHttpServer, Request}; use heapless::String; use serde::{Deserialize, Serialize}; use url::Url; use crate::config::PlantControllerConfig; #[derive(Serialize, Debug)] struct SSIDList<'a> { ssids: Vec<&'a String<32>>, } #[derive(Serialize, Debug)] struct LoadData<'a> { rtc: &'a str, native: &'a str, } #[derive(Serialize, Debug)] struct Moistures { 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, } #[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct WebBackupHeader{ timestamp: std::string::String, size: usize } fn write_time( request: &mut Request<&mut EspHttpConnection>, ) -> Result, anyhow::Error> { 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(); let now = timeval { tv_sec: parsed.to_utc().timestamp(), tv_usec: 0 }; unsafe { settimeofday(&now, core::ptr::null_mut()) }; board.set_rtc_time(&parsed.to_utc())?; anyhow::Ok(None) } fn get_live_moisture( _request: &mut Request<&mut EspHttpConnection>, ) -> Result, anyhow::Error> { let mut board = BOARD_ACCESS.lock().unwrap(); let mut a: Vec = Vec::new(); let mut b: Vec = Vec::new(); for plant in 0..8 { 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(_) => { a.push(200); } } let b_pct = map_range_moisture(b_hz as f32); match b_pct { Ok(result) => { b.push(result); } Err(_) => { b.push(200); } } } let data = Moistures { moisture_a: a, moisture_b: b, }; let json = serde_json::to_string(&data)?; anyhow::Ok(Some(json)) } 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 data = LoadData { rtc: rtc.as_str(), native: native.as_str(), }; 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(&PlantControllerConfig::default())?, }; 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 backup_info( _request: &mut Request<&mut EspHttpConnection>, ) -> Result, anyhow::Error> { let mut board = BOARD_ACCESS.lock().unwrap(); let header = board.get_backup_info(); let json = match header { Ok(h) => { let timestamp = DateTime::from_timestamp_millis(h.timestamp).unwrap(); let wbh = WebBackupHeader{ timestamp: timestamp.to_rfc3339(), size: h.size, }; serde_json::to_string(&wbh)? }, Err(_) => "{\"error\":\"Header could not be parsed\"".to_owned() }; anyhow::Ok(Some(json)) } fn set_config( request: &mut Request<&mut EspHttpConnection>, ) -> Result, anyhow::Error> { 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> { let mut board = BOARD_ACCESS.lock().unwrap(); let battery_state = board.get_battery_state(); let battery_json = serde_json::to_string(&battery_state)?; anyhow::Ok(Some(battery_json)) } fn get_log( _request: &mut Request<&mut EspHttpConnection>, ) -> Result, anyhow::Error> { let output = crate::log::get_log(); anyhow::Ok(Some(serde_json::to_string(&output)?)) } fn get_log_localization_config() -> Result { anyhow::Ok(serde_json::to_string(&LogMessage::to_log_localisation_config())?) } fn get_version_web( _request: &mut Request<&mut EspHttpConnection>, ) -> Result, anyhow::Error> { anyhow::Ok(Some(serde_json::to_string(&get_version())?)) } fn pump_test( request: &mut Request<&mut EspHttpConnection>, ) -> Result, anyhow::Error> { 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) } 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 list_files( request: &mut Request<&mut EspHttpConnection>, ) -> Result, anyhow::Error> { let filename = query_param(request.uri(), "filename").unwrap_or_default(); let board = BOARD_ACCESS.lock().unwrap(); let result = board.list_files(&filename); let file_list_json = serde_json::to_string(&result)?; return anyhow::Ok(Some(file_list_json)); } fn ota( request: &mut Request<&mut EspHttpConnection>, ) -> Result, anyhow::Error> { let mut board = BOARD_ACCESS.lock().unwrap(); 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; let mut lastiter = 0; loop { let read = request.read(&mut buffer)?; total_read += read; println!("received {read} bytes ota {total_read}"); let to_write = &buffer[0..read]; let iter = (total_read/1024)%8; if iter != lastiter { for i in 0..PLANT_COUNT { board.fault(i, iter==i); } lastiter = iter; } 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"); board.set_restart_to_conf(true); drop(board); finalizer.set_as_boot_partition()?; finalizer.restart(); } fn flash_bq(filename: &str, dryrun: bool) -> anyhow::Result<()> { let mut board = BOARD_ACCESS.lock().unwrap(); let mut toggle = true; let delay = Delay::new(1); let file_handle = board.get_file_handle(filename, false)?; let mut reader = std::io::BufRead::lines(std::io::BufReader::with_capacity(512, file_handle)); let mut line = 0; loop { board.general_fault(toggle); toggle = !toggle; delay.delay_us(2); line += 1; match reader.next() { Some(next) => { let input = next?; println!("flashing bq34z100 dryrun:{dryrun} line {line} payload: {input}"); match board.flash_bq34_z100(&input, dryrun) { Ok(_) => { println!("ok") } Err(err) => { bail!( "Error flashing bq34z100 in dryrun: {dryrun} line: {line} error: {err}" ) } } } None => break, } } println!("Finished flashing file {line} lines processed"); board.general_fault(false); return anyhow::Ok(()); } fn query_param(uri: &str, param_name: &str) -> Option { println!("{uri} get {param_name}"); let parsed = Url::parse(&format!("http://127.0.0.1/{uri}")).unwrap(); let value = parsed.query_pairs().filter(|it| it.0 == param_name).next(); match value { Some(found) => { return Some(found.1.into_owned()); } None => return None, } } pub fn httpd(reboot_now: Arc) -> Box> { let server_config = Configuration { stack_size: 32768, ..Default::default() }; let mut server: Box> = Box::new(EspHttpServer::new(&server_config).unwrap()); server .fn_handler("/version", Method::Get, |request| { handle_error_to500(request, get_version_web) }) .unwrap(); server .fn_handler("/log", Method::Get, |request| { handle_error_to500(request, get_log) }) .unwrap(); server.fn_handler("/log_localization", Method::Get, |request| { cors_response(request, 200, &get_log_localization_config().unwrap()) }) .unwrap(); server .fn_handler("/battery", Method::Get, |request| { handle_error_to500(request, get_battery_state) }) .unwrap(); server .fn_handler("/time", Method::Get, |request| { handle_error_to500(request, get_data) }) .unwrap(); server .fn_handler("/moisture", Method::Get, |request| { handle_error_to500(request, get_live_moisture) }) .unwrap(); server .fn_handler("/time", Method::Post, |request| { handle_error_to500(request, write_time) }) .unwrap(); server .fn_handler("/pumptest", Method::Post, |request| { handle_error_to500(request, pump_test) }) .unwrap(); server .fn_handler("/boardtest", Method::Post, move |_| { BOARD_ACCESS.lock().unwrap().test() }) .unwrap(); server .fn_handler("/wifiscan", Method::Post, move |request| { handle_error_to500(request, wifi_scan) }) .unwrap(); server .fn_handler("/ota", Method::Post, |request| { handle_error_to500(request, ota) }) .unwrap(); server .fn_handler("/ota", Method::Options, |request| { cors_response(request, 200, "") }) .unwrap(); server .fn_handler("/get_config", Method::Get, move |request| { 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("/backup_info", Method::Get, move |request| { handle_error_to500(request, backup_info) }) .unwrap(); server .fn_handler("/files", Method::Get, move |request| { 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(); unsafe { vTaskDelay(1) }; 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(); let file_handle = BOARD_ACCESS .lock() .unwrap() .get_file_handle(&filename, false); match file_handle { Ok(mut file_handle) => { let headers = [("Access-Control-Allow-Origin", "*")]; let mut response = request.into_response(200, None, &headers)?; const BUFFER_SIZE: usize = 512; let mut buffer: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; let mut total_read: usize = 0; loop { unsafe { vTaskDelay(1) }; let read = std::io::Read::read(&mut file_handle, &mut buffer)?; total_read += read; let to_write = &buffer[0..read]; response.write(to_write)?; if read == 0 { break; } } println!("wrote {total_read} for file {filename}"); drop(file_handle); response.flush()?; } Err(err) => { //todo set headers here for filename to be error let error_text = err.to_string(); println!("error handling get file {}", error_text); cors_response(request, 500, &error_text)?; } } anyhow::Ok(()) }) .unwrap(); server .fn_handler("/file", Method::Post, move |mut request| { let filename = query_param(request.uri(), "filename").unwrap(); let lock = BOARD_ACCESS .lock() .unwrap(); let file_handle = lock.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]; let mut total_read: usize = 0; let mut lastiter = 0; loop { let iter = (total_read/1024)%8; if iter != lastiter { for i in 0..PLANT_COUNT { lock.fault(i, iter==i); } lastiter = iter; } let read = request.read(&mut buffer)?; total_read += read; let to_write = &buffer[0..read]; std::io::Write::write(&mut file_handle, to_write)?; if read == 0 { break; } } cors_response(request, 200, &format!("saved {total_read} bytes"))?; } Err(err) => { //todo set headers here for filename to be error let error_text = err.to_string(); println!("error handling get file {}", error_text); cors_response(request, 500, &error_text)?; } } drop(lock); anyhow::Ok(()) }) .unwrap(); server .fn_handler("/file", Method::Delete, move |request| { let filename = query_param(request.uri(), "filename").unwrap(); let copy = filename.clone(); let board = BOARD_ACCESS.lock().unwrap(); match board.delete_file(&filename) { Ok(_) => { let info = format!("Deleted file {copy}"); cors_response(request, 200, &info)?; } Err(err) => { let info = format!("Could not delete file {copy} {err:?}"); cors_response(request, 400, &info)?; } } anyhow::Ok(()) }) .unwrap(); server .fn_handler("/file", Method::Options, |request| { cors_response(request, 200, "") }) .unwrap(); server .fn_handler("/flashbattery", Method::Post, move |request| { let filename = query_param(request.uri(),"filename").unwrap(); let dryrun = true; match flash_bq(&filename, false) { Ok(_) => { if !dryrun { match flash_bq(&filename, true) { Ok(_) => { cors_response(request, 200, "Sucessfully flashed bq34z100")?; }, Err(err) => { let info = format!("Could not flash bq34z100, could be bricked now! {filename} {err:?}"); cors_response(request, 500, &info)?; }, } } else { cors_response(request, 200, "Sucessfully processed bq34z100")?; } }, Err(err) => { let info = format!("Could not process firmware file for, bq34z100, refusing to flash! {filename} {err:?}"); cors_response(request, 500, &info)?; }, }; anyhow::Ok(()) }) .unwrap(); unsafe { vTaskDelay(1) }; server .fn_handler("/", Method::Get, move |request| { let mut response = request.into_ok_response()?; response.write(include_bytes!("index.html"))?; anyhow::Ok(()) }) .unwrap(); server .fn_handler("/favicon.ico", Method::Get, |request| { request .into_ok_response()? .write(include_bytes!("favicon.ico"))?; anyhow::Ok(()) }) .unwrap(); server .fn_handler("/bundle.js", Method::Get, |request| { request .into_ok_response()? .write(include_bytes!("bundle.js"))?; anyhow::Ok(()) }) .unwrap(); server } fn cors_response( request: Request<&mut EspHttpConnection>, status: u16, body: &str, ) -> Result<(), anyhow::Error> { let headers = [ ("Access-Control-Allow-Origin", "*"), ("Access-Control-Allow-Headers", "*"), ("Access-Control-Allow-Methods", "*"), ]; let mut response = request.into_response(status, None, &headers)?; response.write(body.as_bytes())?; response.flush()?; return anyhow::Ok(()); } 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) => { cors_response(request, 200, &json)?; } None => { cors_response(request, 200, "")?; } }, Err(err) => { let error_text = err.to_string(); println!("error handling process {}", error_text); cors_response(request, 500, &error_text)?; } } 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) }