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">
|
<component name="ProjectDictionaryState">
|
||||||
<dictionary name="project">
|
<dictionary name="project">
|
||||||
<words>
|
<words>
|
||||||
|
<w>boardtest</w>
|
||||||
<w>buildtime</w>
|
<w>buildtime</w>
|
||||||
<w>deepsleep</w>
|
<w>deepsleep</w>
|
||||||
<w>githash</w>
|
<w>githash</w>
|
||||||
|
<w>lamptest</w>
|
||||||
<w>lightstate</w>
|
<w>lightstate</w>
|
||||||
<w>mppt</w>
|
<w>mppt</w>
|
||||||
<w>plantstate</w>
|
<w>plantstate</w>
|
||||||
|
<w>pumptest</w>
|
||||||
<w>sntp</w>
|
<w>sntp</w>
|
||||||
<w>vergen</w>
|
<w>vergen</w>
|
||||||
|
<w>wifiscan</w>
|
||||||
</words>
|
</words>
|
||||||
</dictionary>
|
</dictionary>
|
||||||
</component>
|
</component>
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
holding buffers for the duration of a data transfer."
|
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!();
|
esp_bootloader_esp_idf::esp_app_desc!();
|
||||||
use esp_backtrace as _;
|
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
|
//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::fat_error::{FatError, FatResult};
|
||||||
use crate::hal::rtc::X25;
|
use crate::webserver::backup_manager::{backup_config, backup_info, get_backup_config};
|
||||||
use crate::hal::{esp_set_time, esp_time};
|
use crate::webserver::file_manager::{file_operations, list_files};
|
||||||
use crate::log::LOG_ACCESS;
|
use crate::webserver::get_json::{
|
||||||
use crate::tank::determine_tank_state;
|
get_battery_state, get_config, get_live_moisture, get_log_localization_config, get_solar_state,
|
||||||
use crate::{bail, do_secure_pump, get_version, log::LogMessage, BOARD_ACCESS};
|
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::borrow::ToOwned;
|
||||||
use alloc::format;
|
|
||||||
use alloc::string::{String, ToString};
|
use alloc::string::{String, ToString};
|
||||||
use alloc::sync::Arc;
|
use alloc::sync::Arc;
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
use chrono::DateTime;
|
|
||||||
use chrono_tz::Tz;
|
|
||||||
use core::fmt::{Debug, Display};
|
use core::fmt::{Debug, Display};
|
||||||
use core::net::{IpAddr, Ipv4Addr, SocketAddr};
|
use core::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||||
use core::result::Result::Ok;
|
use core::result::Result::Ok;
|
||||||
use core::str::{from_utf8, FromStr};
|
|
||||||
use core::sync::atomic::{AtomicBool, Ordering};
|
use core::sync::atomic::{AtomicBool, Ordering};
|
||||||
use edge_http::io::server::{Connection, Handler, Server};
|
use edge_http::io::server::{Connection, Handler, Server};
|
||||||
use edge_http::Method;
|
use edge_http::Method;
|
||||||
@@ -26,97 +35,8 @@ use edge_nal_embassy::{Tcp, TcpBuffers};
|
|||||||
use embassy_net::Stack;
|
use embassy_net::Stack;
|
||||||
use embassy_time::Instant;
|
use embassy_time::Instant;
|
||||||
use embedded_io_async::{Read, Write};
|
use embedded_io_async::{Read, Write};
|
||||||
use esp_println::println;
|
|
||||||
use log::info;
|
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(
|
// fn ota(
|
||||||
// request: &mut Request<&mut EspHttpConnection>,
|
// request: &mut Request<&mut EspHttpConnection>,
|
||||||
// ) -> Result<Option<std::string::String>, anyhow::Error> {
|
// ) -> Result<Option<std::string::String>, anyhow::Error> {
|
||||||
@@ -164,11 +84,11 @@ pub struct NightLampCommand {
|
|||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
|
|
||||||
struct HttpHandler {
|
struct HTTPRequestRouter {
|
||||||
reboot_now: Arc<AtomicBool>,
|
reboot_now: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handler for HttpHandler {
|
impl Handler for HTTPRequestRouter {
|
||||||
type Error<E: Debug> = FatError;
|
type Error<E: Debug> = FatError;
|
||||||
async fn handle<'a, T, const N: usize>(
|
async fn handle<'a, T, const N: usize>(
|
||||||
&self,
|
&self,
|
||||||
@@ -186,159 +106,15 @@ impl Handler for HttpHandler {
|
|||||||
|
|
||||||
let prefix = "/file?filename=";
|
let prefix = "/file?filename=";
|
||||||
let status = if path.starts_with(prefix) {
|
let status = if path.starts_with(prefix) {
|
||||||
let filename = &path[prefix.len()..];
|
file_operations(conn, method, &path, &prefix).await?
|
||||||
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,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
match method {
|
match method {
|
||||||
Method::Get => match path {
|
Method::Get => match path {
|
||||||
"/favicon.ico" => {
|
"/favicon.ico" => serve_favicon(conn).await?,
|
||||||
conn.initiate_response(
|
"/" => serve_index(conn).await?,
|
||||||
200,
|
"/bundle.js" => serve_bundle(conn).await?,
|
||||||
Some("OK"),
|
"/log" => get_log(conn).await?,
|
||||||
&[("Content-Type", "image/x-icon")],
|
"/get_backup_config" => get_backup_config(conn).await?,
|
||||||
)
|
|
||||||
.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)
|
|
||||||
}
|
|
||||||
&_ => {
|
&_ => {
|
||||||
let json = match path {
|
let json = match path {
|
||||||
"/version" => Some(get_version_web(conn).await),
|
"/version" => Some(get_version_web(conn).await),
|
||||||
@@ -350,7 +126,8 @@ impl Handler for HttpHandler {
|
|||||||
"/log_localization" => Some(get_log_localization_config(conn).await),
|
"/log_localization" => Some(get_log_localization_config(conn).await),
|
||||||
"/tank" => Some(tank_info(conn).await),
|
"/tank" => Some(tank_info(conn).await),
|
||||||
"/backup_info" => Some(backup_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,
|
_ => None,
|
||||||
};
|
};
|
||||||
match json {
|
match json {
|
||||||
@@ -367,7 +144,7 @@ impl Handler for HttpHandler {
|
|||||||
"/backup_config" => Some(backup_config(conn).await),
|
"/backup_config" => Some(backup_config(conn).await),
|
||||||
"/pumptest" => Some(pump_test(conn).await),
|
"/pumptest" => Some(pump_test(conn).await),
|
||||||
"/lamptest" => Some(night_lamp_test(conn).await),
|
"/lamptest" => Some(night_lamp_test(conn).await),
|
||||||
"/boardtest" => Some(board_test(conn).await),
|
"/boardtest" => Some(board_test().await),
|
||||||
"/reboot" => {
|
"/reboot" => {
|
||||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||||
board.board_hal.get_esp().set_restart_to_conf(true);
|
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>(
|
async fn read_up_to_bytes_from_request<T, const N: usize>(
|
||||||
request: &mut Connection<'_, T, N>,
|
request: &mut Connection<'_, T, N>,
|
||||||
limit: Option<usize>,
|
limit: Option<usize>,
|
||||||
@@ -689,146 +207,27 @@ where
|
|||||||
}
|
}
|
||||||
data_store.push(actual_data.to_owned());
|
data_store.push(actual_data.to_owned());
|
||||||
}
|
}
|
||||||
let allvec = data_store.concat();
|
let final_buffer = data_store.concat();
|
||||||
log::info!("Raw data {}", from_utf8(&allvec)?);
|
Ok(final_buffer)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[embassy_executor::task]
|
#[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 buffer: TcpBuffers<2, 1024, 1024> = TcpBuffers::new();
|
||||||
let tcp = Tcp::new(stack, &buffer);
|
let tcp = Tcp::new(stack, &buffer);
|
||||||
let acceptor = tcp
|
let acceptor = tcp
|
||||||
.bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 80))
|
.bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 80))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut server: Server<2, 512, 15> = Server::new();
|
let mut server: Server<2, 512, 15> = Server::new();
|
||||||
server
|
server
|
||||||
.run(Some(5000), acceptor, HttpHandler { reboot_now })
|
.run(Some(5000), acceptor, HTTPRequestRouter { reboot_now })
|
||||||
.await
|
.await
|
||||||
.expect("TODO: panic message");
|
.expect("Tcp stack error");
|
||||||
println!("Wait for connection...");
|
info!("Webserver started and waiting for connections");
|
||||||
|
|
||||||
// server
|
//TODO https if mbed_esp lands
|
||||||
// .fn_handler("/moisture", Method::Get, |request| {
|
|
||||||
// handle_error_to500(request, get_live_moisture)
|
|
||||||
// })
|
|
||||||
// .unwrap();
|
|
||||||
|
|
||||||
// server
|
// server
|
||||||
// .fn_handler("/ota", Method::Post, |request| {
|
// .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