From 65f6670ca4db0630de60f4738de611b7a707e1d7 Mon Sep 17 00:00:00 2001 From: Empire Phoenix Date: Mon, 15 Sep 2025 01:21:50 +0200 Subject: [PATCH] adda lot of basic webserver back --- rust/Cargo.toml | 3 +- rust/src/hal/battery.rs | 7 + rust/src/hal/esp.rs | 3 +- rust/src/hal/mod.rs | 22 ++- rust/src/main.rs | 20 +-- rust/src/webserver/mod.rs | 366 ++++++++++++++++++++++++-------------- 6 files changed, 279 insertions(+), 142 deletions(-) diff --git a/rust/Cargo.toml b/rust/Cargo.toml index defe6af..9deaa83 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -83,7 +83,7 @@ esp-println = { version = "0.15.0", features = ["esp32c6", "log-04"] } # for more networking protocol support see https://crates.io/crates/edge-net embassy-executor = { version = "0.7.0", features = [ "log", - "task-arena-size-20480", + "task-arena-size-262144" ] } embassy-time = { version = "0.5.0", features = ["log"] } esp-hal-embassy = { version = "0.9.0", features = ["esp32c6", "log-04"] } @@ -158,6 +158,7 @@ edge-nal = "0.5.0" edge-nal-embassy = "0.6.0" static_cell = "2.1.1" cfg-if = "1.0.3" +edge-http = { version = "0.6.1", features = ["log"] } [patch.crates-io] diff --git a/rust/src/hal/battery.rs b/rust/src/hal/battery.rs index 44e2cbf..f096350 100644 --- a/rust/src/hal/battery.rs +++ b/rust/src/hal/battery.rs @@ -1,6 +1,7 @@ use crate::hal::Box; use alloc::string::String; use async_trait::async_trait; +use core::error::Error; use serde::Serialize; #[async_trait] @@ -17,6 +18,12 @@ pub trait BatteryInteraction { async fn get_battery_state(&mut self) -> Result; } +impl From for anyhow::Error { + fn from(err: BatteryError) -> Self { + anyhow::anyhow!(err) + } +} + #[derive(Debug, Serialize)] pub struct BatteryInfo { pub voltage_milli_volt: u16, diff --git a/rust/src/hal/esp.rs b/rust/src/hal/esp.rs index 3620a7f..10e9bd7 100644 --- a/rust/src/hal/esp.rs +++ b/rust/src/hal/esp.rs @@ -76,6 +76,7 @@ pub struct Esp<'a> { pub slot: usize, pub next_slot: usize, pub ota_state: OtaImageState, + pub slot_addres: u32, } pub struct IpInfo { @@ -200,7 +201,7 @@ impl Esp<'_> { let (stack, runner) = embassy_net::new( device, config, - mk_static!(StackResources<3>, StackResources::<3>::new()), + mk_static!(StackResources<4>, StackResources::<4>::new()), seed, ); let stack = mk_static!(Stack, stack); diff --git a/rust/src/hal/mod.rs b/rust/src/hal/mod.rs index d45c319..b188b75 100644 --- a/rust/src/hal/mod.rs +++ b/rust/src/hal/mod.rs @@ -35,7 +35,9 @@ use embassy_net::{IpListenEndpoint, Ipv4Cidr, Runner, Stack, StackResources, Sta use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::once_lock::OnceLock; use embassy_time::{Duration, Timer}; -use esp_bootloader_esp_idf::partitions::DataPartitionSubType; +use esp_bootloader_esp_idf::partitions::{ + AppPartitionSubType, DataPartitionSubType, PartitionEntry, +}; use esp_hal::clock::CpuClock; use esp_hal::gpio::{Input, InputConfig, Io, Pull}; use esp_hal::timer::systimer::SystemTimer; @@ -286,6 +288,23 @@ impl PlantHal { println!("current {:?} - next {:?}", current, current.next()); let ota_state = ota.current_ota_state()?; + let current_app = if current.number() == 0 { + pt.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App( + AppPartitionSubType::Ota0, + )) + } else { + pt.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App( + AppPartitionSubType::Ota1, + )) + }; + let app_address = match current_app { + Result::Ok(part) => match part { + None => 0, + Some(entry) => entry.offset(), + }, + Err(_) => 0, + }; + let mut esp = Esp { rng, controller: Some(controller), @@ -294,6 +313,7 @@ impl PlantHal { mqtt_client: None, storage, slot: current.number(), + slot_addres: app_address, next_slot: current.next().number(), ota_state, wall_clock_offset: 0, diff --git a/rust/src/main.rs b/rust/src/main.rs index d1c4ed4..38ab0d8 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -147,15 +147,14 @@ async fn safe_main(spawner: Spawner) -> anyhow::Result<()> { info!("Startup Rust"); let mut to_config = false; + let mut board = BOARD_ACCESS.get().await.lock().await; - let version = get_version(); + let version = get_version(&mut board).await; info!( "Version using git has {} build on {}", version.git_hash, version.build_time ); - let _esp = BOARD_ACCESS.get().await.lock().await.board_hal.get_esp(); - //TODO // TODO @@ -179,7 +178,7 @@ async fn safe_main(spawner: Spawner) -> anyhow::Result<()> { //}; //log(LogMessage::PartitionState, 0, 0, "", ota_state_string); let _ota_state_string = "unknown"; - let mut board = BOARD_ACCESS.get().await.lock().await; + board.board_hal.general_fault(false).await; let cur = board .board_hal @@ -1117,15 +1116,16 @@ pub fn in_time_range(cur: &DateTime, start: u8, end: u8) -> bool { } } -fn get_version() -> VersionInfo { +async fn get_version( + board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, +) -> VersionInfo { let branch = env!("VERGEN_GIT_BRANCH").to_owned(); let hash = &env!("VERGEN_GIT_SHA")[0..8]; - //TODO - //let running_partition = unsafe { esp_ota_get_running_partition() }; - let address = 0; - //let address = unsafe { (*running_partition).address }; - let partition = if address > 100000 { + let board = board.board_hal.get_esp(); + let ota_slot = board.slot; + let address = board.slot_addres; + let partition = if ota_slot == 0 { "ota_1 @ " } else { "ota_0 @ " diff --git a/rust/src/webserver/mod.rs b/rust/src/webserver/mod.rs index 203c0e4..6a3eac5 100644 --- a/rust/src/webserver/mod.rs +++ b/rust/src/webserver/mod.rs @@ -6,22 +6,32 @@ use crate::{ hal::PLANT_COUNT, log::LogMessage, plant_state::{MoistureSensorState, PlantState}, - BOARD_ACCESS, + VersionInfo, BOARD_ACCESS, }; use alloc::boxed::Box; -use alloc::string::String; +use alloc::string::{String, ToString}; use alloc::sync::Arc; use alloc::vec::Vec; use anyhow::{bail, Context}; use chrono::DateTime; +use core::fmt::{Debug, Display, Pointer}; +use core::future::Future; +use core::net::{IpAddr, Ipv4Addr, SocketAddr}; use core::result::Result::Ok; use core::sync::atomic::AtomicBool; +use edge_http::io::server::{Connection, DefaultServer, Handler, Server}; +use edge_http::io::Error; +use edge_http::Method; +use edge_nal::TcpBind; +use edge_nal_embassy::{Tcp, TcpAccept, TcpBuffers}; use embassy_executor::Spawner; use embassy_net::tcp::TcpSocket; use embassy_net::{IpListenEndpoint, Stack}; -use embassy_time::{Duration, Timer}; +use embassy_time::{Duration, Instant, Timer}; +use embedded_io_async::{Read, Write}; use esp_println::{print, println}; use esp_wifi::wifi::WifiController; +use log::info; use serde::{Deserialize, Serialize}; #[derive(Serialize, Debug)] @@ -89,31 +99,7 @@ pub struct NightLampCommand { // anyhow::Ok(None) // } // -// fn get_time( -// _request: &mut Request<&mut EspHttpConnection>, -// ) -> Result, anyhow::Error> { -// let mut board = BOARD_ACCESS.lock().expect("board access"); -// let native = board -// .board_hal -// .get_esp() -// .time() -// .map(|t| t.to_rfc3339()) -// .unwrap_or("error".to_string()); -// let rtc = board -// .board_hal -// .get_rtc_module() -// .get_rtc_time() -// .map(|t| 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_timezones( // _request: &mut Request<&mut EspHttpConnection>, @@ -231,25 +217,9 @@ pub struct NightLampCommand { // anyhow::Ok(Some("saved".to_owned())) // } // -// fn get_solar_state( -// _request: &mut Request<&mut EspHttpConnection>, -// ) -> Result, anyhow::Error> { -// let mut board = BOARD_ACCESS.lock().expect("board access"); -// let state = SolarState { -// mppt_voltage: board.board_hal.get_mptt_voltage()?.as_millivolts() as f32, -// mppt_current: board.board_hal.get_mptt_current()?.as_milliamperes() as f32, -// is_day: board.board_hal.is_day(), -// }; -// anyhow::Ok(Some(serde_json::to_string(&state)?)) -// } + // -// fn get_battery_state( -// _request: &mut Request<&mut EspHttpConnection>, -// ) -> Result, anyhow::Error> { -// let mut board = BOARD_ACCESS.lock().expect("board access"); -// let battery_state = board.board_hal.get_battery_monitor().get_battery_state(); -// anyhow::Ok(Some(serde_json::to_string(&battery_state)?)) -// } + // // fn get_log( // _request: &mut Request<&mut EspHttpConnection>, @@ -264,11 +234,7 @@ pub struct NightLampCommand { // )?) // } // -// 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>, @@ -323,16 +289,7 @@ pub struct NightLampCommand { // anyhow::Ok(Some(ssid_json)) // } // -// fn list_files( -// _request: &mut Request<&mut EspHttpConnection>, -// ) -> Result, anyhow::Error> { -// let mut board = BOARD_ACCESS -// .lock() -// .expect("It should be possible to lock the board for exclusive fs access"); -// let result = board.board_hal.get_esp().list_files(); -// let file_list_json = serde_json::to_string(&result)?; -// anyhow::Ok(Some(file_list_json)) -// } + // // fn ota( // request: &mut Request<&mut EspHttpConnection>, @@ -390,82 +347,175 @@ pub struct NightLampCommand { // } // } -#[embassy_executor::task] -pub async fn httpd(reboot_now: Arc, stack: Stack<'static>) { - let mut rx_buffer = [0; 1536]; - let mut tx_buffer = [0; 1536]; - let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); - socket.set_keep_alive(Some(Duration::from_secs(30))); - socket.set_timeout(Some(Duration::from_secs(5))); +struct HttpHandler; - loop { - println!("Wait for connection..."); - let r = socket - .accept(IpListenEndpoint { - addr: None, - port: 8080, - }) - .await; - println!("Connected..."); +impl Handler for HttpHandler { + type Error + = Error + where + E: Debug; - if let Err(e) = r { - println!("connect error: {:?}", e); - continue; - } + async fn handle<'a, T, const N: usize>( + &self, + _task_id: impl Display + Copy, + conn: &mut Connection<'a, T, N>, + ) -> anyhow::Result<(), Self::Error> + where + T: Read + Write, + { + let start = Instant::now(); + let headers = conn.headers()?; - use embedded_io_async::Write; + let method = headers.method; + let path = headers.path; - let mut buffer = [0u8; 1024]; - let mut pos = 0; - loop { - match socket.read(&mut buffer).await { - core::result::Result::Ok(len) => { - if len == 0 { - println!("read EOF"); - break; - } else { - let to_print = - unsafe { core::str::from_utf8_unchecked(&buffer[..(pos + len)]) }; - - if to_print.contains("\r\n\r\n") { - print!("{}", to_print); - println!(); - break; - } - - pos += len; + let status = 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")]) + .await?; + conn.write_all(include_bytes!("index.html")).await?; + Some(200) + } + "/bundle.js" => { + conn.initiate_response(200, Some("OK"), &[("Content-Type", "text/javascript")]) + .await?; + conn.write_all(include_bytes!("bundle.js")).await?; + Some(200) + } + &_ => { + 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), + _ => None, + }; + match json { + None => None, + Some(json) => Some(handle_json(conn, json).await?), } } - Err(e) => { - println!("read error: {:?}", e); - break; - } - }; - } - let r = socket - .write_all( - b"HTTP/1.0 200 OK\r\n\r\n\ - \ - \ -

Hello Rust! Hello esp-wifi!

\ - \ - \r\n\ - ", - ) - .await; - if let Err(e) = r { - println!("write error: {:?}", e); - } + }, + Method::Options | Method::Delete | Method::Head | Method::Post | Method::Put => None, + _ => None, + }; + let code = match status { + None => { + conn.initiate_response(404, Some("Not found"), &[]).await?; + 404 + } + Some(code) => code, + }; - let r = socket.flush().await; - if let Err(e) = r { - println!("flush error: {:?}", e); - } - Timer::after_millis(100).await; - socket.close(); - Timer::after_millis(100).await; - socket.abort(); + conn.complete().await?; + let response_time = Instant::now().duration_since(start).as_millis(); + + info!("\"{method} {path}\" {code} {response_time}ms"); + Ok(()) } +} + +async fn get_log_localization_config( + _request: &mut Connection<'_, T, N>, +) -> Result, anyhow::Error> { + anyhow::Ok(Some(serde_json::to_string( + &LogMessage::to_log_localisation_config(), + )?)) +} +async fn list_files( + _request: &mut Connection<'_, T, N>, +) -> Result, anyhow::Error> { + 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)?; + anyhow::Ok(Some(file_list_json)) +} +async fn get_config( + _request: &mut Connection<'_, T, N>, +) -> Result, anyhow::Error> { + let mut board = BOARD_ACCESS.get().await.lock().await; + let json = serde_json::to_string(&board.board_hal.get_config())?; + anyhow::Ok(Some(json)) +} + +async fn get_solar_state( + _request: &mut Connection<'_, T, N>, +) -> Result, anyhow::Error> { + 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(), + }; + anyhow::Ok(Some(serde_json::to_string(&state)?)) +} + +async fn get_battery_state( + _request: &mut Connection<'_, T, N>, +) -> Result, anyhow::Error> { + let mut board = BOARD_ACCESS.get().await.lock().await; + let battery_state = board + .board_hal + .get_battery_monitor() + .get_battery_state() + .await?; + anyhow::Ok(Some(serde_json::to_string(&battery_state)?)) +} + +async fn get_version_web( + _request: &mut Connection<'_, T, N>, +) -> Result, anyhow::Error> { + let mut board = BOARD_ACCESS.get().await.lock().await; + anyhow::Ok(Some(serde_json::to_string(&get_version(&mut board).await)?)) +} + +async fn get_time( + _request: &mut Connection<'_, T, N>, +) -> Result, anyhow::Error> { + let mut board = BOARD_ACCESS.get().await.lock().await; + //TODO do not fail if rtc module is missing + let native = board.board_hal.get_esp().time().to_rfc3339(); + let rtc = board + .board_hal + .get_rtc_module() + .get_rtc_time() + .await? + .to_rfc3339(); + + let data = LoadData { + rtc: rtc.as_str(), + native: native.as_str(), + }; + let json = serde_json::to_string(&data)?; + + anyhow::Ok(Some(json)) +} + +#[embassy_executor::task] +pub async fn httpd(reboot_now: Arc, 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)), 8080)) + .await + .unwrap(); + + let mut server: Server<2, 512, 10> = Server::new(); + server + .run(Some(5000), acceptor, HttpHandler) + .await + .expect("TODO: panic message"); + println!("Wait for connection..."); // let server_config = Configuration { // stack_size: 32768, @@ -805,3 +855,61 @@ pub async fn httpd(reboot_now: Arc, stack: Stack<'static>) { // log::info!("Raw data {}", from_utf8(&allvec)?); // Ok(allvec) // } + +async fn handle_json<'a, T, const N: usize>( + conn: &mut Connection<'a, T, N>, + chain: anyhow::Result>, +) -> anyhow::Result> +where + T: Read + Write, + ::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) + } + } +}