PlantCtrl/rust/src/webserver/webserver.rs

702 lines
22 KiB
Rust

//offer ota and config mode
use std::{
str::from_utf8,
sync::{atomic::AtomicBool, Arc},
};
use crate::{
espota::OtaUpdate, get_version, log::LogMessage, map_range_moisture, plant_hal::{FileInfo, PLANT_COUNT}, BOARD_ACCESS
};
use anyhow::bail;
use chrono::DateTime;
use esp_idf_sys::{esp_set_time_from_rtc, settimeofday, timeval, vTaskDelay};
use core::result::Result::Ok;
use embedded_svc::http::Method;
use esp_idf_hal::delay::Delay;
use esp_idf_svc::http::server::{Configuration, EspHttpConnection, EspHttpServer, Request};
use heapless::String;
use serde::{Deserialize, Serialize};
use url::Url;
use crate::config::PlantControllerConfig;
#[derive(Serialize, Debug)]
struct SSIDList<'a> {
ssids: Vec<&'a String<32>>,
}
#[derive(Serialize, Debug)]
struct LoadData<'a> {
rtc: &'a str,
native: &'a str,
}
#[derive(Serialize, Debug)]
struct Moistures {
moisture_a: Vec<u8>,
moisture_b: Vec<u8>,
}
#[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: std::string::String,
size: usize
}
fn write_time(
request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
let actual_data = read_up_to_bytes_from_request(request, None)?;
let time: SetTime = serde_json::from_slice(&actual_data)?;
let parsed = DateTime::parse_from_rfc3339(time.time).map_err(|err| anyhow::anyhow!(err))?;
let mut board = BOARD_ACCESS.lock().unwrap();
let now = timeval {
tv_sec: parsed.to_utc().timestamp(),
tv_usec: 0
};
unsafe { settimeofday(&now, core::ptr::null_mut()) };
board.set_rtc_time(&parsed.to_utc())?;
anyhow::Ok(None)
}
fn get_live_moisture(
_request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
let mut board = BOARD_ACCESS.lock().unwrap();
let mut a: Vec<u8> = Vec::new();
let mut b: Vec<u8> = Vec::new();
for plant in 0..8 {
let a_hz = board.measure_moisture_hz(plant, crate::plant_hal::Sensor::A)?;
let b_hz = board.measure_moisture_hz(plant, crate::plant_hal::Sensor::B)?;
let a_pct = map_range_moisture(a_hz as f32);
match a_pct {
Ok(result) => {
a.push(result);
}
Err(_) => {
a.push(200);
}
}
let b_pct = map_range_moisture(b_hz as f32);
match b_pct {
Ok(result) => {
b.push(result);
}
Err(_) => {
b.push(200);
}
}
}
let data = Moistures {
moisture_a: a,
moisture_b: b,
};
let json = serde_json::to_string(&data)?;
anyhow::Ok(Some(json))
}
fn get_data(
_request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
let mut board = BOARD_ACCESS.lock().unwrap();
let native = board
.time()
.and_then(|t| Ok(t.to_rfc3339()))
.unwrap_or("error".to_string());
let rtc = board
.get_rtc_time()
.and_then(|t| Ok(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_config(
_request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
let mut board = BOARD_ACCESS.lock().unwrap();
let json = match board.get_config() {
Ok(config) => serde_json::to_string(&config)?,
Err(_) => serde_json::to_string(&PlantControllerConfig::default())?,
};
anyhow::Ok(Some(json))
}
fn backup_config(
request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
let all = read_up_to_bytes_from_request(request, Some(3072))?;
let mut board = BOARD_ACCESS.lock().unwrap();
board.backup_config(&all)?;
anyhow::Ok(Some("saved".to_owned()))
}
fn get_backup_config(
_request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
let mut board = BOARD_ACCESS.lock().unwrap();
let json = match board.get_backup_config() {
Ok(config) => std::str::from_utf8(&config)?.to_owned(),
Err(err) => {
println!("Error get backup config {:?}", err);
err.to_string()
}
};
anyhow::Ok(Some(json))
}
fn backup_info(
_request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
let mut board = BOARD_ACCESS.lock().unwrap();
let header = board.get_backup_info();
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(_) => "{\"error\":\"Header could not be parsed\"".to_owned()
};
anyhow::Ok(Some(json))
}
fn set_config(
request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
let all = read_up_to_bytes_from_request(request, Some(3072))?;
let config: PlantControllerConfig = serde_json::from_slice(&all)?;
let mut board = BOARD_ACCESS.lock().unwrap();
board.set_config(&config)?;
anyhow::Ok(Some("saved".to_owned()))
}
fn get_battery_state(
_request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
let mut board = BOARD_ACCESS.lock().unwrap();
let battery_state = board.get_battery_state();
let battery_json = serde_json::to_string(&battery_state)?;
anyhow::Ok(Some(battery_json))
}
fn get_log(
_request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
let output = crate::log::get_log();
anyhow::Ok(Some(serde_json::to_string(&output)?))
}
fn get_log_localization_config() -> Result<std::string::String, anyhow::Error> {
anyhow::Ok(serde_json::to_string(&LogMessage::to_log_localisation_config())?)
}
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(
request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
let actual_data = read_up_to_bytes_from_request(request, None)?;
let pump_test: TestPump = serde_json::from_slice(&actual_data)?;
let mut board = BOARD_ACCESS.lock().unwrap();
board.test_pump(pump_test.pump)?;
anyhow::Ok(None)
}
fn wifi_scan(
_request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
let mut board = BOARD_ACCESS.lock().unwrap();
let scan_result = board.wifi_scan()?;
let mut ssids: Vec<&String<32>> = Vec::new();
scan_result.iter().for_each(|s| ssids.push(&s.ssid));
let ssid_json = serde_json::to_string(&SSIDList { ssids })?;
println!("Sending ssid list {}", &ssid_json);
anyhow::Ok(Some(ssid_json))
}
fn list_files(
request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
let filename = query_param(request.uri(), "filename").unwrap_or_default();
let board = BOARD_ACCESS.lock().unwrap();
let result = board.list_files(&filename);
let file_list_json = serde_json::to_string(&result)?;
return anyhow::Ok(Some(file_list_json));
}
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()?;
println!("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;
println!("received {read} bytes ota {total_read}");
let to_write = &buffer[0..read];
let iter = (total_read/1024)%8;
if iter != lastiter {
for i in 0..PLANT_COUNT {
board.fault(i, iter==i);
}
lastiter = iter;
}
ota.write(to_write)?;
println!("wrote {read} bytes ota {total_read}");
if read == 0 {
break;
}
}
println!("finish ota");
let partition = ota.raw_partition();
println!("finalizing and changing boot partition to {partition:?}");
let mut finalizer = ota.finalize()?;
println!("changing boot partition");
board.set_restart_to_conf(true);
drop(board);
finalizer.set_as_boot_partition()?;
finalizer.restart();
}
fn flash_bq(filename: &str, dryrun: bool) -> anyhow::Result<()> {
let mut board = BOARD_ACCESS.lock().unwrap();
let mut toggle = true;
let delay = Delay::new(1);
let file_handle = board.get_file_handle(filename, false)?;
let mut reader = std::io::BufRead::lines(std::io::BufReader::with_capacity(512, file_handle));
let mut line = 0;
loop {
board.general_fault(toggle);
toggle = !toggle;
delay.delay_us(2);
line += 1;
match reader.next() {
Some(next) => {
let input = next?;
println!("flashing bq34z100 dryrun:{dryrun} line {line} payload: {input}");
match board.flash_bq34_z100(&input, dryrun) {
Ok(_) => {
println!("ok")
}
Err(err) => {
bail!(
"Error flashing bq34z100 in dryrun: {dryrun} line: {line} error: {err}"
)
}
}
}
None => break,
}
}
println!("Finished flashing file {line} lines processed");
board.general_fault(false);
return anyhow::Ok(());
}
fn query_param(uri: &str, param_name: &str) -> Option<std::string::String> {
println!("{uri} get {param_name}");
let parsed = Url::parse(&format!("http://127.0.0.1/{uri}")).unwrap();
let value = parsed.query_pairs().filter(|it| it.0 == param_name).next();
match value {
Some(found) => {
return Some(found.1.into_owned());
}
None => return None,
}
}
pub fn httpd(reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
let server_config = Configuration {
stack_size: 32768,
..Default::default()
};
let mut server: Box<EspHttpServer<'static>> =
Box::new(EspHttpServer::new(&server_config).unwrap());
server
.fn_handler("/version", Method::Get, |request| {
handle_error_to500(request, get_version_web)
})
.unwrap();
server
.fn_handler("/log", Method::Get, |request| {
handle_error_to500(request, get_log)
})
.unwrap();
server.fn_handler("/log_localization", Method::Get, |request| {
cors_response(request, 200, &get_log_localization_config().unwrap())
})
.unwrap();
server
.fn_handler("/battery", Method::Get, |request| {
handle_error_to500(request, get_battery_state)
})
.unwrap();
server
.fn_handler("/time", Method::Get, |request| {
handle_error_to500(request, get_data)
})
.unwrap();
server
.fn_handler("/moisture", Method::Get, |request| {
handle_error_to500(request, get_live_moisture)
})
.unwrap();
server
.fn_handler("/time", Method::Post, |request| {
handle_error_to500(request, write_time)
})
.unwrap();
server
.fn_handler("/pumptest", Method::Post, |request| {
handle_error_to500(request, pump_test)
})
.unwrap();
server
.fn_handler("/boardtest", Method::Post, move |_| {
BOARD_ACCESS.lock().unwrap().test()
})
.unwrap();
server
.fn_handler("/wifiscan", Method::Post, move |request| {
handle_error_to500(request, wifi_scan)
})
.unwrap();
server
.fn_handler("/ota", Method::Post, |request| {
handle_error_to500(request, ota)
})
.unwrap();
server
.fn_handler("/ota", Method::Options, |request| {
cors_response(request, 200, "")
})
.unwrap();
server
.fn_handler("/get_config", Method::Get, move |request| {
handle_error_to500(request, get_config)
})
.unwrap();
server
.fn_handler("/get_backup_config", Method::Get, move |request| {
handle_error_to500(request, get_backup_config)
})
.unwrap();
server
.fn_handler("/set_config", Method::Post, move |request| {
handle_error_to500(request, set_config)
})
.unwrap();
server
.fn_handler("/backup_config", Method::Post, move |request| {
handle_error_to500(request, backup_config)
})
.unwrap();
server
.fn_handler("/backup_info", Method::Get, move |request| {
handle_error_to500(request, backup_info)
})
.unwrap();
server
.fn_handler("/files", Method::Get, move |request| {
handle_error_to500(request, list_files)
})
.unwrap();
let reboot_now_for_reboot = reboot_now.clone();
server
.fn_handler("/reboot", Method::Post, move |_| {
BOARD_ACCESS
.lock()
.unwrap()
.set_restart_to_conf(true);
reboot_now_for_reboot.store(true, std::sync::atomic::Ordering::Relaxed);
anyhow::Ok(())
})
.unwrap();
unsafe { vTaskDelay(1) };
let reboot_now_for_exit = reboot_now.clone();
server
.fn_handler("/exit", Method::Post, move |_| {
reboot_now_for_exit.store(true, std::sync::atomic::Ordering::Relaxed);
anyhow::Ok(())
})
.unwrap();
server
.fn_handler("/file", Method::Get, move |request| {
let filename = query_param(request.uri(), "filename").unwrap();
let file_handle = BOARD_ACCESS
.lock()
.unwrap()
.get_file_handle(&filename, false);
match file_handle {
Ok(mut file_handle) => {
let headers = [("Access-Control-Allow-Origin", "*")];
let mut response = request.into_response(200, None, &headers)?;
const BUFFER_SIZE: usize = 512;
let mut buffer: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
let mut total_read: usize = 0;
loop {
unsafe { vTaskDelay(1) };
let read = std::io::Read::read(&mut file_handle, &mut buffer)?;
total_read += read;
let to_write = &buffer[0..read];
response.write(to_write)?;
if read == 0 {
break;
}
}
println!("wrote {total_read} for file {filename}");
drop(file_handle);
response.flush()?;
}
Err(err) => {
//todo set headers here for filename to be error
let error_text = err.to_string();
println!("error handling get file {}", error_text);
cors_response(request, 500, &error_text)?;
}
}
anyhow::Ok(())
})
.unwrap();
server
.fn_handler("/file", Method::Post, move |mut request| {
let filename = query_param(request.uri(), "filename").unwrap();
let lock = BOARD_ACCESS
.lock()
.unwrap();
let file_handle =
lock.get_file_handle(&filename, true);
match file_handle {
//TODO get free filesystem size, check against during write if not to large
Ok(mut file_handle) => {
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 iter = (total_read/1024)%8;
if iter != lastiter {
for i in 0..PLANT_COUNT {
lock.fault(i, iter==i);
}
lastiter = iter;
}
let read = request.read(&mut buffer)?;
total_read += read;
let to_write = &buffer[0..read];
std::io::Write::write(&mut file_handle, to_write)?;
if read == 0 {
break;
}
}
cors_response(request, 200, &format!("saved {total_read} bytes"))?;
}
Err(err) => {
//todo set headers here for filename to be error
let error_text = err.to_string();
println!("error handling get file {}", error_text);
cors_response(request, 500, &error_text)?;
}
}
drop(lock);
anyhow::Ok(())
})
.unwrap();
server
.fn_handler("/file", Method::Delete, move |request| {
let filename = query_param(request.uri(), "filename").unwrap();
let copy = filename.clone();
let board = BOARD_ACCESS.lock().unwrap();
match board.delete_file(&filename) {
Ok(_) => {
let info = format!("Deleted file {copy}");
cors_response(request, 200, &info)?;
}
Err(err) => {
let info = format!("Could not delete file {copy} {err:?}");
cors_response(request, 400, &info)?;
}
}
anyhow::Ok(())
})
.unwrap();
server
.fn_handler("/file", Method::Options, |request| {
cors_response(request, 200, "")
})
.unwrap();
server
.fn_handler("/flashbattery", Method::Post, move |request| {
let filename = query_param(request.uri(),"filename").unwrap();
let dryrun = true;
match flash_bq(&filename, false) {
Ok(_) => {
if !dryrun {
match flash_bq(&filename, true) {
Ok(_) => {
cors_response(request, 200, "Sucessfully flashed bq34z100")?;
},
Err(err) => {
let info = format!("Could not flash bq34z100, could be bricked now! {filename} {err:?}");
cors_response(request, 500, &info)?;
},
}
} else {
cors_response(request, 200, "Sucessfully processed bq34z100")?;
}
},
Err(err) => {
let info = format!("Could not process firmware file for, bq34z100, refusing to flash! {filename} {err:?}");
cors_response(request, 500, &info)?;
},
};
anyhow::Ok(())
})
.unwrap();
unsafe { vTaskDelay(1) };
server
.fn_handler("/", Method::Get, move |request| {
let mut response = request.into_ok_response()?;
response.write(include_bytes!("index.html"))?;
anyhow::Ok(())
})
.unwrap();
server
.fn_handler("/favicon.ico", Method::Get, |request| {
request
.into_ok_response()?
.write(include_bytes!("favicon.ico"))?;
anyhow::Ok(())
})
.unwrap();
server
.fn_handler("/bundle.js", Method::Get, |request| {
request
.into_ok_response()?
.write(include_bytes!("bundle.js"))?;
anyhow::Ok(())
})
.unwrap();
server
}
fn cors_response(
request: Request<&mut EspHttpConnection>,
status: u16,
body: &str,
) -> Result<(), anyhow::Error> {
let headers = [
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
];
let mut response = request.into_response(status, None, &headers)?;
response.write(body.as_bytes())?;
response.flush()?;
return anyhow::Ok(());
}
type AnyhowHandler =
fn(&mut Request<&mut EspHttpConnection>) -> Result<Option<std::string::String>, anyhow::Error>;
fn handle_error_to500(
mut request: Request<&mut EspHttpConnection>,
chain: AnyhowHandler,
) -> Result<(), anyhow::Error> {
let error = chain(&mut request);
match error {
Ok(answer) => match answer {
Some(json) => {
cors_response(request, 200, &json)?;
}
None => {
cors_response(request, 200, "")?;
}
},
Err(err) => {
let error_text = err.to_string();
println!("error handling process {}", error_text);
cors_response(request, 500, &error_text)?;
}
}
return anyhow::Ok(());
}
fn read_up_to_bytes_from_request(request: &mut Request<&mut EspHttpConnection<'_>>, limit: Option<usize>) -> Result<Vec<u8>, anyhow::Error> {
let max_read = limit.unwrap_or(1024);
let mut data_store = Vec::new();
let mut total_read = 0;
loop{
let mut buf = [0_u8; 64];
let read = request.read(&mut buf)?;
if read == 0 {
break;
}
let actual_data = &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 allvec = data_store.concat();
println!("Raw data {}", from_utf8(&allvec)?);
Ok(allvec)
}