Files
PlantCtrl/Software/MainBoard/rust/src/webserver/mod.rs
2025-10-15 02:14:49 +02:00

303 lines
11 KiB
Rust

//offer ota and config mode
mod backup_manager;
mod file_manager;
mod get_json;
mod get_log;
mod get_static;
mod ota;
mod post_json;
use crate::fat_error::{FatError, FatResult};
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::ota::ota_operations;
use crate::webserver::post_json::{
board_test, night_lamp_test, pump_test, set_config, wifi_scan, write_time, detect_sensors,
};
use crate::{bail, BOARD_ACCESS};
use alloc::borrow::ToOwned;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt::{Debug, Display};
use core::net::{IpAddr, Ipv4Addr, SocketAddr};
use core::result::Result::Ok;
use core::sync::atomic::{AtomicBool, Ordering};
use edge_http::io::server::{Connection, Handler, Server};
use edge_http::Method;
use edge_nal::TcpBind;
use edge_nal_embassy::{Tcp, TcpBuffers};
use embassy_net::Stack;
use embassy_time::Instant;
use embedded_io_async::{Read, Write};
use log::{error, info};
// fn ota(
// request: &mut Request<&mut EspHttpConnection>,
// ) -> Result<Option<std::string::String>, anyhow::Error> {
// let mut board = BOARD_ACCESS.lock().unwrap();
// let mut ota = OtaUpdate::begin()?;
// log::info!("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;
// let to_write = &buffer[0..read];
// //delay for watchdog and wifi stuff
// board.board_hal.get_esp().delay.delay_ms(1);
//
// let iter = (total_read / 1024) % 8;
// if iter != lastiter {
// board.board_hal.general_fault(iter % 5 == 0);
// for i in 0..PLANT_COUNT {
// let _ = board.board_hal.fault(i, iter == i);
// }
// lastiter = iter;
// }
//
// ota.write(to_write)?;
// if read == 0 {
// break;
// }
// }
// log::info!("wrote bytes ota {total_read}");
// log::info!("finish ota");
// let partition = ota.raw_partition();
// log::info!("finalizing and changing boot partition to {partition:?}");
//
// let mut finalizer = ota.finalize()?;
// log::info!("changing boot partition");
// board.board_hal.get_esp().set_restart_to_conf(true);
// drop(board);
// finalizer.set_as_boot_partition()?;
// anyhow::Ok(None)
// }
//
struct HTTPRequestRouter {
reboot_now: Arc<AtomicBool>,
}
impl Handler for HTTPRequestRouter {
type Error<E: Debug> = FatError;
async fn handle<'a, T, const N: usize>(
&self,
_task_id: impl Display + Copy,
conn: &mut Connection<'a, T, N>,
) -> Result<(), FatError>
where
T: Read + Write,
{
let start = Instant::now();
let headers = conn.headers()?;
let method = headers.method;
let path = headers.path;
let prefix = "/file?filename=";
let status = if path.starts_with(prefix) {
file_operations(conn, method, &path, &prefix).await?
} else if path == "/ota" {
ota_operations(conn, method).await.map_err(|e| {
error!("Error handling ota: {}", e);
e
})?
} else {
match method {
Method::Get => match path {
"/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),
"/time" => Some(get_time(conn).await),
"/battery" => Some(get_battery_state(conn).await),
"/solar" => Some(get_solar_state(conn).await),
"/get_config" => Some(get_config(conn).await),
"/files" => Some(list_files(conn).await),
"/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().await),
"/moisture" => Some(get_live_moisture(conn).await),
_ => None,
};
match json {
None => None,
Some(json) => Some(handle_json(conn, json).await?),
}
}
},
Method::Post => {
let json = match path {
"/wifiscan" => Some(wifi_scan(conn).await),
"/set_config" => Some(set_config(conn).await),
"/time" => Some(write_time(conn).await),
"/backup_config" => Some(backup_config(conn).await),
"/pumptest" => Some(pump_test(conn).await),
"/lamptest" => Some(night_lamp_test(conn).await),
"/boardtest" => Some(board_test().await),
"/detect_sensors" => Some(detect_sensors().await),
"/reboot" => {
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.get_esp().set_restart_to_conf(true);
self.reboot_now.store(true, Ordering::Relaxed);
Some(Ok(None))
}
"/exit" => {
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.get_esp().set_restart_to_conf(false);
self.reboot_now.store(true, Ordering::Relaxed);
Some(Ok(None))
}
_ => None,
};
match json {
None => None,
Some(json) => Some(handle_json(conn, json).await?),
}
}
Method::Options | Method::Delete | Method::Head | Method::Put => None,
_ => None,
}
};
let code = match status {
None => {
conn.initiate_response(404, Some("Not found"), &[]).await?;
404
}
Some(code) => code,
};
conn.complete().await?;
let response_time = Instant::now().duration_since(start).as_millis();
info!("\"{method} {path}\" {code} {response_time}ms");
Ok(())
}
}
async fn read_up_to_bytes_from_request<T, const N: usize>(
request: &mut Connection<'_, T, N>,
limit: Option<usize>,
) -> FatResult<Vec<u8>>
where
T: Read + Write,
{
let max_read = limit.unwrap_or(1024);
let mut data_store = Vec::new();
let mut total_read = 0;
loop {
let left = max_read - total_read;
let mut buf = [0_u8; 64];
let s_buf = if buf.len() <= left {
&mut buf
} else {
&mut buf[0..left]
};
let read = request.read(s_buf).await?;
if read == 0 {
break;
}
let actual_data = &s_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 final_buffer = data_store.concat();
Ok(final_buffer)
}
#[embassy_executor::task]
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::UNSPECIFIED), 80))
.await
.unwrap();
let mut server: Server<2, 512, 15> = Server::new();
server
.run(Some(5000), acceptor, HTTPRequestRouter { reboot_now })
.await
.expect("Tcp stack error");
info!("Webserver started and waiting for connections");
//TODO https if mbed_esp lands
}
async fn handle_json<'a, T, const N: usize>(
conn: &mut Connection<'a, T, N>,
chain: FatResult<Option<String>>,
) -> FatResult<u32>
where
T: Read + Write,
<T as embedded_io_async::ErrorType>::Error: Debug,
{
match chain {
Ok(answer) => match answer {
Some(json) => {
conn.initiate_response(
200,
Some("OK"),
&[
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
("Content-Type", "application/json"),
],
)
.await?;
conn.write_all(json.as_bytes()).await?;
Ok(200)
}
None => {
conn.initiate_response(
200,
Some("OK"),
&[
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
],
)
.await?;
Ok(200)
}
},
Err(err) => {
let error_text = err.to_string();
info!("error handling process {}", error_text);
conn.initiate_response(
500,
Some("OK"),
&[
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
],
)
.await?;
conn.write_all(error_text.as_bytes()).await?;
Ok(500)
}
}
}