split webserver into submodules
This commit is contained in:
		
							
								
								
									
										4
									
								
								rust/.idea/dictionaries/project.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								rust/.idea/dictionaries/project.xml
									
									
									
										generated
									
									
									
								
							| @@ -1,14 +1,18 @@ | ||||
| <component name="ProjectDictionaryState"> | ||||
|   <dictionary name="project"> | ||||
|     <words> | ||||
|       <w>boardtest</w> | ||||
|       <w>buildtime</w> | ||||
|       <w>deepsleep</w> | ||||
|       <w>githash</w> | ||||
|       <w>lamptest</w> | ||||
|       <w>lightstate</w> | ||||
|       <w>mppt</w> | ||||
|       <w>plantstate</w> | ||||
|       <w>pumptest</w> | ||||
|       <w>sntp</w> | ||||
|       <w>vergen</w> | ||||
|       <w>wifiscan</w> | ||||
|     </words> | ||||
|   </dictionary> | ||||
| </component> | ||||
| @@ -9,6 +9,7 @@ | ||||
|     holding buffers for the duration of a data transfer." | ||||
| )] | ||||
|  | ||||
| //TODO insert version here and read it in other parts, also read this for the ota webview | ||||
| esp_bootloader_esp_idf::esp_app_desc!(); | ||||
| use esp_backtrace as _; | ||||
|  | ||||
|   | ||||
							
								
								
									
										191
									
								
								rust/src/webserver/backup_manager.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								rust/src/webserver/backup_manager.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,191 @@ | ||||
| use crate::fat_error::{FatError, FatResult}; | ||||
| use crate::hal::rtc::X25; | ||||
| 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 embedded_io_async::{Read, Write}; | ||||
| use log::info; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| #[derive(Serialize, Deserialize, PartialEq, Debug)] | ||||
| pub struct WebBackupHeader { | ||||
|     timestamp: String, | ||||
|     size: u16, | ||||
| } | ||||
| pub(crate) async fn get_backup_config<T, const N: usize>( | ||||
|     conn: &mut Connection<'_, T, { N }>, | ||||
| ) -> FatResult<Option<u32>> | ||||
| 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?; | ||||
|  | ||||
|         // 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 {} got {}", | ||||
|                             expected_crc, actual_crc | ||||
|                         ) | ||||
|                         .as_str(), | ||||
|                     ), | ||||
|                     &[], | ||||
|                 ) | ||||
|                 .await?; | ||||
|                 return Ok(Some(409)); | ||||
|             } | ||||
|             break; | ||||
|         } | ||||
|         chunk += 1; | ||||
|     } | ||||
|     // Second pass: stream data | ||||
|     conn.initiate_response( | ||||
|         200, | ||||
|         Some("OK"), | ||||
|         &[ | ||||
|             ("Access-Control-Allow-Origin", "*"), | ||||
|             ("Access-Control-Allow-Headers", "*"), | ||||
|             ("Access-Control-Allow-Methods", "*"), | ||||
|         ], | ||||
|     ) | ||||
|     .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; | ||||
|     Ok(Some(200)) | ||||
| } | ||||
|  | ||||
| pub(crate) async fn backup_config<T, const N: usize>( | ||||
|     conn: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> | ||||
| where | ||||
|     T: Read + Write, | ||||
| { | ||||
|     let mut offset = 0_usize; | ||||
|     let mut buf = [0_u8; 32]; | ||||
|  | ||||
|     let mut checksum = crate::hal::rtc::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 = counter + 1; | ||||
|             board | ||||
|                 .board_hal | ||||
|                 .get_rtc_module() | ||||
|                 .backup_config(offset, &buf[0..to_write]) | ||||
|                 .await?; | ||||
|             checksum.update(&buf[0..to_write]); | ||||
|         } | ||||
|         offset = offset + to_write; | ||||
|     } | ||||
|  | ||||
|     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; | ||||
|     conn.initiate_response( | ||||
|         200, | ||||
|         Some("OK"), | ||||
|         &[ | ||||
|             ("Access-Control-Allow-Origin", "*"), | ||||
|             ("Access-Control-Allow-Headers", "*"), | ||||
|             ("Access-Control-Allow-Methods", "*"), | ||||
|         ], | ||||
|     ) | ||||
|     .await?; | ||||
|     Ok(Some("saved".to_owned())) | ||||
| } | ||||
|  | ||||
| pub(crate) async fn backup_info<T, const N: usize>( | ||||
|     _request: &mut Connection<'_, T, N>, | ||||
| ) -> Result<Option<String>, FatError> | ||||
| 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 { | ||||
|         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(err) => { | ||||
|             let wbh = WebBackupHeader { | ||||
|                 timestamp: err.to_string(), | ||||
|                 size: 0, | ||||
|             }; | ||||
|             serde_json::to_string(&wbh)? | ||||
|         } | ||||
|     }; | ||||
|     Ok(Some(json)) | ||||
| } | ||||
							
								
								
									
										160
									
								
								rust/src/webserver/file_manager.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								rust/src/webserver/file_manager.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,160 @@ | ||||
| use crate::fat_error::{FatError, FatResult}; | ||||
| use crate::BOARD_ACCESS; | ||||
| use alloc::borrow::ToOwned; | ||||
| use alloc::format; | ||||
| use alloc::string::String; | ||||
| use edge_http::io::server::Connection; | ||||
| use edge_http::Method; | ||||
| use embedded_io_async::{Read, Write}; | ||||
| use log::info; | ||||
|  | ||||
| pub(crate) async fn list_files<T, const N: usize>( | ||||
|     _request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> { | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     let result = board.board_hal.get_esp().list_files().await?; | ||||
|     let file_list_json = serde_json::to_string(&result)?; | ||||
|     Ok(Some(file_list_json)) | ||||
| } | ||||
| pub(crate) async fn file_operations<T, const N: usize>( | ||||
|     conn: &mut Connection<'_, T, { N }>, | ||||
|     method: Method, | ||||
|     path: &&str, | ||||
|     prefix: &&str, | ||||
| ) -> Result<Option<u32>, FatError> | ||||
| where | ||||
|     T: Read + Write, | ||||
| { | ||||
|     let filename = &path[prefix.len()..]; | ||||
|     info!("file request for {} with method {}", filename, method); | ||||
|     Ok(match method { | ||||
|         Method::Delete => { | ||||
|             let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|             board | ||||
|                 .board_hal | ||||
|                 .get_esp() | ||||
|                 .delete_file(filename.to_owned()) | ||||
|                 .await?; | ||||
|             conn.initiate_response( | ||||
|                 200, | ||||
|                 Some("OK"), | ||||
|                 &[ | ||||
|                     ("Access-Control-Allow-Origin", "*"), | ||||
|                     ("Access-Control-Allow-Headers", "*"), | ||||
|                     ("Access-Control-Allow-Methods", "*"), | ||||
|                 ], | ||||
|             ) | ||||
|             .await?; | ||||
|             Some(200) | ||||
|         } | ||||
|         Method::Get => { | ||||
|             let disp = format!("attachment; filename=\"{filename}\""); | ||||
|             let size = { | ||||
|                 let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|                 board | ||||
|                     .board_hal | ||||
|                     .get_esp() | ||||
|                     .get_size(filename.to_owned()) | ||||
|                     .await? | ||||
|             }; | ||||
|  | ||||
|             conn.initiate_response( | ||||
|                 200, | ||||
|                 Some("OK"), | ||||
|                 &[ | ||||
|                     ("Content-Type", "application/octet-stream"), | ||||
|                     ("Content-Disposition", disp.as_str()), | ||||
|                     ("Content-Length", &format!("{}", size)), | ||||
|                     ("Access-Control-Allow-Origin", "*"), | ||||
|                     ("Access-Control-Allow-Headers", "*"), | ||||
|                     ("Access-Control-Allow-Methods", "*"), | ||||
|                 ], | ||||
|             ) | ||||
|             .await?; | ||||
|  | ||||
|             let mut chunk = 0; | ||||
|             loop { | ||||
|                 let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|                 board.board_hal.progress(chunk as u32).await; | ||||
|                 let read_chunk = board | ||||
|                     .board_hal | ||||
|                     .get_esp() | ||||
|                     .get_file(filename.to_owned(), chunk) | ||||
|                     .await?; | ||||
|                 let length = read_chunk.1; | ||||
|                 if length == 0 { | ||||
|                     info!("file request for {} finished", filename); | ||||
|                     break; | ||||
|                 } | ||||
|                 let data = &read_chunk.0[0..length]; | ||||
|                 conn.write_all(data).await?; | ||||
|                 if length < read_chunk.0.len() { | ||||
|                     info!("file request for {} finished", filename); | ||||
|                     break; | ||||
|                 } | ||||
|                 chunk = chunk + 1; | ||||
|             } | ||||
|             BOARD_ACCESS | ||||
|                 .get() | ||||
|                 .await | ||||
|                 .lock() | ||||
|                 .await | ||||
|                 .board_hal | ||||
|                 .clear_progress() | ||||
|                 .await; | ||||
|             Some(200) | ||||
|         } | ||||
|         Method::Post => { | ||||
|             { | ||||
|                 let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|                 //ensure file is deleted, otherwise we would need to truncate the file which will not work with streaming | ||||
|                 let _ = board | ||||
|                     .board_hal | ||||
|                     .get_esp() | ||||
|                     .delete_file(filename.to_owned()) | ||||
|                     .await; | ||||
|             } | ||||
|  | ||||
|             let mut offset = 0_usize; | ||||
|             let mut chunk = 0; | ||||
|             loop { | ||||
|                 let mut buf = [0_u8; 1024]; | ||||
|                 let to_write = conn.read(&mut buf).await?; | ||||
|                 if to_write == 0 { | ||||
|                     info!("file request for {} finished", filename); | ||||
|                     break; | ||||
|                 } else { | ||||
|                     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|                     board.board_hal.progress(chunk as u32).await; | ||||
|                     board | ||||
|                         .board_hal | ||||
|                         .get_esp() | ||||
|                         .write_file(filename.to_owned(), offset as u32, &buf[0..to_write]) | ||||
|                         .await?; | ||||
|                 } | ||||
|                 offset = offset + to_write; | ||||
|                 chunk = chunk + 1; | ||||
|             } | ||||
|             BOARD_ACCESS | ||||
|                 .get() | ||||
|                 .await | ||||
|                 .lock() | ||||
|                 .await | ||||
|                 .board_hal | ||||
|                 .clear_progress() | ||||
|                 .await; | ||||
|             conn.initiate_response( | ||||
|                 200, | ||||
|                 Some("OK"), | ||||
|                 &[ | ||||
|                     ("Access-Control-Allow-Origin", "*"), | ||||
|                     ("Access-Control-Allow-Headers", "*"), | ||||
|                     ("Access-Control-Allow-Methods", "*"), | ||||
|                 ], | ||||
|             ) | ||||
|             .await?; | ||||
|             Some(200) | ||||
|         } | ||||
|         _ => None, | ||||
|     }) | ||||
| } | ||||
							
								
								
									
										168
									
								
								rust/src/webserver/get_json.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								rust/src/webserver/get_json.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,168 @@ | ||||
| use crate::fat_error::{FatError, FatResult}; | ||||
| use crate::hal::{esp_time, PLANT_COUNT}; | ||||
| use crate::log::{LogMessage, LOG_ACCESS}; | ||||
| use crate::plant_state::{MoistureSensorState, PlantState}; | ||||
| use crate::tank::determine_tank_state; | ||||
| use crate::{get_version, BOARD_ACCESS}; | ||||
| use alloc::format; | ||||
| use alloc::string::{String, ToString}; | ||||
| use alloc::vec::Vec; | ||||
| use chrono_tz::Tz; | ||||
| use core::str::FromStr; | ||||
| use edge_http::io::server::Connection; | ||||
| use embedded_io_async::{Read, Write}; | ||||
| use log::info; | ||||
| use serde::Serialize; | ||||
|  | ||||
| #[derive(Serialize, Debug)] | ||||
| struct LoadData<'a> { | ||||
|     rtc: &'a str, | ||||
|     native: &'a str, | ||||
| } | ||||
| #[derive(Serialize, Debug)] | ||||
| struct Moistures { | ||||
|     moisture_a: Vec<String>, | ||||
|     moisture_b: Vec<String>, | ||||
| } | ||||
| #[derive(Serialize, Debug)] | ||||
| struct SolarState { | ||||
|     mppt_voltage: f32, | ||||
|     mppt_current: f32, | ||||
|     is_day: bool, | ||||
| } | ||||
| pub(crate) async fn get_live_moisture<T, const N: usize>( | ||||
|     _request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> | ||||
| where | ||||
|     T: Read + Write, | ||||
| { | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     let mut plant_state = Vec::new(); | ||||
|     for i in 0..PLANT_COUNT { | ||||
|         plant_state.push(PlantState::read_hardware_state(i, &mut board).await); | ||||
|     } | ||||
|     let a = Vec::from_iter(plant_state.iter().map(|s| match &s.sensor_a { | ||||
|         MoistureSensorState::Disabled => "disabled".to_string(), | ||||
|         MoistureSensorState::MoistureValue { | ||||
|             raw_hz, | ||||
|             moisture_percent, | ||||
|         } => { | ||||
|             format!("{moisture_percent:.2}% {raw_hz}hz",) | ||||
|         } | ||||
|         MoistureSensorState::SensorError(err) => format!("{err:?}"), | ||||
|     })); | ||||
|     let b = Vec::from_iter(plant_state.iter().map(|s| match &s.sensor_b { | ||||
|         MoistureSensorState::Disabled => "disabled".to_string(), | ||||
|         MoistureSensorState::MoistureValue { | ||||
|             raw_hz, | ||||
|             moisture_percent, | ||||
|         } => { | ||||
|             format!("{moisture_percent:.2}% {raw_hz}hz",) | ||||
|         } | ||||
|         MoistureSensorState::SensorError(err) => format!("{err:?}"), | ||||
|     })); | ||||
|  | ||||
|     let data = Moistures { | ||||
|         moisture_a: a, | ||||
|         moisture_b: b, | ||||
|     }; | ||||
|     let json = serde_json::to_string(&data)?; | ||||
|  | ||||
|     Ok(Some(json)) | ||||
| } | ||||
|  | ||||
| pub(crate) async fn tank_info<T, const N: usize>( | ||||
|     _request: &mut Connection<'_, T, N>, | ||||
| ) -> Result<Option<String>, FatError> | ||||
| where | ||||
|     T: Read + Write, | ||||
| { | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     let tank_state = determine_tank_state(&mut board).await; | ||||
|     //should be multisampled | ||||
|     let sensor = board.board_hal.get_tank_sensor()?; | ||||
|  | ||||
|     let water_temp: FatResult<f32> = sensor.water_temperature_c().await; | ||||
|     Ok(Some(serde_json::to_string(&tank_state.as_mqtt_info( | ||||
|         &board.board_hal.get_config().tank, | ||||
|         &water_temp, | ||||
|     ))?)) | ||||
| } | ||||
|  | ||||
| pub(crate) async fn get_timezones() -> FatResult<Option<String>> { | ||||
|     // Get all timezones compiled into the binary from chrono-tz | ||||
|     let timezones: Vec<&'static str> = chrono_tz::TZ_VARIANTS.iter().map(|tz| tz.name()).collect(); | ||||
|     let json = serde_json::to_string(&timezones)?; | ||||
|     Ok(Some(json)) | ||||
| } | ||||
|  | ||||
| pub(crate) async fn get_solar_state<T, const N: usize>( | ||||
|     _request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> { | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     let state = SolarState { | ||||
|         mppt_voltage: board.board_hal.get_mptt_voltage().await?.as_millivolts() as f32, | ||||
|         mppt_current: board.board_hal.get_mptt_current().await?.as_milliamperes() as f32, | ||||
|         is_day: board.board_hal.is_day(), | ||||
|     }; | ||||
|     Ok(Some(serde_json::to_string(&state)?)) | ||||
| } | ||||
|  | ||||
| pub(crate) async fn get_version_web<T, const N: usize>( | ||||
|     _request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> { | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     Ok(Some(serde_json::to_string(&get_version(&mut board).await)?)) | ||||
| } | ||||
|  | ||||
| pub(crate) async fn get_config<T, const N: usize>( | ||||
|     _request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> { | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     let json = serde_json::to_string(&board.board_hal.get_config())?; | ||||
|     Ok(Some(json)) | ||||
| } | ||||
|  | ||||
| pub(crate) async fn get_battery_state<T, const N: usize>( | ||||
|     _request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> { | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     let battery_state = board | ||||
|         .board_hal | ||||
|         .get_battery_monitor() | ||||
|         .get_battery_state() | ||||
|         .await?; | ||||
|     Ok(Some(serde_json::to_string(&battery_state)?)) | ||||
| } | ||||
|  | ||||
| pub(crate) async fn get_time<T, const N: usize>( | ||||
|     _request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> { | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     let conf = board.board_hal.get_config(); | ||||
|     let tz = Tz::from_str(conf.timezone.as_ref().unwrap().as_str()).unwrap(); | ||||
|     let native = esp_time().await.with_timezone(&tz).to_rfc3339(); | ||||
|  | ||||
|     let rtc = match board.board_hal.get_rtc_module().get_rtc_time().await { | ||||
|         Ok(time) => time.with_timezone(&tz).to_rfc3339(), | ||||
|         Err(err) => { | ||||
|             format!("Error getting time: {}", err) | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     let data = LoadData { | ||||
|         rtc: rtc.as_str(), | ||||
|         native: native.as_str(), | ||||
|     }; | ||||
|     let json = serde_json::to_string(&data)?; | ||||
|  | ||||
|     Ok(Some(json)) | ||||
| } | ||||
|  | ||||
| pub(crate) async fn get_log_localization_config<T, const N: usize>( | ||||
|     _request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> { | ||||
|     Ok(Some(serde_json::to_string( | ||||
|         &LogMessage::to_log_localisation_config(), | ||||
|     )?)) | ||||
| } | ||||
							
								
								
									
										36
									
								
								rust/src/webserver/get_log.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								rust/src/webserver/get_log.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| use crate::fat_error::FatResult; | ||||
| use crate::log::LOG_ACCESS; | ||||
| use edge_http::io::server::Connection; | ||||
| use embedded_io_async::{Read, Write}; | ||||
|  | ||||
| pub(crate) async fn get_log<T, const N: usize>( | ||||
|     conn: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<u32>> | ||||
| where | ||||
|     T: Read + Write, | ||||
| { | ||||
|     let log = LOG_ACCESS.lock().await.get(); | ||||
|     conn.initiate_response( | ||||
|         200, | ||||
|         Some("OK"), | ||||
|         &[ | ||||
|             ("Content-Type", "text/javascript"), | ||||
|             ("Access-Control-Allow-Origin", "*"), | ||||
|             ("Access-Control-Allow-Headers", "*"), | ||||
|             ("Access-Control-Allow-Methods", "*"), | ||||
|         ], | ||||
|     ) | ||||
|     .await?; | ||||
|     conn.write_all("[".as_bytes()).await?; | ||||
|     let mut append = false; | ||||
|     for entry in log { | ||||
|         if append { | ||||
|             conn.write_all(",".as_bytes()).await?; | ||||
|         } | ||||
|         append = true; | ||||
|         let json = serde_json::to_string(&entry)?; | ||||
|         conn.write_all(json.as_bytes()).await?; | ||||
|     } | ||||
|     conn.write_all("]".as_bytes()).await?; | ||||
|     Ok(Some(200)) | ||||
| } | ||||
							
								
								
									
										50
									
								
								rust/src/webserver/get_static.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								rust/src/webserver/get_static.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| use crate::fat_error::FatError; | ||||
| use edge_http::io::server::Connection; | ||||
| use embedded_io_async::{Read, Write}; | ||||
|  | ||||
| pub(crate) async fn serve_favicon<T, const N: usize>( | ||||
|     conn: &mut Connection<'_, T, { N }>, | ||||
| ) -> Result<Option<u32>, FatError> | ||||
| where | ||||
|     T: Read + Write, | ||||
| { | ||||
|     conn.initiate_response(200, Some("OK"), &[("Content-Type", "image/x-icon")]) | ||||
|         .await?; | ||||
|     conn.write_all(include_bytes!("favicon.ico")).await?; | ||||
|     Ok(Some(200)) | ||||
| } | ||||
|  | ||||
| pub(crate) async fn serve_index<T, const N: usize>( | ||||
|     conn: &mut Connection<'_, T, { N }>, | ||||
| ) -> Result<Option<u32>, FatError> | ||||
| where | ||||
|     T: Read + Write, | ||||
| { | ||||
|     conn.initiate_response( | ||||
|         200, | ||||
|         Some("OK"), | ||||
|         &[("Content-Type", "text/html"), ("Content-Encoding", "gzip")], | ||||
|     ) | ||||
|     .await?; | ||||
|     conn.write_all(include_bytes!("index.html.gz")).await?; | ||||
|     Ok(Some(200)) | ||||
| } | ||||
|  | ||||
| pub(crate) async fn serve_bundle<T, const N: usize>( | ||||
|     conn: &mut Connection<'_, T, { N }>, | ||||
| ) -> Result<Option<u32>, FatError> | ||||
| where | ||||
|     T: Read + Write, | ||||
| { | ||||
|     conn.initiate_response( | ||||
|         200, | ||||
|         Some("OK"), | ||||
|         &[ | ||||
|             ("Content-Type", "text/javascript"), | ||||
|             ("Content-Encoding", "gzip"), | ||||
|         ], | ||||
|     ) | ||||
|     .await?; | ||||
|     conn.write_all(include_bytes!("bundle.js.gz")).await?; | ||||
|     Ok(Some(200)) | ||||
| } | ||||
| @@ -1,23 +1,32 @@ | ||||
| //offer ota and config mode | ||||
|  | ||||
| use crate::config::PlantControllerConfig; | ||||
| mod backup_manager; | ||||
| mod file_manager; | ||||
| mod get_json; | ||||
| mod get_log; | ||||
| mod get_static; | ||||
| mod post_json; | ||||
|  | ||||
| use crate::fat_error::{FatError, FatResult}; | ||||
| use crate::hal::rtc::X25; | ||||
| use crate::hal::{esp_set_time, esp_time}; | ||||
| use crate::log::LOG_ACCESS; | ||||
| use crate::tank::determine_tank_state; | ||||
| use crate::{bail, do_secure_pump, get_version, log::LogMessage, BOARD_ACCESS}; | ||||
| use crate::webserver::backup_manager::{backup_config, backup_info, get_backup_config}; | ||||
| use crate::webserver::file_manager::{file_operations, list_files}; | ||||
| use crate::webserver::get_json::{ | ||||
|     get_battery_state, get_config, get_live_moisture, get_log_localization_config, get_solar_state, | ||||
|     get_time, get_timezones, get_version_web, tank_info, | ||||
| }; | ||||
| use crate::webserver::get_log::get_log; | ||||
| use crate::webserver::get_static::{serve_bundle, serve_favicon, serve_index}; | ||||
| use crate::webserver::post_json::{ | ||||
|     board_test, night_lamp_test, pump_test, set_config, wifi_scan, write_time, | ||||
| }; | ||||
| use crate::{bail, BOARD_ACCESS}; | ||||
| use alloc::borrow::ToOwned; | ||||
| use alloc::format; | ||||
| use alloc::string::{String, ToString}; | ||||
| use alloc::sync::Arc; | ||||
| use alloc::vec::Vec; | ||||
| use chrono::DateTime; | ||||
| use chrono_tz::Tz; | ||||
| use core::fmt::{Debug, Display}; | ||||
| use core::net::{IpAddr, Ipv4Addr, SocketAddr}; | ||||
| use core::result::Result::Ok; | ||||
| use core::str::{from_utf8, FromStr}; | ||||
| use core::sync::atomic::{AtomicBool, Ordering}; | ||||
| use edge_http::io::server::{Connection, Handler, Server}; | ||||
| use edge_http::Method; | ||||
| @@ -26,97 +35,8 @@ use edge_nal_embassy::{Tcp, TcpBuffers}; | ||||
| use embassy_net::Stack; | ||||
| use embassy_time::Instant; | ||||
| use embedded_io_async::{Read, Write}; | ||||
| use esp_println::println; | ||||
| use log::info; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| #[derive(Serialize, Debug)] | ||||
| struct SSIDList { | ||||
|     ssids: Vec<String>, | ||||
| } | ||||
|  | ||||
| #[derive(Serialize, Debug)] | ||||
| struct LoadData<'a> { | ||||
|     rtc: &'a str, | ||||
|     native: &'a str, | ||||
| } | ||||
|  | ||||
| #[derive(Serialize, Debug)] | ||||
| struct Moistures { | ||||
|     moisture_a: Vec<String>, | ||||
|     moisture_b: Vec<String>, | ||||
| } | ||||
|  | ||||
| #[derive(Serialize, Debug)] | ||||
| struct SolarState { | ||||
|     mppt_voltage: f32, | ||||
|     mppt_current: f32, | ||||
|     is_day: bool, | ||||
| } | ||||
|  | ||||
| #[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: String, | ||||
|     size: u16, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| pub struct NightLampCommand { | ||||
|     active: bool, | ||||
| } | ||||
| // | ||||
| // | ||||
|  | ||||
| // | ||||
|  | ||||
| // | ||||
| // fn get_live_moisture( | ||||
| //     _request: &mut Request<&mut EspHttpConnection>, | ||||
| // ) -> Result<Option<std::string::String>, anyhow::Error> { | ||||
| //     let mut board = BOARD_ACCESS.lock().expect("Should never fail"); | ||||
| //     let plant_state = | ||||
| //         Vec::from_iter((0..PLANT_COUNT).map(|i| PlantState::read_hardware_state(i, &mut board))); | ||||
| //     let a = Vec::from_iter(plant_state.iter().map(|s| match &s.sensor_a { | ||||
| //         MoistureSensorState::Disabled => "disabled".to_string(), | ||||
| //         MoistureSensorState::MoistureValue { | ||||
| //             raw_hz, | ||||
| //             moisture_percent, | ||||
| //         } => { | ||||
| //             format!("{moisture_percent:.2}% {raw_hz}hz",) | ||||
| //         } | ||||
| //         MoistureSensorState::SensorError(err) => format!("{err:?}"), | ||||
| //     })); | ||||
| //     let b = Vec::from_iter(plant_state.iter().map(|s| match &s.sensor_b { | ||||
| //         MoistureSensorState::Disabled => "disabled".to_string(), | ||||
| //         MoistureSensorState::MoistureValue { | ||||
| //             raw_hz, | ||||
| //             moisture_percent, | ||||
| //         } => { | ||||
| //             format!("{moisture_percent:.2}% {raw_hz}hz",) | ||||
| //         } | ||||
| //         MoistureSensorState::SensorError(err) => format!("{err:?}"), | ||||
| //     })); | ||||
| // | ||||
| //     let data = Moistures { | ||||
| //         moisture_a: a, | ||||
| //         moisture_b: b, | ||||
| //     }; | ||||
| //     let json = serde_json::to_string(&data)?; | ||||
| // | ||||
| //     anyhow::Ok(Some(json)) | ||||
| // } | ||||
| // | ||||
| // | ||||
| // fn ota( | ||||
| //     request: &mut Request<&mut EspHttpConnection>, | ||||
| // ) -> Result<Option<std::string::String>, anyhow::Error> { | ||||
| @@ -164,11 +84,11 @@ pub struct NightLampCommand { | ||||
| // } | ||||
| // | ||||
|  | ||||
| struct HttpHandler { | ||||
| struct HTTPRequestRouter { | ||||
|     reboot_now: Arc<AtomicBool>, | ||||
| } | ||||
|  | ||||
| impl Handler for HttpHandler { | ||||
| impl Handler for HTTPRequestRouter { | ||||
|     type Error<E: Debug> = FatError; | ||||
|     async fn handle<'a, T, const N: usize>( | ||||
|         &self, | ||||
| @@ -186,159 +106,15 @@ impl Handler for HttpHandler { | ||||
|  | ||||
|         let prefix = "/file?filename="; | ||||
|         let status = if path.starts_with(prefix) { | ||||
|             let filename = &path[prefix.len()..]; | ||||
|             info!("file request for {} with method {}", filename, method); | ||||
|             match method { | ||||
|                 Method::Delete => { | ||||
|                     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|                     board | ||||
|                         .board_hal | ||||
|                         .get_esp() | ||||
|                         .delete_file(filename.to_owned()) | ||||
|                         .await?; | ||||
|                     Some(200) | ||||
|                 } | ||||
|                 Method::Get => { | ||||
|                     let disp = format!("attachment; filename=\"{filename}\""); | ||||
|                     let size = { | ||||
|                         let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|                         board | ||||
|                             .board_hal | ||||
|                             .get_esp() | ||||
|                             .get_size(filename.to_owned()) | ||||
|                             .await? | ||||
|                     }; | ||||
|  | ||||
|                     conn.initiate_response( | ||||
|                         200, | ||||
|                         Some("OK"), | ||||
|                         &[ | ||||
|                             ("Content-Type", "application/octet-stream"), | ||||
|                             ("Content-Disposition", disp.as_str()), | ||||
|                             ("Content-Length", &format!("{}", size)), | ||||
|                         ], | ||||
|                     ) | ||||
|                     .await?; | ||||
|  | ||||
|                     let mut chunk = 0; | ||||
|                     loop { | ||||
|                         let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|                         board.board_hal.progress(chunk as u32).await; | ||||
|                         let read_chunk = board | ||||
|                             .board_hal | ||||
|                             .get_esp() | ||||
|                             .get_file(filename.to_owned(), chunk) | ||||
|                             .await?; | ||||
|                         let length = read_chunk.1; | ||||
|                         if length == 0 { | ||||
|                             info!("file request for {} finished", filename); | ||||
|                             break; | ||||
|                         } | ||||
|                         let data = &read_chunk.0[0..length]; | ||||
|                         conn.write_all(data).await?; | ||||
|                         if length < read_chunk.0.len() { | ||||
|                             info!("file request for {} finished", filename); | ||||
|                             break; | ||||
|                         } | ||||
|                         chunk = chunk + 1; | ||||
|                     } | ||||
|                     BOARD_ACCESS | ||||
|                         .get() | ||||
|                         .await | ||||
|                         .lock() | ||||
|                         .await | ||||
|                         .board_hal | ||||
|                         .clear_progress() | ||||
|                         .await; | ||||
|                     Some(200) | ||||
|                 } | ||||
|                 Method::Post => { | ||||
|                     { | ||||
|                         let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|                         //ensure file is deleted, otherwise we would need to truncate the file which will not work with streaming | ||||
|                         let _ = board | ||||
|                             .board_hal | ||||
|                             .get_esp() | ||||
|                             .delete_file(filename.to_owned()) | ||||
|                             .await; | ||||
|                     } | ||||
|  | ||||
|                     let mut offset = 0_usize; | ||||
|                     let mut chunk = 0; | ||||
|                     loop { | ||||
|                         let mut buf = [0_u8; 1024]; | ||||
|                         let to_write = conn.read(&mut buf).await?; | ||||
|                         if to_write == 0 { | ||||
|                             info!("file request for {} finished", filename); | ||||
|                             break; | ||||
|                         } else { | ||||
|                             let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|                             board.board_hal.progress(chunk as u32).await; | ||||
|                             board | ||||
|                                 .board_hal | ||||
|                                 .get_esp() | ||||
|                                 .write_file(filename.to_owned(), offset as u32, &buf[0..to_write]) | ||||
|                                 .await?; | ||||
|                         } | ||||
|                         offset = offset + to_write; | ||||
|                         chunk = chunk + 1; | ||||
|                     } | ||||
|                     BOARD_ACCESS | ||||
|                         .get() | ||||
|                         .await | ||||
|                         .lock() | ||||
|                         .await | ||||
|                         .board_hal | ||||
|                         .clear_progress() | ||||
|                         .await; | ||||
|                     Some(200) | ||||
|                 } | ||||
|                 _ => None, | ||||
|             } | ||||
|             file_operations(conn, method, &path, &prefix).await? | ||||
|         } else { | ||||
|             match method { | ||||
|                 Method::Get => match path { | ||||
|                     "/favicon.ico" => { | ||||
|                         conn.initiate_response( | ||||
|                             200, | ||||
|                             Some("OK"), | ||||
|                             &[("Content-Type", "image/x-icon")], | ||||
|                         ) | ||||
|                         .await?; | ||||
|                         conn.write_all(include_bytes!("favicon.ico")).await?; | ||||
|                         Some(200) | ||||
|                     } | ||||
|                     "/" => { | ||||
|                         conn.initiate_response( | ||||
|                             200, | ||||
|                             Some("OK"), | ||||
|                             &[("Content-Type", "text/html"), ("Content-Encoding", "gzip")], | ||||
|                         ) | ||||
|                         .await?; | ||||
|                         conn.write_all(include_bytes!("index.html.gz")).await?; | ||||
|                         Some(200) | ||||
|                     } | ||||
|                     "/bundle.js" => { | ||||
|                         conn.initiate_response( | ||||
|                             200, | ||||
|                             Some("OK"), | ||||
|                             &[ | ||||
|                                 ("Content-Type", "text/javascript"), | ||||
|                                 ("Content-Encoding", "gzip"), | ||||
|                             ], | ||||
|                         ) | ||||
|                         .await?; | ||||
|                         conn.write_all(include_bytes!("bundle.js.gz")).await?; | ||||
|                         Some(200) | ||||
|                     } | ||||
|                     "/log" => { | ||||
|                         get_log(conn).await?; | ||||
|                         Some(200) | ||||
|                     } | ||||
|                     "/get_backup_config" => { | ||||
|                         get_backup_config(conn).await?; | ||||
|                         Some(200) | ||||
|                     } | ||||
|                     "/favicon.ico" => serve_favicon(conn).await?, | ||||
|                     "/" => serve_index(conn).await?, | ||||
|                     "/bundle.js" => serve_bundle(conn).await?, | ||||
|                     "/log" => get_log(conn).await?, | ||||
|                     "/get_backup_config" => get_backup_config(conn).await?, | ||||
|                     &_ => { | ||||
|                         let json = match path { | ||||
|                             "/version" => Some(get_version_web(conn).await), | ||||
| @@ -350,7 +126,8 @@ impl Handler for HttpHandler { | ||||
|                             "/log_localization" => Some(get_log_localization_config(conn).await), | ||||
|                             "/tank" => Some(tank_info(conn).await), | ||||
|                             "/backup_info" => Some(backup_info(conn).await), | ||||
|                             "/timezones" => Some(get_timezones(conn).await), | ||||
|                             "/timezones" => Some(get_timezones().await), | ||||
|                             "/moisture" => Some(get_live_moisture(conn).await), | ||||
|                             _ => None, | ||||
|                         }; | ||||
|                         match json { | ||||
| @@ -367,7 +144,7 @@ impl Handler for HttpHandler { | ||||
|                         "/backup_config" => Some(backup_config(conn).await), | ||||
|                         "/pumptest" => Some(pump_test(conn).await), | ||||
|                         "/lamptest" => Some(night_lamp_test(conn).await), | ||||
|                         "/boardtest" => Some(board_test(conn).await), | ||||
|                         "/boardtest" => Some(board_test().await), | ||||
|                         "/reboot" => { | ||||
|                             let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|                             board.board_hal.get_esp().set_restart_to_conf(true); | ||||
| @@ -407,265 +184,6 @@ impl Handler for HttpHandler { | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn get_timezones<T, const N: usize>( | ||||
|     request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> | ||||
| where | ||||
|     T: Read + Write, | ||||
| { | ||||
|     // Get all timezones using chrono-tz | ||||
|     let timezones: Vec<&'static str> = chrono_tz::TZ_VARIANTS.iter().map(|tz| tz.name()).collect(); | ||||
|     let json = serde_json::to_string(&timezones)?; | ||||
|     Ok(Some(json)) | ||||
| } | ||||
|  | ||||
| async fn board_test<T, const N: usize>( | ||||
|     request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> | ||||
| where | ||||
|     T: Read + Write, | ||||
| { | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     board.board_hal.test().await?; | ||||
|     Ok(None) | ||||
| } | ||||
|  | ||||
| async fn pump_test<T, const N: usize>( | ||||
|     request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> | ||||
| where | ||||
|     T: Read + Write, | ||||
| { | ||||
|     let actual_data = read_up_to_bytes_from_request(request, None).await?; | ||||
|     let pump_test: TestPump = serde_json::from_slice(&actual_data)?; | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|  | ||||
|     let config = &board.board_hal.get_config().plants[pump_test.pump].clone(); | ||||
|     let pump_result = do_secure_pump(&mut board, pump_test.pump, config, false).await; | ||||
|     //ensure it is disabled before unwrapping | ||||
|     board.board_hal.pump(pump_test.pump, false).await?; | ||||
|  | ||||
|     Ok(Some(serde_json::to_string(&pump_result?)?)) | ||||
| } | ||||
|  | ||||
| async fn night_lamp_test<T, const N: usize>( | ||||
|     request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> | ||||
| where | ||||
|     T: Read + Write, | ||||
| { | ||||
|     let actual_data = read_up_to_bytes_from_request(request, None).await?; | ||||
|     let light_command: NightLampCommand = serde_json::from_slice(&actual_data)?; | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     board.board_hal.light(light_command.active).await?; | ||||
|     Ok(None) | ||||
| } | ||||
|  | ||||
| async fn get_backup_config<T, const N: usize>( | ||||
|     conn: &mut Connection<'_, T, { N }>, | ||||
| ) -> Result<(), FatError> | ||||
| 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?; | ||||
|  | ||||
|         // 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 {} got {}", | ||||
|                             expected_crc, actual_crc | ||||
|                         ) | ||||
|                         .as_str(), | ||||
|                     ), | ||||
|                     &[], | ||||
|                 ) | ||||
|                 .await?; | ||||
|                 return Ok(()); | ||||
|             } | ||||
|             break; | ||||
|         } | ||||
|         chunk += 1; | ||||
|     } | ||||
|  | ||||
|     // Second pass: stream data | ||||
|     conn.initiate_response(200, Some("OK"), &[]).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; | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| async fn backup_config<T, const N: usize>( | ||||
|     conn: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> | ||||
| where | ||||
|     T: Read + Write, | ||||
| { | ||||
|     let mut offset = 0_usize; | ||||
|     let mut buf = [0_u8; 32]; | ||||
|  | ||||
|     let mut checksum = crate::hal::rtc::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 = counter + 1; | ||||
|             board | ||||
|                 .board_hal | ||||
|                 .get_rtc_module() | ||||
|                 .backup_config(offset, &buf[0..to_write]) | ||||
|                 .await?; | ||||
|             checksum.update(&buf[0..to_write]); | ||||
|         } | ||||
|         offset = offset + to_write; | ||||
|     } | ||||
|  | ||||
|     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; | ||||
|     Ok(Some("saved".to_owned())) | ||||
| } | ||||
|  | ||||
| async fn backup_info<T, const N: usize>( | ||||
|     _request: &mut Connection<'_, T, N>, | ||||
| ) -> Result<Option<String>, FatError> | ||||
| 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 { | ||||
|         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(err) => { | ||||
|             let wbh = WebBackupHeader { | ||||
|                 timestamp: err.to_string(), | ||||
|                 size: 0, | ||||
|             }; | ||||
|             serde_json::to_string(&wbh)? | ||||
|         } | ||||
|     }; | ||||
|     Ok(Some(json)) | ||||
| } | ||||
|  | ||||
| async fn tank_info<T, const N: usize>( | ||||
|     _request: &mut Connection<'_, T, N>, | ||||
| ) -> Result<Option<String>, FatError> | ||||
| where | ||||
|     T: Read + Write, | ||||
| { | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     let tank_state = determine_tank_state(&mut board).await; | ||||
|     //should be multisampled | ||||
|     let sensor = board.board_hal.get_tank_sensor()?; | ||||
|  | ||||
|     let water_temp: FatResult<f32> = sensor.water_temperature_c().await; | ||||
|     Ok(Some(serde_json::to_string(&tank_state.as_mqtt_info( | ||||
|         &board.board_hal.get_config().tank, | ||||
|         &water_temp, | ||||
|     ))?)) | ||||
| } | ||||
|  | ||||
| async fn write_time<T, const N: usize>( | ||||
|     request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> | ||||
| where | ||||
|     T: Read + Write, | ||||
| { | ||||
|     let actual_data = read_up_to_bytes_from_request(request, None).await?; | ||||
|     let time: SetTime = serde_json::from_slice(&actual_data)?; | ||||
|     let parsed = DateTime::parse_from_rfc3339(time.time).unwrap(); | ||||
|     esp_set_time(parsed).await?; | ||||
|     Ok(None) | ||||
| } | ||||
|  | ||||
| async fn set_config<T, const N: usize>( | ||||
|     request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> | ||||
| where | ||||
|     T: Read + Write, | ||||
| { | ||||
|     let all = read_up_to_bytes_from_request(request, Some(4096)).await?; | ||||
|     let length = all.len(); | ||||
|     let config: PlantControllerConfig = serde_json::from_slice(&all)?; | ||||
|  | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     board.board_hal.get_esp().save_config(all).await?; | ||||
|     log::info!("Wrote config config {:?} with size {}", config, length); | ||||
|     board.board_hal.set_config(config); | ||||
|     Ok(Some("saved".to_string())) | ||||
| } | ||||
|  | ||||
| async fn read_up_to_bytes_from_request<T, const N: usize>( | ||||
|     request: &mut Connection<'_, T, N>, | ||||
|     limit: Option<usize>, | ||||
| @@ -689,146 +207,27 @@ where | ||||
|         } | ||||
|         data_store.push(actual_data.to_owned()); | ||||
|     } | ||||
|     let allvec = data_store.concat(); | ||||
|     log::info!("Raw data {}", from_utf8(&allvec)?); | ||||
|     Ok(allvec) | ||||
| } | ||||
|  | ||||
| async fn wifi_scan<T, const N: usize>( | ||||
|     _request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> { | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     info!("start wifi scan"); | ||||
|     let mut ssids: Vec<String> = Vec::new(); | ||||
|     let scan_result = board.board_hal.get_esp().wifi_scan().await?; | ||||
|     scan_result | ||||
|         .iter() | ||||
|         .for_each(|s| ssids.push(s.ssid.to_string())); | ||||
|     let ssid_json = serde_json::to_string(&SSIDList { ssids })?; | ||||
|     info!("Sending ssid list {}", &ssid_json); | ||||
|     Ok(Some(ssid_json)) | ||||
| } | ||||
|  | ||||
| async fn get_log<T, const N: usize>(conn: &mut Connection<'_, T, N>) -> FatResult<()> | ||||
| where | ||||
|     T: Read + Write, | ||||
| { | ||||
|     let log = LOG_ACCESS.lock().await.get(); | ||||
|     conn.initiate_response(200, Some("OK"), &[("Content-Type", "text/javascript")]) | ||||
|         .await?; | ||||
|     conn.write_all("[".as_bytes()).await?; | ||||
|     let mut append = false; | ||||
|     for entry in log { | ||||
|         if append { | ||||
|             conn.write_all(",".as_bytes()).await?; | ||||
|         } | ||||
|         append = true; | ||||
|         let json = serde_json::to_string(&entry)?; | ||||
|         conn.write_all(json.as_bytes()).await?; | ||||
|     } | ||||
|     conn.write_all("]".as_bytes()).await?; | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| async fn get_log_localization_config<T, const N: usize>( | ||||
|     _request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> { | ||||
|     Ok(Some(serde_json::to_string( | ||||
|         &LogMessage::to_log_localisation_config(), | ||||
|     )?)) | ||||
| } | ||||
| async fn list_files<T, const N: usize>( | ||||
|     _request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> { | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     let result = board.board_hal.get_esp().list_files().await?; | ||||
|     let file_list_json = serde_json::to_string(&result)?; | ||||
|     Ok(Some(file_list_json)) | ||||
| } | ||||
| async fn get_config<T, const N: usize>( | ||||
|     _request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> { | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     let json = serde_json::to_string(&board.board_hal.get_config())?; | ||||
|     Ok(Some(json)) | ||||
| } | ||||
|  | ||||
| async fn get_solar_state<T, const N: usize>( | ||||
|     _request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> { | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     let state = SolarState { | ||||
|         mppt_voltage: board.board_hal.get_mptt_voltage().await?.as_millivolts() as f32, | ||||
|         mppt_current: board.board_hal.get_mptt_current().await?.as_milliamperes() as f32, | ||||
|         is_day: board.board_hal.is_day(), | ||||
|     }; | ||||
|     Ok(Some(serde_json::to_string(&state)?)) | ||||
| } | ||||
|  | ||||
| async fn get_battery_state<T, const N: usize>( | ||||
|     _request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> { | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     let battery_state = board | ||||
|         .board_hal | ||||
|         .get_battery_monitor() | ||||
|         .get_battery_state() | ||||
|         .await?; | ||||
|     Ok(Some(serde_json::to_string(&battery_state)?)) | ||||
| } | ||||
|  | ||||
| async fn get_version_web<T, const N: usize>( | ||||
|     _request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> { | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     Ok(Some(serde_json::to_string(&get_version(&mut board).await)?)) | ||||
| } | ||||
|  | ||||
| async fn get_time<T, const N: usize>( | ||||
|     _request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> { | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     let conf = board.board_hal.get_config(); | ||||
|     let tz = Tz::from_str(conf.timezone.as_ref().unwrap().as_str()).unwrap(); | ||||
|     let native = esp_time().await.with_timezone(&tz).to_rfc3339(); | ||||
|  | ||||
|     let rtc = match board.board_hal.get_rtc_module().get_rtc_time().await { | ||||
|         Ok(time) => time.with_timezone(&tz).to_rfc3339(), | ||||
|         Err(err) => { | ||||
|             format!("Error getting time: {}", err) | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     let data = LoadData { | ||||
|         rtc: rtc.as_str(), | ||||
|         native: native.as_str(), | ||||
|     }; | ||||
|     let json = serde_json::to_string(&data)?; | ||||
|  | ||||
|     Ok(Some(json)) | ||||
|     let final_buffer = data_store.concat(); | ||||
|     Ok(final_buffer) | ||||
| } | ||||
|  | ||||
| #[embassy_executor::task] | ||||
| pub async fn httpd(reboot_now: Arc<AtomicBool>, stack: Stack<'static>) { | ||||
| pub async fn http_server(reboot_now: Arc<AtomicBool>, stack: Stack<'static>) { | ||||
|     let buffer: TcpBuffers<2, 1024, 1024> = TcpBuffers::new(); | ||||
|     let tcp = Tcp::new(stack, &buffer); | ||||
|     let acceptor = tcp | ||||
|         .bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 80)) | ||||
|         .bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 80)) | ||||
|         .await | ||||
|         .unwrap(); | ||||
|  | ||||
|     let mut server: Server<2, 512, 15> = Server::new(); | ||||
|     server | ||||
|         .run(Some(5000), acceptor, HttpHandler { reboot_now }) | ||||
|         .run(Some(5000), acceptor, HTTPRequestRouter { reboot_now }) | ||||
|         .await | ||||
|         .expect("TODO: panic message"); | ||||
|     println!("Wait for connection..."); | ||||
|         .expect("Tcp stack error"); | ||||
|     info!("Webserver started and waiting for connections"); | ||||
|  | ||||
|     // server | ||||
|     //     .fn_handler("/moisture", Method::Get, |request| { | ||||
|     //         handle_error_to500(request, get_live_moisture) | ||||
|     //     }) | ||||
|     //     .unwrap(); | ||||
|     //TODO https if mbed_esp lands | ||||
|  | ||||
|     // server | ||||
|     //     .fn_handler("/ota", Method::Post, |request| { | ||||
|   | ||||
							
								
								
									
										112
									
								
								rust/src/webserver/post_json.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								rust/src/webserver/post_json.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| use crate::config::PlantControllerConfig; | ||||
| use crate::fat_error::FatResult; | ||||
| use crate::hal::esp_set_time; | ||||
| use crate::webserver::read_up_to_bytes_from_request; | ||||
| use crate::{do_secure_pump, BOARD_ACCESS}; | ||||
| use alloc::string::{String, ToString}; | ||||
| use alloc::vec::Vec; | ||||
| use chrono::DateTime; | ||||
| use edge_http::io::server::Connection; | ||||
| use embedded_io_async::{Read, Write}; | ||||
| use log::info; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| pub struct NightLampCommand { | ||||
|     active: bool, | ||||
| } | ||||
| #[derive(Serialize, Debug)] | ||||
| struct SSIDList { | ||||
|     ssids: Vec<String>, | ||||
| } | ||||
| #[derive(Deserialize, Debug)] | ||||
| struct SetTime<'a> { | ||||
|     time: &'a str, | ||||
| } | ||||
|  | ||||
| #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] | ||||
| pub struct TestPump { | ||||
|     pump: usize, | ||||
| } | ||||
|  | ||||
| pub(crate) async fn wifi_scan<T, const N: usize>( | ||||
|     _request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> { | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     info!("start wifi scan"); | ||||
|     let mut ssids: Vec<String> = Vec::new(); | ||||
|     let scan_result = board.board_hal.get_esp().wifi_scan().await?; | ||||
|     scan_result | ||||
|         .iter() | ||||
|         .for_each(|s| ssids.push(s.ssid.to_string())); | ||||
|     let ssid_json = serde_json::to_string(&SSIDList { ssids })?; | ||||
|     info!("Sending ssid list {}", &ssid_json); | ||||
|     Ok(Some(ssid_json)) | ||||
| } | ||||
|  | ||||
| pub(crate) async fn board_test() -> FatResult<Option<String>> { | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     board.board_hal.test().await?; | ||||
|     Ok(None) | ||||
| } | ||||
|  | ||||
| pub(crate) async fn pump_test<T, const N: usize>( | ||||
|     request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> | ||||
| where | ||||
|     T: Read + Write, | ||||
| { | ||||
|     let actual_data = read_up_to_bytes_from_request(request, None).await?; | ||||
|     let pump_test: TestPump = serde_json::from_slice(&actual_data)?; | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|  | ||||
|     let config = &board.board_hal.get_config().plants[pump_test.pump].clone(); | ||||
|     let pump_result = do_secure_pump(&mut board, pump_test.pump, config, false).await; | ||||
|     //ensure it is disabled before unwrapping | ||||
|     board.board_hal.pump(pump_test.pump, false).await?; | ||||
|  | ||||
|     Ok(Some(serde_json::to_string(&pump_result?)?)) | ||||
| } | ||||
|  | ||||
| pub(crate) async fn night_lamp_test<T, const N: usize>( | ||||
|     request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> | ||||
| where | ||||
|     T: Read + Write, | ||||
| { | ||||
|     let actual_data = read_up_to_bytes_from_request(request, None).await?; | ||||
|     let light_command: NightLampCommand = serde_json::from_slice(&actual_data)?; | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     board.board_hal.light(light_command.active).await?; | ||||
|     Ok(None) | ||||
| } | ||||
|  | ||||
| pub(crate) async fn write_time<T, const N: usize>( | ||||
|     request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> | ||||
| where | ||||
|     T: Read + Write, | ||||
| { | ||||
|     let actual_data = read_up_to_bytes_from_request(request, None).await?; | ||||
|     let time: SetTime = serde_json::from_slice(&actual_data)?; | ||||
|     let parsed = DateTime::parse_from_rfc3339(time.time).unwrap(); | ||||
|     esp_set_time(parsed).await?; | ||||
|     Ok(None) | ||||
| } | ||||
|  | ||||
| pub(crate) async fn set_config<T, const N: usize>( | ||||
|     request: &mut Connection<'_, T, N>, | ||||
| ) -> FatResult<Option<String>> | ||||
| where | ||||
|     T: Read + Write, | ||||
| { | ||||
|     let all = read_up_to_bytes_from_request(request, Some(4096)).await?; | ||||
|     let length = all.len(); | ||||
|     let config: PlantControllerConfig = serde_json::from_slice(&all)?; | ||||
|  | ||||
|     let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|     board.board_hal.get_esp().save_config(all).await?; | ||||
|     info!("Wrote config config {:?} with size {}", config, length); | ||||
|     board.board_hal.set_config(config); | ||||
|     Ok(Some("saved".to_string())) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user