adda lot of basic webserver back

This commit is contained in:
Empire Phoenix 2025-09-15 01:21:50 +02:00
parent 049a9d027c
commit 65f6670ca4
6 changed files with 279 additions and 142 deletions

View File

@ -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 # for more networking protocol support see https://crates.io/crates/edge-net
embassy-executor = { version = "0.7.0", features = [ embassy-executor = { version = "0.7.0", features = [
"log", "log",
"task-arena-size-20480", "task-arena-size-262144"
] } ] }
embassy-time = { version = "0.5.0", features = ["log"] } embassy-time = { version = "0.5.0", features = ["log"] }
esp-hal-embassy = { version = "0.9.0", features = ["esp32c6", "log-04"] } 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" edge-nal-embassy = "0.6.0"
static_cell = "2.1.1" static_cell = "2.1.1"
cfg-if = "1.0.3" cfg-if = "1.0.3"
edge-http = { version = "0.6.1", features = ["log"] }
[patch.crates-io] [patch.crates-io]

View File

@ -1,6 +1,7 @@
use crate::hal::Box; use crate::hal::Box;
use alloc::string::String; use alloc::string::String;
use async_trait::async_trait; use async_trait::async_trait;
use core::error::Error;
use serde::Serialize; use serde::Serialize;
#[async_trait] #[async_trait]
@ -17,6 +18,12 @@ pub trait BatteryInteraction {
async fn get_battery_state(&mut self) -> Result<BatteryState, BatteryError>; async fn get_battery_state(&mut self) -> Result<BatteryState, BatteryError>;
} }
impl From<BatteryError> for anyhow::Error {
fn from(err: BatteryError) -> Self {
anyhow::anyhow!(err)
}
}
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct BatteryInfo { pub struct BatteryInfo {
pub voltage_milli_volt: u16, pub voltage_milli_volt: u16,

View File

@ -76,6 +76,7 @@ pub struct Esp<'a> {
pub slot: usize, pub slot: usize,
pub next_slot: usize, pub next_slot: usize,
pub ota_state: OtaImageState, pub ota_state: OtaImageState,
pub slot_addres: u32,
} }
pub struct IpInfo { pub struct IpInfo {
@ -200,7 +201,7 @@ impl Esp<'_> {
let (stack, runner) = embassy_net::new( let (stack, runner) = embassy_net::new(
device, device,
config, config,
mk_static!(StackResources<3>, StackResources::<3>::new()), mk_static!(StackResources<4>, StackResources::<4>::new()),
seed, seed,
); );
let stack = mk_static!(Stack, stack); let stack = mk_static!(Stack, stack);

View File

@ -35,7 +35,9 @@ use embassy_net::{IpListenEndpoint, Ipv4Cidr, Runner, Stack, StackResources, Sta
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::once_lock::OnceLock; use embassy_sync::once_lock::OnceLock;
use embassy_time::{Duration, Timer}; 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::clock::CpuClock;
use esp_hal::gpio::{Input, InputConfig, Io, Pull}; use esp_hal::gpio::{Input, InputConfig, Io, Pull};
use esp_hal::timer::systimer::SystemTimer; use esp_hal::timer::systimer::SystemTimer;
@ -286,6 +288,23 @@ impl PlantHal {
println!("current {:?} - next {:?}", current, current.next()); println!("current {:?} - next {:?}", current, current.next());
let ota_state = ota.current_ota_state()?; 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 { let mut esp = Esp {
rng, rng,
controller: Some(controller), controller: Some(controller),
@ -294,6 +313,7 @@ impl PlantHal {
mqtt_client: None, mqtt_client: None,
storage, storage,
slot: current.number(), slot: current.number(),
slot_addres: app_address,
next_slot: current.next().number(), next_slot: current.next().number(),
ota_state, ota_state,
wall_clock_offset: 0, wall_clock_offset: 0,

View File

@ -147,15 +147,14 @@ async fn safe_main(spawner: Spawner) -> anyhow::Result<()> {
info!("Startup Rust"); info!("Startup Rust");
let mut to_config = false; 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!( info!(
"Version using git has {} build on {}", "Version using git has {} build on {}",
version.git_hash, version.build_time version.git_hash, version.build_time
); );
let _esp = BOARD_ACCESS.get().await.lock().await.board_hal.get_esp();
//TODO //TODO
// TODO // TODO
@ -179,7 +178,7 @@ async fn safe_main(spawner: Spawner) -> anyhow::Result<()> {
//}; //};
//log(LogMessage::PartitionState, 0, 0, "", ota_state_string); //log(LogMessage::PartitionState, 0, 0, "", ota_state_string);
let _ota_state_string = "unknown"; let _ota_state_string = "unknown";
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.general_fault(false).await; board.board_hal.general_fault(false).await;
let cur = board let cur = board
.board_hal .board_hal
@ -1117,15 +1116,16 @@ pub fn in_time_range(cur: &DateTime<Tz>, 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 branch = env!("VERGEN_GIT_BRANCH").to_owned();
let hash = &env!("VERGEN_GIT_SHA")[0..8]; let hash = &env!("VERGEN_GIT_SHA")[0..8];
//TODO let board = board.board_hal.get_esp();
//let running_partition = unsafe { esp_ota_get_running_partition() }; let ota_slot = board.slot;
let address = 0; let address = board.slot_addres;
//let address = unsafe { (*running_partition).address }; let partition = if ota_slot == 0 {
let partition = if address > 100000 {
"ota_1 @ " "ota_1 @ "
} else { } else {
"ota_0 @ " "ota_0 @ "

View File

@ -6,22 +6,32 @@ use crate::{
hal::PLANT_COUNT, hal::PLANT_COUNT,
log::LogMessage, log::LogMessage,
plant_state::{MoistureSensorState, PlantState}, plant_state::{MoistureSensorState, PlantState},
BOARD_ACCESS, VersionInfo, BOARD_ACCESS,
}; };
use alloc::boxed::Box; use alloc::boxed::Box;
use alloc::string::String; use alloc::string::{String, ToString};
use alloc::sync::Arc; use alloc::sync::Arc;
use alloc::vec::Vec; use alloc::vec::Vec;
use anyhow::{bail, Context}; use anyhow::{bail, Context};
use chrono::DateTime; 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::result::Result::Ok;
use core::sync::atomic::AtomicBool; 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_executor::Spawner;
use embassy_net::tcp::TcpSocket; use embassy_net::tcp::TcpSocket;
use embassy_net::{IpListenEndpoint, Stack}; 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_println::{print, println};
use esp_wifi::wifi::WifiController; use esp_wifi::wifi::WifiController;
use log::info;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
@ -89,31 +99,7 @@ pub struct NightLampCommand {
// anyhow::Ok(None) // anyhow::Ok(None)
// } // }
// //
// fn get_time(
// _request: &mut Request<&mut EspHttpConnection>,
// ) -> Result<Option<std::string::String>, 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( // fn get_timezones(
// _request: &mut Request<&mut EspHttpConnection>, // _request: &mut Request<&mut EspHttpConnection>,
@ -231,25 +217,9 @@ pub struct NightLampCommand {
// anyhow::Ok(Some("saved".to_owned())) // anyhow::Ok(Some("saved".to_owned()))
// } // }
// //
// fn get_solar_state(
// _request: &mut Request<&mut EspHttpConnection>,
// ) -> Result<Option<std::string::String>, 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<Option<std::string::String>, 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( // fn get_log(
// _request: &mut Request<&mut EspHttpConnection>, // _request: &mut Request<&mut EspHttpConnection>,
@ -264,11 +234,7 @@ pub struct NightLampCommand {
// )?) // )?)
// } // }
// //
// fn get_version_web(
// _request: &mut Request<&mut EspHttpConnection>,
// ) -> Result<Option<std::string::String>, anyhow::Error> {
// anyhow::Ok(Some(serde_json::to_string(&get_version())?))
// }
// //
// fn pump_test( // fn pump_test(
// request: &mut Request<&mut EspHttpConnection>, // request: &mut Request<&mut EspHttpConnection>,
@ -323,16 +289,7 @@ pub struct NightLampCommand {
// anyhow::Ok(Some(ssid_json)) // anyhow::Ok(Some(ssid_json))
// } // }
// //
// fn list_files(
// _request: &mut Request<&mut EspHttpConnection>,
// ) -> Result<Option<std::string::String>, 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( // fn ota(
// request: &mut Request<&mut EspHttpConnection>, // request: &mut Request<&mut EspHttpConnection>,
@ -390,82 +347,175 @@ pub struct NightLampCommand {
// } // }
// } // }
#[embassy_executor::task] struct HttpHandler;
pub async fn httpd(reboot_now: Arc<AtomicBool>, 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)));
loop { impl Handler for HttpHandler {
println!("Wait for connection..."); type Error<E>
let r = socket = Error<E>
.accept(IpListenEndpoint { where
addr: None, E: Debug;
port: 8080,
})
.await;
println!("Connected...");
if let Err(e) = r { async fn handle<'a, T, const N: usize>(
println!("connect error: {:?}", e); &self,
continue; _task_id: impl Display + Copy,
} conn: &mut Connection<'a, T, N>,
) -> anyhow::Result<(), Self::Error<T::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 status = match method {
let mut pos = 0; Method::Get => match path {
loop { "/favicon.ico" => {
match socket.read(&mut buffer).await { conn.initiate_response(200, Some("OK"), &[("Content-Type", "image/x-icon")])
core::result::Result::Ok(len) => { .await?;
if len == 0 { conn.write_all(include_bytes!("favicon.ico")).await?;
println!("read EOF"); Some(200)
break; }
} else { "/" => {
let to_print = conn.initiate_response(200, Some("OK"), &[("Content-Type", "text/html")])
unsafe { core::str::from_utf8_unchecked(&buffer[..(pos + len)]) }; .await?;
conn.write_all(include_bytes!("index.html")).await?;
if to_print.contains("\r\n\r\n") { Some(200)
print!("{}", to_print); }
println!(); "/bundle.js" => {
break; conn.initiate_response(200, Some("OK"), &[("Content-Type", "text/javascript")])
} .await?;
conn.write_all(include_bytes!("bundle.js")).await?;
pos += len; 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); Method::Options | Method::Delete | Method::Head | Method::Post | Method::Put => None,
break; _ => None,
} };
}; let code = match status {
} None => {
let r = socket conn.initiate_response(404, Some("Not found"), &[]).await?;
.write_all( 404
b"HTTP/1.0 200 OK\r\n\r\n\ }
<html>\ Some(code) => code,
<body>\ };
<h1>Hello Rust! Hello esp-wifi!</h1>\
</body>\
</html>\r\n\
",
)
.await;
if let Err(e) = r {
println!("write error: {:?}", e);
}
let r = socket.flush().await; conn.complete().await?;
if let Err(e) = r { let response_time = Instant::now().duration_since(start).as_millis();
println!("flush error: {:?}", e);
} info!("\"{method} {path}\" {code} {response_time}ms");
Timer::after_millis(100).await; Ok(())
socket.close();
Timer::after_millis(100).await;
socket.abort();
} }
}
async fn get_log_localization_config<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> Result<Option<String>, anyhow::Error> {
anyhow::Ok(Some(serde_json::to_string(
&LogMessage::to_log_localisation_config(),
)?))
}
async fn list_files<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> Result<Option<String>, 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<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> Result<Option<String>, 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<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> Result<Option<String>, 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<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> Result<Option<String>, 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<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> Result<Option<String>, 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<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> Result<Option<String>, 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<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)), 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 { // let server_config = Configuration {
// stack_size: 32768, // stack_size: 32768,
@ -805,3 +855,61 @@ pub async fn httpd(reboot_now: Arc<AtomicBool>, stack: Stack<'static>) {
// log::info!("Raw data {}", from_utf8(&allvec)?); // log::info!("Raw data {}", from_utf8(&allvec)?);
// Ok(allvec) // Ok(allvec)
// } // }
async fn handle_json<'a, T, const N: usize>(
conn: &mut Connection<'a, T, N>,
chain: anyhow::Result<Option<String>>,
) -> anyhow::Result<u32, Error<T::Error>>
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)
}
}
}