2 Commits

Author SHA1 Message Date
cfe23c8a09 mqtt via mcutie 2025-09-29 01:00:11 +02:00
3d18b0dbf6 split webserver into submodules 2025-09-28 19:08:43 +02:00
20 changed files with 1235 additions and 1114 deletions

View File

@@ -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>

View File

@@ -159,7 +159,7 @@ option-lock = { version = "0.3.1", default-features = false }
#stay in sync with mcutie version here! #stay in sync with mcutie version here!
heapless = { version = "0.7.17", features = ["serde"] } heapless = { version = "0.7.17", features = ["serde"] }
mcutie = { version = "0.3.0", default-features = false } mcutie = { version = "0.3.0", default-features = false, features = ["log", "homeassistant"] }

View File

@@ -1,7 +1,7 @@
use alloc::string::String;
use core::str::FromStr;
use crate::hal::PLANT_COUNT; use crate::hal::PLANT_COUNT;
use crate::plant_state::PlantWateringMode; use crate::plant_state::PlantWateringMode;
use alloc::string::String;
use core::str::FromStr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
@@ -10,10 +10,10 @@ pub struct NetworkConfig {
pub ap_ssid: heapless::String<32>, pub ap_ssid: heapless::String<32>,
pub ssid: Option<heapless::String<32>>, pub ssid: Option<heapless::String<32>>,
pub password: Option<heapless::String<64>>, pub password: Option<heapless::String<64>>,
pub mqtt_url: Option<heapless::String<128>>, pub mqtt_url: Option<String>,
pub base_topic: Option<heapless::String<64>>, pub base_topic: Option<heapless::String<64>>,
pub mqtt_user: Option<heapless::String<32>>, pub mqtt_user: Option<String>,
pub mqtt_password: Option<heapless::String<64>>, pub mqtt_password: Option<String>,
pub max_wait: u32, pub max_wait: u32,
} }
impl Default for NetworkConfig { impl Default for NetworkConfig {

View File

@@ -1,5 +1,5 @@
use crate::hal::Box;
use crate::fat_error::{FatError, FatResult}; use crate::fat_error::{FatError, FatResult};
use crate::hal::Box;
use alloc::string::String; use alloc::string::String;
use async_trait::async_trait; use async_trait::async_trait;
use bq34z100::{Bq34z100g1, Bq34z100g1Driver, Flags}; use bq34z100::{Bq34z100g1, Bq34z100g1Driver, Flags};
@@ -43,14 +43,6 @@ pub enum BatteryError {
CommunicationError(String), CommunicationError(String),
} }
// impl From<Bq34Z100Error<esp_idf_hal::i2c::I2cError>> for BatteryError {
// fn from(err: Bq34Z100Error<esp_idf_hal::i2c::I2cError>) -> Self {
// BatteryError::CommunicationError(
// anyhow!("failed to communicate with battery monitor: {:?}", err).to_string(),
// )
// }
// }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub enum BatteryState { pub enum BatteryState {
Unknown, Unknown,

View File

@@ -7,26 +7,23 @@ use serde::Serialize;
use crate::fat_error::{ContextExt, FatError, FatResult}; use crate::fat_error::{ContextExt, FatError, FatResult};
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem; use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
use alloc::borrow::ToOwned;
use alloc::string::ToString; use alloc::string::ToString;
use alloc::sync::Arc; use alloc::sync::Arc;
use alloc::{format, string::String, vec::Vec}; use alloc::{format, string::String, vec::Vec};
use core::marker::PhantomData;
use core::net::{IpAddr, Ipv4Addr, SocketAddr}; use core::net::{IpAddr, Ipv4Addr, SocketAddr};
use core::str::FromStr; use core::str::FromStr;
use core::sync::atomic::Ordering; use core::sync::atomic::Ordering;
use core::sync::atomic::Ordering::Relaxed;
use edge_dhcp::io::server::run;
use embassy_executor::Spawner; use embassy_executor::Spawner;
use embassy_net::udp::UdpSocket; use embassy_net::udp::UdpSocket;
use embassy_net::{DhcpConfig, Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4}; use embassy_net::{DhcpConfig, Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4};
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::mutex::{Mutex, MutexGuard}; use embassy_sync::mutex::{Mutex, MutexGuard};
use embassy_sync::once_lock::OnceLock;
use embassy_time::{Duration, Timer}; use embassy_time::{Duration, Timer};
use embedded_storage::nor_flash::ReadNorFlash; use embedded_storage::nor_flash::ReadNorFlash;
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState}; use esp_bootloader_esp_idf::ota::{Ota, OtaImageState};
use esp_bootloader_esp_idf::partitions::FlashRegion; use esp_bootloader_esp_idf::partitions::FlashRegion;
use esp_hal::gpio::{Input, InputConfig, Pull, RtcPinWithResistors}; use esp_hal::gpio::{Input, RtcPinWithResistors};
use esp_hal::rng::Rng; use esp_hal::rng::Rng;
use esp_hal::rtc_cntl::{ use esp_hal::rtc_cntl::{
sleep::{TimerWakeupSource, WakeupLevel}, sleep::{TimerWakeupSource, WakeupLevel},
@@ -37,11 +34,15 @@ use esp_println::println;
use esp_storage::FlashStorage; use esp_storage::FlashStorage;
use esp_wifi::wifi::{ use esp_wifi::wifi::{
AccessPointConfiguration, AccessPointInfo, AuthMethod, ClientConfiguration, Configuration, AccessPointConfiguration, AccessPointInfo, AuthMethod, ClientConfiguration, Configuration,
Interfaces, ScanConfig, ScanTypeConfig, WifiController, WifiDevice, WifiState, ScanConfig, ScanTypeConfig, WifiController, WifiDevice, WifiState,
}; };
use littlefs2::fs::Filesystem; use littlefs2::fs::Filesystem;
use littlefs2_core::{FileType, PathBuf, SeekFrom}; use littlefs2_core::{FileType, PathBuf, SeekFrom};
use log::{info, warn}; use log::{info, warn};
use mcutie::{
Error, McutieBuilder, McutieReceiver, McutieTask, MqttMessage, PublishDisplay, Publishable,
QoS, Topic,
};
use portable_atomic::AtomicBool; use portable_atomic::AtomicBool;
use smoltcp::socket::udp::PacketMetadata; use smoltcp::socket::udp::PacketMetadata;
use smoltcp::wire::DnsQueryType; use smoltcp::wire::DnsQueryType;
@@ -59,6 +60,11 @@ static mut RESTART_TO_CONF: i8 = 0;
const CONFIG_FILE: &str = "config.json"; const CONFIG_FILE: &str = "config.json";
const NTP_SERVER: &str = "pool.ntp.org"; const NTP_SERVER: &str = "pool.ntp.org";
static MQTT_CONNECTED_EVENT_RECEIVED: AtomicBool = AtomicBool::new(false);
static MQTT_ROUND_TRIP_RECEIVED: AtomicBool = AtomicBool::new(false);
pub static MQTT_STAY_ALIVE: AtomicBool = AtomicBool::new(false);
static MQTT_BASE_TOPIC: OnceLock<String> = OnceLock::new();
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct FileInfo { pub struct FileInfo {
filename: String, filename: String,
@@ -72,18 +78,6 @@ pub struct FileList {
files: Vec<FileInfo>, files: Vec<FileInfo>,
} }
pub struct FileSystemSizeInfo {
pub total_size: usize,
pub used_size: usize,
pub free_size: usize,
}
pub struct MqttClient<'a> {
dummy: PhantomData<&'a ()>,
//mqtt_client: EspMqttClient<'a>,
base_topic: String,
}
#[derive(Copy, Clone, Default)] #[derive(Copy, Clone, Default)]
struct Timestamp { struct Timestamp {
stamp: DateTime<Utc>, stamp: DateTime<Utc>,
@@ -127,9 +121,6 @@ pub struct Esp<'a> {
pub interface_ap: Option<WifiDevice<'static>>, pub interface_ap: Option<WifiDevice<'static>>,
pub controller: Arc<Mutex<CriticalSectionRawMutex, WifiController<'static>>>, pub controller: Arc<Mutex<CriticalSectionRawMutex, WifiController<'static>>>,
//only filled, if a useable mqtt client with working roundtrip could be established
pub(crate) mqtt_client: Option<MqttClient<'a>>,
pub boot_button: Input<'a>, pub boot_button: Input<'a>,
// RTC-capable GPIO used as external wake source (store the raw peripheral) // RTC-capable GPIO used as external wake source (store the raw peripheral)
@@ -147,12 +138,6 @@ pub struct Esp<'a> {
// CPU cores/threads, reconsider this. // CPU cores/threads, reconsider this.
unsafe impl Send for Esp<'_> {} unsafe impl Send for Esp<'_> {}
pub struct IpInfo {
pub(crate) ip: IpAddr,
netmask: IpAddr,
gateway: IpAddr,
}
macro_rules! mk_static { macro_rules! mk_static {
($t:ty,$val:expr) => {{ ($t:ty,$val:expr) => {{
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new(); static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
@@ -164,7 +149,7 @@ macro_rules! mk_static {
impl Esp<'_> { impl Esp<'_> {
pub(crate) async fn delete_file(&self, filename: String) -> FatResult<()> { pub(crate) async fn delete_file(&self, filename: String) -> FatResult<()> {
let file = PathBuf::try_from(filename.as_str()).unwrap(); let file = PathBuf::try_from(filename.as_str())?;
let access = self.fs.lock().await; let access = self.fs.lock().await;
access.remove(&*file)?; access.remove(&*file)?;
Ok(()) Ok(())
@@ -383,7 +368,7 @@ impl Esp<'_> {
} }
} }
pub(crate) async fn wifi_ap(&mut self, fallback: bool) -> FatResult<Stack<'static>> { pub(crate) async fn wifi_ap(&mut self) -> FatResult<Stack<'static>> {
let ssid = match self.load_config().await { let ssid = match self.load_config().await {
Ok(config) => config.network.ap_ssid.as_str().to_string(), Ok(config) => config.network.ap_ssid.as_str().to_string(),
Err(_) => "PlantCtrl Emergency Mode".to_string(), Err(_) => "PlantCtrl Emergency Mode".to_string(),
@@ -482,7 +467,7 @@ impl Esp<'_> {
let (stack, runner) = embassy_net::new( let (stack, runner) = embassy_net::new(
device, device,
config, config,
mk_static!(StackResources<4>, StackResources::<4>::new()), mk_static!(StackResources<8>, StackResources::<8>::new()),
seed, seed,
); );
let stack = mk_static!(Stack, stack); let stack = mk_static!(Stack, stack);
@@ -572,6 +557,8 @@ impl Esp<'_> {
} }
Timer::after(Duration::from_millis(100)).await Timer::after(Duration::from_millis(100)).await
} }
info!("Connected WIFI, dhcp: {:?}", stack.config_v4());
Ok(stack.clone()) Ok(stack.clone())
} }
@@ -602,9 +589,6 @@ impl Esp<'_> {
let ext1 = esp_hal::rtc_cntl::sleep::Ext1WakeupSource::new(&mut wake_pins); let ext1 = esp_hal::rtc_cntl::sleep::Ext1WakeupSource::new(&mut wake_pins);
rtc.sleep_deep(&[&timer, &ext1]); rtc.sleep_deep(&[&timer, &ext1]);
} }
// We should never reach here because sleep_deep never returns, but just in case, reset.
software_reset();
} }
pub(crate) async fn load_config(&mut self) -> FatResult<PlantControllerConfig> { pub(crate) async fn load_config(&mut self) -> FatResult<PlantControllerConfig> {
@@ -708,7 +692,11 @@ impl Esp<'_> {
} }
} }
pub(crate) async fn mqtt(&mut self, network_config: &NetworkConfig) -> FatResult<()> { pub(crate) async fn mqtt(
&mut self,
network_config: &'static NetworkConfig,
stack: Stack<'static>,
) -> FatResult<()> {
let base_topic = network_config let base_topic = network_config
.base_topic .base_topic
.as_ref() .as_ref()
@@ -716,7 +704,12 @@ impl Esp<'_> {
if base_topic.is_empty() { if base_topic.is_empty() {
bail!("Mqtt base_topic was empty") bail!("Mqtt base_topic was empty")
} }
let _base_topic_copy = base_topic.clone(); MQTT_BASE_TOPIC
.init(base_topic.to_string())
.map_err(|_| FatError::String {
error: "Error setting basetopic".to_string(),
})?;
let mqtt_url = network_config let mqtt_url = network_config
.mqtt_url .mqtt_url
.as_ref() .as_ref()
@@ -725,202 +718,223 @@ impl Esp<'_> {
bail!("Mqtt url was empty") bail!("Mqtt url was empty")
} }
bail!("todo"); let last_will_topic = format!("{}/state", base_topic);
// let round_trip_topic = format!("{}/internal/roundtrip", base_topic);
// let last_will_topic = format!("{}/state", base_topic); let stay_alive_topic = format!("{}/stay_alive", base_topic);
// let mqtt_client_config = MqttClientConfiguration {
// lwt: Some(LwtConfiguration { let mut builder: McutieBuilder<'_, String, PublishDisplay<String, &str>, 0> =
// topic: &last_will_topic, McutieBuilder::new(stack, "plant ctrl", mqtt_url);
// payload: "lost".as_bytes(), if network_config.mqtt_user.is_some() && network_config.mqtt_password.is_some() {
// qos: AtLeastOnce, builder = builder.with_authentication(
// retain: true, network_config.mqtt_user.as_ref().unwrap().as_str(),
// }), network_config.mqtt_password.as_ref().unwrap().as_str(),
// client_id: Some("plantctrl"), );
// keep_alive_interval: Some(Duration::from_secs(60 * 60 * 2)), info!("With authentification");
// username: network_config.mqtt_user.as_ref().map(|v| &**v),
// password: network_config.mqtt_password.as_ref().map(|v| &**v),
// //room for improvement
// ..Default::default()
// };
//
// let mqtt_connected_event_received = Arc::new(AtomicBool::new(false));
// let mqtt_connected_event_ok = Arc::new(AtomicBool::new(false));
//
// let round_trip_ok = Arc::new(AtomicBool::new(false));
// let round_trip_topic = format!("{}/internal/roundtrip", base_topic);
// let stay_alive_topic = format!("{}/stay_alive", base_topic);
// log(LogMessage::StayAlive, 0, 0, "", &stay_alive_topic);
//
// let mqtt_connected_event_received_copy = mqtt_connected_event_received.clone();
// let mqtt_connected_event_ok_copy = mqtt_connected_event_ok.clone();
// let stay_alive_topic_copy = stay_alive_topic.clone();
// let round_trip_topic_copy = round_trip_topic.clone();
// let round_trip_ok_copy = round_trip_ok.clone();
// let client_id = mqtt_client_config.client_id.unwrap_or("not set");
// log(LogMessage::MqttInfo, 0, 0, client_id, mqtt_url);
// let mut client = EspMqttClient::new_cb(mqtt_url, &mqtt_client_config, move |event| {
// let payload = event.payload();
// match payload {
// embedded_svc::mqtt::client::EventPayload::Received {
// id: _,
// topic,
// data,
// details: _,
// } => {
// let data = String::from_utf8_lossy(data);
// if let Some(topic) = topic {
// //todo use enums
// if topic.eq(round_trip_topic_copy.as_str()) {
// round_trip_ok_copy.store(true, std::sync::atomic::Ordering::Relaxed);
// } else if topic.eq(stay_alive_topic_copy.as_str()) {
// let value =
// data.eq_ignore_ascii_case("true") || data.eq_ignore_ascii_case("1");
// log(LogMessage::MqttStayAliveRec, 0, 0, &data, "");
// STAY_ALIVE.store(value, std::sync::atomic::Ordering::Relaxed);
// } else {
// log(LogMessage::UnknownTopic, 0, 0, "", topic);
// }
// }
// }
// esp_idf_svc::mqtt::client::EventPayload::Connected(_) => {
// mqtt_connected_event_received_copy
// .store(true, std::sync::atomic::Ordering::Relaxed);
// mqtt_connected_event_ok_copy.store(true, std::sync::atomic::Ordering::Relaxed);
// log::info!("Mqtt connected");
// }
// esp_idf_svc::mqtt::client::EventPayload::Disconnected => {
// mqtt_connected_event_received_copy
// .store(true, std::sync::atomic::Ordering::Relaxed);
// mqtt_connected_event_ok_copy.store(false, std::sync::atomic::Ordering::Relaxed);
// log::info!("Mqtt disconnected");
// }
// esp_idf_svc::mqtt::client::EventPayload::Error(esp_error) => {
// log::info!("EspMqttError reported {:?}", esp_error);
// mqtt_connected_event_received_copy
// .store(true, std::sync::atomic::Ordering::Relaxed);
// mqtt_connected_event_ok_copy.store(false, std::sync::atomic::Ordering::Relaxed);
// log::info!("Mqtt error");
// }
// esp_idf_svc::mqtt::client::EventPayload::BeforeConnect => {
// log::info!("Mqtt before connect")
// }
// esp_idf_svc::mqtt::client::EventPayload::Subscribed(_) => {
// log::info!("Mqtt subscribed")
// }
// esp_idf_svc::mqtt::client::EventPayload::Unsubscribed(_) => {
// log::info!("Mqtt unsubscribed")
// }
// esp_idf_svc::mqtt::client::EventPayload::Published(_) => {
// log::info!("Mqtt published")
// }
// esp_idf_svc::mqtt::client::EventPayload::Deleted(_) => {
// log::info!("Mqtt deleted")
// }
// }
// })?;
//
// let mut wait_for_connections_event = 0;
// while wait_for_connections_event < 100 {
// wait_for_connections_event += 1;
// match mqtt_connected_event_received.load(std::sync::atomic::Ordering::Relaxed) {
// true => {
// log::info!("Mqtt connection callback received, progressing");
// match mqtt_connected_event_ok.load(std::sync::atomic::Ordering::Relaxed) {
// true => {
// log::info!(
// "Mqtt did callback as connected, testing with roundtrip now"
// );
// //subscribe to roundtrip
// client.subscribe(round_trip_topic.as_str(), ExactlyOnce)?;
// client.subscribe(stay_alive_topic.as_str(), ExactlyOnce)?;
// //publish to roundtrip
// client.publish(
// round_trip_topic.as_str(),
// ExactlyOnce,
// false,
// "online_test".as_bytes(),
// )?;
//
// let mut wait_for_roundtrip = 0;
// while wait_for_roundtrip < 100 {
// wait_for_roundtrip += 1;
// match round_trip_ok.load(std::sync::atomic::Ordering::Relaxed) {
// true => {
// log::info!("Round trip registered, proceeding");
// self.mqtt_client = Some(MqttClient {
// mqtt_client: client,
// base_topic: base_topic_copy,
// });
// return anyhow::Ok(());
// }
// false => {
// unsafe { vTaskDelay(10) };
// }
// }
// }
// bail!("Mqtt did not complete roundtrip in time");
// }
// false => {
// bail!("Mqtt did respond but with failure")
// }
// }
// }
// false => {
// unsafe { vTaskDelay(10) };
// }
// }
// }
// bail!("Mqtt did not fire connection callback in time");
} }
pub(crate) async fn mqtt_publish(&mut self, _subtopic: &str, _message: &[u8]) -> FatResult<()> {
bail!("todo"); let lwt = Topic::General(last_will_topic);
// let lwt = mk_static!(Topic<String>, lwt);
// if self.mqtt_client.is_none() { let lwt = lwt.with_display("lost").retain(true).qos(QoS::AtLeastOnce);
// return anyhow::Ok(()); builder = builder.with_last_will(lwt);
// } //TODO make configurable
// if !subtopic.starts_with("/") { builder = builder.with_device_id("plantctrl");
// log::info!("Subtopic without / at start {}", subtopic);
// bail!("Subtopic without / at start {}", subtopic); let builder: McutieBuilder<'_, String, PublishDisplay<String, &str>, 2> = builder
// } .with_subscriptions([
// if subtopic.len() > 192 { Topic::General(round_trip_topic.clone()),
// log::info!("Subtopic exceeds 192 chars {}", subtopic); Topic::General(stay_alive_topic.clone()),
// bail!("Subtopic exceeds 192 chars {}", subtopic); ]);
// }
// let client = self.mqtt_client.as_mut().unwrap(); let keep_alive = Duration::from_secs(60 * 60 * 2).as_secs() as u16;
// let mut full_topic: heapless::String<256> = heapless::String::new(); let (receiver, task) = builder.build(keep_alive);
// if full_topic.push_str(client.base_topic.as_str()).is_err() {
// log::info!("Some error assembling full_topic 1"); let spawner = Spawner::for_current_executor().await;
// bail!("Some error assembling full_topic 1") spawner.spawn(mqtt_incoming_task(
// }; receiver,
// if full_topic.push_str(subtopic).is_err() { round_trip_topic.clone(),
// log::info!("Some error assembling full_topic 2"); stay_alive_topic.clone(),
// bail!("Some error assembling full_topic 2") ))?;
// }; spawner.spawn(mqtt_runner(task))?;
// let publish = client
// .mqtt_client LOG_ACCESS
// .publish(&full_topic, ExactlyOnce, true, message); .lock()
// Timer::after_millis(10).await; .await
// match publish { .log(LogMessage::StayAlive, 0, 0, "", &stay_alive_topic)
// OkStd(message_id) => { .await;
// log::info!(
// "Published mqtt topic {} with message {:#?} msgid is {:?}", LOG_ACCESS
// full_topic, .lock()
// String::from_utf8_lossy(message), .await
// message_id .log(LogMessage::MqttInfo, 0, 0, "", mqtt_url)
// ); .await;
// anyhow::Ok(())
// } let mqtt_timeout = 15000;
// Err(err) => { let timeout = {
// log::info!( let guard = TIME_ACCESS.get().await.lock().await;
// "Error during mqtt send on topic {} with message {:#?} error is {:?}", guard.current_time_us()
// full_topic, } + mqtt_timeout as u64 * 1000;
// String::from_utf8_lossy(message), while !MQTT_CONNECTED_EVENT_RECEIVED.load(Ordering::Relaxed) {
// err let cur = TIME_ACCESS.get().await.lock().await.current_time_us();
// ); if cur > timeout {
// Err(err)? bail!("Timeout waiting MQTT connect event")
// }
// }
} }
Timer::after(Duration::from_millis(100)).await;
}
Topic::General(round_trip_topic.clone())
.with_display("online_text")
.publish()
.await
.unwrap();
let timeout = {
let guard = TIME_ACCESS.get().await.lock().await;
guard.current_time_us()
} + mqtt_timeout as u64 * 1000;
while !MQTT_ROUND_TRIP_RECEIVED.load(Ordering::Relaxed) {
let cur = TIME_ACCESS.get().await.lock().await.current_time_us();
if cur > timeout {
//ensure we do not further try to publish
MQTT_CONNECTED_EVENT_RECEIVED.store(false, Ordering::Relaxed);
bail!("Timeout waiting MQTT roundtrip")
}
Timer::after(Duration::from_millis(100)).await;
}
Ok(())
}
pub(crate) async fn mqtt_inner(&mut self, subtopic: &str, message: &str) -> FatResult<()> {
if !subtopic.starts_with("/") {
bail!("Subtopic without / at start {}", subtopic);
}
if subtopic.len() > 192 {
bail!("Subtopic exceeds 192 chars {}", subtopic);
}
let base_topic = MQTT_BASE_TOPIC
.try_get()
.context("missing base topic in static!")?;
let full_topic = format!("{base_topic}{subtopic}");
loop {
let result = Topic::General(full_topic.as_str())
.with_display(message)
.retain(true)
.publish()
.await;
match result {
Ok(()) => return Ok(()),
Err(err) => {
let retry = match err {
Error::IOError => false,
Error::TimedOut => true,
Error::TooLarge => false,
Error::PacketError => false,
Error::Invalid => false,
};
if !retry {
bail!(
"Error during mqtt send on topic {} with message {:#?} error is {:?}",
&full_topic,
message,
err
);
}
info!(
"Retransmit for {} with message {:#?} error is {:?} retrying {}",
&full_topic, message, err, retry
);
Timer::after(Duration::from_millis(100)).await;
}
}
}
}
pub(crate) async fn mqtt_publish(&mut self, subtopic: &str, message: &str) {
let online = MQTT_CONNECTED_EVENT_RECEIVED.load(Ordering::Relaxed);
if !online {
return;
}
let roundtrip_ok = MQTT_ROUND_TRIP_RECEIVED.load(Ordering::Relaxed);
if !roundtrip_ok {
info!("MQTT roundtrip not received yet, dropping message");
return;
}
match self.mqtt_inner(subtopic, message).await {
Ok(()) => {}
Err(err) => {
info!(
"Error during mqtt send on topic {} with message {:#?} error is {:?}",
subtopic, message, err
);
}
};
}
}
#[embassy_executor::task]
async fn mqtt_runner(
task: McutieTask<'static, String, PublishDisplay<'static, String, &'static str>, 2>,
) {
task.run().await;
}
#[embassy_executor::task]
async fn mqtt_incoming_task(
receiver: McutieReceiver,
round_trip_topic: String,
stay_alive_topic: String,
) {
loop {
let message = receiver.receive().await;
match message {
MqttMessage::Connected => {
info!("Mqtt connected");
MQTT_CONNECTED_EVENT_RECEIVED.store(true, Ordering::Relaxed);
}
MqttMessage::Publish(topic, payload) => match topic {
Topic::DeviceType(type_topic) => {}
Topic::Device(device_topic) => {}
Topic::General(topic) => {
let subtopic = topic.as_str();
if subtopic.eq(round_trip_topic.as_str()) {
MQTT_ROUND_TRIP_RECEIVED.store(true, Ordering::Relaxed);
} else if subtopic.eq(stay_alive_topic.as_str()) {
let value = payload.eq_ignore_ascii_case("true".as_ref())
|| payload.eq_ignore_ascii_case("1".as_ref());
let a = match value {
true => 1,
false => 0,
};
LOG_ACCESS
.lock()
.await
.log(LogMessage::MqttStayAliveRec, a, 0, "", "")
.await;
MQTT_STAY_ALIVE.store(value, Ordering::Relaxed);
} else {
LOG_ACCESS
.lock()
.await
.log(LogMessage::UnknownTopic, 0, 0, "", &*topic)
.await;
}
}
},
MqttMessage::Disconnected => {
MQTT_CONNECTED_EVENT_RECEIVED.store(false, Ordering::Relaxed);
info!("Mqtt disconnected");
}
MqttMessage::HomeAssistantOnline => {
info!("Home assistant is online");
}
}
}
}
#[embassy_executor::task(pool_size = 2)]
async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) {
runner.run().await;
} }
#[embassy_executor::task] #[embassy_executor::task]
@@ -943,7 +957,7 @@ async fn run_dhcp(stack: Stack<'static>, gw_ip_addr: &'static str) {
let buffers = UdpBuffers::<3, 1024, 1024, 10>::new(); let buffers = UdpBuffers::<3, 1024, 1024, 10>::new();
let unbound_socket = Udp::new(stack, &buffers); let unbound_socket = Udp::new(stack, &buffers);
let mut bound_socket = unbound_socket let mut bound_socket = unbound_socket
.bind(core::net::SocketAddr::V4(SocketAddrV4::new( .bind(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::UNSPECIFIED, Ipv4Addr::UNSPECIFIED,
DEFAULT_SERVER_PORT, DEFAULT_SERVER_PORT,
))) )))
@@ -962,8 +976,3 @@ async fn run_dhcp(stack: Stack<'static>, gw_ip_addr: &'static str) {
Timer::after(Duration::from_millis(500)).await; Timer::after(Duration::from_millis(500)).await;
} }
} }
#[embassy_executor::task(pool_size = 2)]
async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) {
runner.run().await
}

View File

@@ -4,6 +4,7 @@ mod initial_hal;
mod little_fs2storage_adapter; mod little_fs2storage_adapter;
pub(crate) mod rtc; pub(crate) mod rtc;
mod v3_hal; mod v3_hal;
mod v3_shift_register;
mod v4_hal; mod v4_hal;
mod v4_sensor; mod v4_sensor;
mod water; mod water;
@@ -68,11 +69,8 @@ use eeprom24x::page_size::B32;
use eeprom24x::unique_serial::No; use eeprom24x::unique_serial::No;
use eeprom24x::{Eeprom24x, SlaveAddr, Storage}; use eeprom24x::{Eeprom24x, SlaveAddr, Storage};
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice; use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embassy_executor::Spawner;
use embassy_sync::blocking_mutex::CriticalSectionMutex;
//use battery::BQ34Z100G1;
//use bq34z100::Bq34z100g1Driver;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::blocking_mutex::CriticalSectionMutex;
use esp_bootloader_esp_idf::partitions::{ use esp_bootloader_esp_idf::partitions::{
AppPartitionSubType, DataPartitionSubType, FlashRegion, PartitionEntry, AppPartitionSubType, DataPartitionSubType, FlashRegion, PartitionEntry,
}; };
@@ -218,12 +216,8 @@ macro_rules! mk_static {
}}; }};
} }
const GW_IP_ADDR_ENV: Option<&'static str> = option_env!("GATEWAY_IP");
impl PlantHal { impl PlantHal {
pub async fn create( pub async fn create() -> Result<Mutex<CriticalSectionRawMutex, HAL<'static>>, FatError> {
spawner: Spawner,
) -> Result<Mutex<CriticalSectionRawMutex, HAL<'static>>, FatError> {
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals: Peripherals = esp_hal::init(config); let peripherals: Peripherals = esp_hal::init(config);
@@ -394,7 +388,6 @@ impl PlantHal {
interface_ap: Some(ap), interface_ap: Some(ap),
boot_button, boot_button,
wake_gpio1, wake_gpio1,
mqtt_client: None,
ota, ota,
ota_next, ota_next,
}; };
@@ -548,9 +541,6 @@ impl PlantHal {
v4_hal::create_v4(free_pins, esp, config, battery_interaction, rtc_module) v4_hal::create_v4(free_pins, esp, config, battery_interaction, rtc_module)
.await? .await?
} }
_ => {
bail!("Unknown board version");
}
}; };
HAL { board_hal } HAL { board_hal }

View File

@@ -2,10 +2,10 @@ use crate::bail;
use crate::fat_error::FatError; use crate::fat_error::FatError;
use crate::hal::esp::{hold_disable, hold_enable}; use crate::hal::esp::{hold_disable, hold_enable};
use crate::hal::rtc::RTCModuleInteraction; use crate::hal::rtc::RTCModuleInteraction;
use crate::hal::v3_shift_register::ShiftRegister40;
use crate::hal::water::TankSensor; use crate::hal::water::TankSensor;
use crate::hal::{BoardInteraction, FreePeripherals, Sensor, PLANT_COUNT, TIME_ACCESS}; use crate::hal::{BoardInteraction, FreePeripherals, Sensor, PLANT_COUNT, TIME_ACCESS};
use crate::log::{LogMessage, LOG_ACCESS}; use crate::log::{LogMessage, LOG_ACCESS};
use crate::sipo::ShiftRegister40;
use crate::{ use crate::{
config::PlantControllerConfig, config::PlantControllerConfig,
hal::{battery::BatteryInteraction, esp::Esp}, hal::{battery::BatteryInteraction, esp::Esp},
@@ -15,7 +15,6 @@ use alloc::format;
use alloc::string::ToString; use alloc::string::ToString;
use async_trait::async_trait; use async_trait::async_trait;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::blocking_mutex::CriticalSectionMutex;
use embassy_sync::mutex::Mutex; use embassy_sync::mutex::Mutex;
use embassy_time::Timer; use embassy_time::Timer;
use embedded_hal::digital::OutputPin as _; use embedded_hal::digital::OutputPin as _;
@@ -267,7 +266,7 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
//self.signal_counter.counter_clear()?; //self.signal_counter.counter_clear()?;
//Disable all //Disable all
{ {
let mut shift_register = self.shift_register.lock().await; let shift_register = self.shift_register.lock().await;
shift_register.decompose()[MS_4].set_high()?; shift_register.decompose()[MS_4].set_high()?;
} }
@@ -298,7 +297,7 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
let is_bit_set = |b: u8| -> bool { sensor_channel & (1 << b) != 0 }; let is_bit_set = |b: u8| -> bool { sensor_channel & (1 << b) != 0 };
{ {
let mut shift_register = self.shift_register.lock().await; let shift_register = self.shift_register.lock().await;
let pin_0 = &mut shift_register.decompose()[MS_0]; let pin_0 = &mut shift_register.decompose()[MS_0];
let pin_1 = &mut shift_register.decompose()[MS_1]; let pin_1 = &mut shift_register.decompose()[MS_1];
let pin_2 = &mut shift_register.decompose()[MS_2]; let pin_2 = &mut shift_register.decompose()[MS_2];
@@ -336,7 +335,7 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
Timer::after_millis(measurement).await; Timer::after_millis(measurement).await;
//self.signal_counter.counter_pause()?; //self.signal_counter.counter_pause()?;
{ {
let mut shift_register = self.shift_register.lock().await; let shift_register = self.shift_register.lock().await;
shift_register.decompose()[MS_4].set_high()?; shift_register.decompose()[MS_4].set_high()?;
shift_register.decompose()[SENSOR_ON].set_low()?; shift_register.decompose()[SENSOR_ON].set_low()?;
} }

View File

@@ -1,5 +1,5 @@
//! Serial-in parallel-out shift register //! Serial-in parallel-out shift register
#![allow(warnings)]
use core::cell::RefCell; use core::cell::RefCell;
use core::convert::Infallible; use core::convert::Infallible;
use core::iter::Iterator; use core::iter::Iterator;

View File

@@ -10,7 +10,6 @@ use async_trait::async_trait;
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice; use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_time::Timer; use embassy_time::Timer;
use esp_hal::analog::adc::{Adc, AdcConfig, Attenuation};
use esp_hal::{twai, Blocking}; use esp_hal::{twai, Blocking};
//use embedded_hal_bus::i2c::MutexDevice; //use embedded_hal_bus::i2c::MutexDevice;
use crate::bail; use crate::bail;
@@ -137,10 +136,6 @@ pub struct V4<'a> {
extra2: Output<'a>, extra2: Output<'a>,
} }
struct InputOutput<'a> {
pin: Flex<'a>,
}
pub(crate) async fn create_v4( pub(crate) async fn create_v4(
peripherals: FreePeripherals<'static>, peripherals: FreePeripherals<'static>,
esp: Esp<'static>, esp: Esp<'static>,
@@ -346,7 +341,7 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
async fn deep_sleep(&mut self, duration_in_ms: u64) -> ! { async fn deep_sleep(&mut self, duration_in_ms: u64) -> ! {
self.awake.set_low(); self.awake.set_low();
//self.charger.power_save(); self.charger.power_save();
let rtc = TIME_ACCESS.get().await.lock().await; let rtc = TIME_ACCESS.get().await.lock().await;
self.esp.deep_sleep(duration_in_ms, rtc); self.esp.deep_sleep(duration_in_ms, rtc);
} }

View File

@@ -1,6 +1,6 @@
use crate::bail; use crate::bail;
use crate::hal::{ADC1, TANK_MULTI_SAMPLE};
use crate::fat_error::FatError; use crate::fat_error::FatError;
use crate::hal::{ADC1, TANK_MULTI_SAMPLE};
use embassy_time::Timer; use embassy_time::Timer;
use esp_hal::analog::adc::{Adc, AdcConfig, AdcPin, Attenuation}; use esp_hal::analog::adc::{Adc, AdcConfig, AdcPin, Attenuation};
use esp_hal::delay::Delay; use esp_hal::delay::Delay;
@@ -9,7 +9,6 @@ use esp_hal::pcnt::unit::Unit;
use esp_hal::peripherals::GPIO5; use esp_hal::peripherals::GPIO5;
use esp_hal::Blocking; use esp_hal::Blocking;
use esp_println::println; use esp_println::println;
use littlefs2::object_safe::DynStorage;
use onewire::{ds18b20, Device, DeviceSearch, OneWire, DS18B20}; use onewire::{ds18b20, Device, DeviceSearch, OneWire, DS18B20};
pub struct TankSensor<'a> { pub struct TankSensor<'a> {
@@ -34,7 +33,7 @@ impl<'a> TankSensor<'a> {
let mut adc1_config = AdcConfig::new(); let mut adc1_config = AdcConfig::new();
let tank_pin = adc1_config.enable_pin(gpio5, Attenuation::_11dB); let tank_pin = adc1_config.enable_pin(gpio5, Attenuation::_11dB);
let mut tank_channel = Adc::new(adc1, adc1_config); let tank_channel = Adc::new(adc1, adc1_config);
let one_wire_bus = OneWire::new(one_wire_pin, false); let one_wire_bus = OneWire::new(one_wire_pin, false);
@@ -154,9 +153,7 @@ impl<'a> TankSensor<'a> {
let mut store = [0_u16; TANK_MULTI_SAMPLE]; let mut store = [0_u16; TANK_MULTI_SAMPLE];
for multisample in 0..TANK_MULTI_SAMPLE { for multisample in 0..TANK_MULTI_SAMPLE {
let mut asy = (&mut self.tank_channel); let value = self.tank_channel.read_oneshot(&mut self.tank_pin);
let value = asy.read_oneshot(&mut self.tank_pin);
//force yield //force yield
Timer::after_millis(10).await; Timer::after_millis(10).await;
store[multisample] = value.unwrap(); store[multisample] = value.unwrap();

View File

@@ -111,7 +111,10 @@ impl LogArray {
limit_length(txt_short, &mut txt_short_stack); limit_length(txt_short, &mut txt_short_stack);
limit_length(txt_long, &mut txt_long_stack); limit_length(txt_long, &mut txt_long_stack);
let time = { let guard = TIME_ACCESS.get().await.lock().await; guard.current_time_us() } / 1000; let time = {
let guard = TIME_ACCESS.get().await.lock().await;
guard.current_time_us()
} / 1000;
let ordinal = message_key.ordinal() as u16; let ordinal = message_key.ordinal() as u16;
let template: &str = message_key.into(); let template: &str = message_key.into();
@@ -196,7 +199,7 @@ pub enum LogMessage {
StayAlive, StayAlive,
#[strum(serialize = "Connecting mqtt ${txt_short} with id ${txt_long}")] #[strum(serialize = "Connecting mqtt ${txt_short} with id ${txt_long}")]
MqttInfo, MqttInfo,
#[strum(serialize = "Received stay alive with value ${txt_short}")] #[strum(serialize = "Received stay alive with value ${number_a}")]
MqttStayAliveRec, MqttStayAliveRec,
#[strum(serialize = "Unknown topic recieved ${txt_long}")] #[strum(serialize = "Unknown topic recieved ${txt_long}")]
UnknownTopic, UnknownTopic,

View File

@@ -9,20 +9,22 @@
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 _;
use crate::config::PlantConfig; use crate::config::{NetworkConfig, PlantConfig};
use crate::fat_error::FatResult; use crate::fat_error::FatResult;
use crate::hal::esp::MQTT_STAY_ALIVE;
use crate::hal::{esp_time, TIME_ACCESS}; use crate::hal::{esp_time, TIME_ACCESS};
use crate::log::LOG_ACCESS; use crate::log::LOG_ACCESS;
use crate::tank::{determine_tank_state, TankError, WATER_FROZEN_THRESH}; use crate::tank::{determine_tank_state, TankError, TankState, WATER_FROZEN_THRESH};
use crate::webserver::httpd; use crate::webserver::http_server;
use crate::{ use crate::{
config::BoardVersion::INITIAL, config::BoardVersion::INITIAL,
hal::{PlantHal, HAL, PLANT_COUNT}, hal::{PlantHal, HAL, PLANT_COUNT},
}; };
use ::log::{error, info, warn}; use ::log::{info, warn};
use alloc::borrow::ToOwned; use alloc::borrow::ToOwned;
use alloc::string::{String, ToString}; use alloc::string::{String, ToString};
use alloc::sync::Arc; use alloc::sync::Arc;
@@ -64,7 +66,6 @@ mod fat_error;
mod hal; mod hal;
mod log; mod log;
mod plant_state; mod plant_state;
mod sipo;
mod tank; mod tank;
mod webserver; mod webserver;
@@ -73,8 +74,6 @@ extern crate alloc;
pub static BOARD_ACCESS: OnceLock<Mutex<CriticalSectionRawMutex, HAL<'static>>> = OnceLock::new(); pub static BOARD_ACCESS: OnceLock<Mutex<CriticalSectionRawMutex, HAL<'static>>> = OnceLock::new();
pub static STAY_ALIVE: AtomicBool = AtomicBool::new(false);
#[derive(Serialize, Deserialize, Debug, PartialEq)] #[derive(Serialize, Deserialize, Debug, PartialEq)]
enum WaitType { enum WaitType {
MissingConfig, MissingConfig,
@@ -166,7 +165,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
let cur = match board.board_hal.get_rtc_module().get_rtc_time().await { let cur = match board.board_hal.get_rtc_module().get_rtc_time().await {
Ok(value) => { Ok(value) => {
{ {
let mut guard = TIME_ACCESS.get().await.lock().await; let guard = TIME_ACCESS.get().await.lock().await;
guard.set_current_time_us(value.timestamp_micros() as u64); guard.set_current_time_us(value.timestamp_micros() as u64);
} }
value value
@@ -228,17 +227,17 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
info!("no mode override"); info!("no mode override");
} }
if (board.board_hal.get_config().hardware.board == INITIAL if board.board_hal.get_config().hardware.board == INITIAL
&& board.board_hal.get_config().network.ssid.is_none()) && board.board_hal.get_config().network.ssid.is_none()
{ {
info!("No wifi configured, starting initial config mode"); info!("No wifi configured, starting initial config mode");
let stack = board.board_hal.get_esp().wifi_ap(false).await?; let stack = board.board_hal.get_esp().wifi_ap().await?;
let reboot_now = Arc::new(AtomicBool::new(false)); let reboot_now = Arc::new(AtomicBool::new(false));
println!("starting webserver"); println!("starting webserver");
spawner.spawn(httpd(reboot_now.clone(), stack))?; spawner.spawn(http_server(reboot_now.clone(), stack))?;
wait_infinity(board, WaitType::MissingConfig, reboot_now.clone()).await; wait_infinity(board, WaitType::MissingConfig, reboot_now.clone()).await;
} }
@@ -247,7 +246,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
try_connect_wifi_sntp_mqtt(&mut board, &mut stack).await try_connect_wifi_sntp_mqtt(&mut board, &mut stack).await
} else { } else {
info!("No wifi configured"); info!("No wifi configured");
//the current sensors require this amount to stabilize, in case of wifi this is already handled due to connect timings; //the current sensors require this amount to stabilize, in the case of Wi-Fi this is already handled due to connect timings;
Timer::after_millis(100).await; Timer::after_millis(100).await;
NetworkMode::OFFLINE NetworkMode::OFFLINE
}; };
@@ -257,7 +256,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
let res = { let res = {
let esp = board.board_hal.get_esp(); let esp = board.board_hal.get_esp();
esp.wifi_ap(true).await esp.wifi_ap().await
}; };
match res { match res {
Ok(ap_stack) => { Ok(ap_stack) => {
@@ -276,7 +275,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
}), }),
None => UTC, // Fallback to UTC if no timezone is set None => UTC, // Fallback to UTC if no timezone is set
}; };
let _timezone = Tz::UTC; let _timezone = UTC;
let timezone_time = cur.with_timezone(&timezone); let timezone_time = cur.with_timezone(&timezone);
info!( info!(
@@ -317,7 +316,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
info!("executing config mode override"); info!("executing config mode override");
//config upload will trigger reboot! //config upload will trigger reboot!
let reboot_now = Arc::new(AtomicBool::new(false)); let reboot_now = Arc::new(AtomicBool::new(false));
spawner.spawn(httpd(reboot_now.clone(), stack.take().unwrap()))?; spawner.spawn(http_server(reboot_now.clone(), stack.take().unwrap()))?;
wait_infinity(board, WaitType::ConfigButton, reboot_now.clone()).await; wait_infinity(board, WaitType::ConfigButton, reboot_now.clone()).await;
} else { } else {
LOG_ACCESS LOG_ACCESS
@@ -395,11 +394,11 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
_water_frozen = true; _water_frozen = true;
} }
} }
info!("Water temp is {}", water_temp.unwrap_or(0.)); info!("Water temp is {}", water_temp.as_ref().unwrap_or(&0.));
//publish_tank_state(&tank_state, &water_temp).await; publish_tank_state(&mut board, &tank_state, water_temp).await;
let _plantstate: [PlantState; PLANT_COUNT] = [ let plantstate: [PlantState; PLANT_COUNT] = [
PlantState::read_hardware_state(0, &mut board).await, PlantState::read_hardware_state(0, &mut board).await,
PlantState::read_hardware_state(1, &mut board).await, PlantState::read_hardware_state(1, &mut board).await,
PlantState::read_hardware_state(2, &mut board).await, PlantState::read_hardware_state(2, &mut board).await,
@@ -410,7 +409,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
PlantState::read_hardware_state(7, &mut board).await, PlantState::read_hardware_state(7, &mut board).await,
]; ];
//publish_plant_states(&timezone_time.clone(), &plantstate).await; publish_plant_states(&mut board, &timezone_time.clone(), &plantstate).await;
// let pump_required = plantstate // let pump_required = plantstate
// .iter() // .iter()
@@ -567,12 +566,12 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
info!("Lightstate is {:?}", light_state); info!("Lightstate is {:?}", light_state);
} }
match serde_json::to_string(&light_state) { match &serde_json::to_string(&light_state) {
Ok(state) => { Ok(state) => {
let _ = board let _ = board
.board_hal .board_hal
.get_esp() .get_esp()
.mqtt_publish("/light", state.as_bytes()) .mqtt_publish("/light", state)
.await; .await;
} }
Err(err) => { Err(err) => {
@@ -586,25 +585,25 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
let _ = board let _ = board
.board_hal .board_hal
.get_esp() .get_esp()
.mqtt_publish("/deepsleep", "low Volt 12h".as_bytes()).await; .mqtt_publish("/deepsleep", "low Volt 12h").await;
12 * 60 12 * 60
} else if is_day { } else if is_day {
let _ = board let _ = board
.board_hal .board_hal
.get_esp() .get_esp()
.mqtt_publish("/deepsleep", "normal 20m".as_bytes()).await; .mqtt_publish("/deepsleep", "normal 20m").await;
20 20
} else { } else {
let _ = board let _ = board
.board_hal .board_hal
.get_esp() .get_esp()
.mqtt_publish("/deepsleep", "night 1h".as_bytes()).await; .mqtt_publish("/deepsleep", "night 1h").await;
60 60
}; };
let _ = board let _ = board
.board_hal .board_hal
.get_esp() .get_esp()
.mqtt_publish("/state", "sleep".as_bytes()) .mqtt_publish("/state", "sleep")
.await; .await;
info!("Go to sleep for {} minutes", deep_sleep_duration_minutes); info!("Go to sleep for {} minutes", deep_sleep_duration_minutes);
@@ -615,18 +614,17 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
//TODO //TODO
//mark_app_valid(); //mark_app_valid();
let stay_alive_mqtt = STAY_ALIVE.load(Ordering::Relaxed); let stay_alive = MQTT_STAY_ALIVE.load(Ordering::Relaxed);
let stay_alive = stay_alive_mqtt;
info!("Check stay alive, current state is {}", stay_alive); info!("Check stay alive, current state is {}", stay_alive);
if stay_alive { if stay_alive {
info!("Go to stay alive move");
let reboot_now = Arc::new(AtomicBool::new(false)); let reboot_now = Arc::new(AtomicBool::new(false));
//TODO let _webserver = http_server(reboot_now.clone(), stack.take().unwrap());
//let _webserver = httpd(reboot_now.clone());
wait_infinity(board, WaitType::MqttConfig, reboot_now.clone()).await; wait_infinity(board, WaitType::MqttConfig, reboot_now.clone()).await;
} else { } else {
//TODO wait for all mqtt publishes?
Timer::after_millis(5000).await;
board.board_hal.get_esp().set_restart_to_conf(false); board.board_hal.get_esp().set_restart_to_conf(false);
board board
.board_hal .board_hal
@@ -653,7 +651,6 @@ pub async fn do_secure_pump(
Timer::after_millis(10).await; Timer::after_millis(10).await;
for step in 0..plant_config.pump_time_s as usize { for step in 0..plant_config.pump_time_s as usize {
let flow_value = board.board_hal.get_tank_sensor()?.get_flow_meter_value(); let flow_value = board.board_hal.get_tank_sensor()?.get_flow_meter_value();
let flow_value = 1;
flow_collector[step] = flow_value; flow_collector[step] = flow_value;
let flow_value_ml = flow_value as f32 * board.board_hal.get_config().tank.ml_per_pulse; let flow_value_ml = flow_value as f32 * board.board_hal.get_config().tank.ml_per_pulse;
@@ -735,7 +732,7 @@ pub async fn do_secure_pump(
error = true; error = true;
break; break;
} else { } else {
//eg v3 without a sensor ends here, do not spam //e.g., v3 without a sensor ends here, do not spam
} }
} }
} }
@@ -743,9 +740,8 @@ pub async fn do_secure_pump(
pump_time_s += 1; pump_time_s += 1;
} }
} }
board.board_hal.get_tank_sensor().unwrap().stop_flow_meter(); board.board_hal.get_tank_sensor()?.stop_flow_meter();
let final_flow_value = board.board_hal.get_tank_sensor()?.get_flow_meter_value(); let final_flow_value = board.board_hal.get_tank_sensor()?.get_flow_meter_value();
let final_flow_value = 12;
let flow_value_ml = final_flow_value as f32 * board.board_hal.get_config().tank.ml_per_pulse; let flow_value_ml = final_flow_value as f32 * board.board_hal.get_config().tank.ml_per_pulse;
info!( info!(
"Final flow value is {} with {} ml", "Final flow value is {} with {} ml",
@@ -763,42 +759,64 @@ pub async fn do_secure_pump(
}) })
} }
async fn update_charge_indicator(board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'_>>) { async fn update_charge_indicator(
board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>,
) {
//we have mppt controller, ask it for charging current //we have mppt controller, ask it for charging current
// let tank_state = determine_tank_state(&mut board); let tank_state = determine_tank_state(board).await;
//
// if tank_state.is_enabled() { if tank_state.is_enabled() {
// if let Some(err) = tank_state.got_error(&board.board_hal.get_config().tank) { if let Some(err) = tank_state.got_error(&board.board_hal.get_config().tank) {
// match err { match err {
// TankError::SensorDisabled => { /* unreachable */ } TankError::SensorDisabled => { /* unreachable */ }
// TankError::SensorMissing(raw_value_mv) => log( TankError::SensorMissing(raw_value_mv) => {
// LogMessage::TankSensorMissing, LOG_ACCESS
// raw_value_mv as u32, .lock()
// 0, .await
// "", .log(
// "", LogMessage::TankSensorMissing,
// ).await, raw_value_mv as u32,
// TankError::SensorValueError { value, min, max } => log( 0,
// LogMessage::TankSensorValueRangeError, "",
// min as u32, "",
// max as u32, )
// &format!("{}", value), .await
// "", }
// ).await, TankError::SensorValueError { value, min, max } => {
// TankError::BoardError(err) => { LOG_ACCESS
// log(LogMessage::TankSensorBoardError, 0, 0, "", &err.to_string()).await .lock()
// } .await
// } .log(
// // disabled cannot trigger this because of wrapping if is_enabled LogMessage::TankSensorValueRangeError,
// board.board_hal.general_fault(true).await; min as u32,
// } else if tank_state max as u32,
// .warn_level(&board.board_hal.get_config().tank) &format!("{}", value),
// .is_ok_and(|warn| warn) "",
// { )
// log(LogMessage::TankWaterLevelLow, 0, 0, "", "").await; .await
// board.board_hal.general_fault(true).await; }
// } TankError::BoardError(err) => {
// } LOG_ACCESS
.lock()
.await
.log(LogMessage::TankSensorBoardError, 0, 0, "", &err.to_string())
.await
}
}
// disabled cannot trigger this because of wrapping if is_enabled
board.board_hal.general_fault(true).await;
} else if tank_state
.warn_level(&board.board_hal.get_config().tank)
.is_ok_and(|warn| warn)
{
LOG_ACCESS
.lock()
.await
.log(LogMessage::TankWaterLevelLow, 0, 0, "", "")
.await;
board.board_hal.general_fault(true).await;
}
}
if let Ok(current) = board.board_hal.get_mptt_current().await { if let Ok(current) = board.board_hal.get_mptt_current().await {
let _ = board let _ = board
.board_hal .board_hal
@@ -817,48 +835,39 @@ async fn update_charge_indicator(board: &mut MutexGuard<'_, CriticalSectionRawMu
let _ = board.board_hal.set_charge_indicator(false); let _ = board.board_hal.set_charge_indicator(false);
} }
} }
//
// async fn publish_tank_state(tank_state: &TankState, water_temp: &anyhow::Result<f32>) {
// let board = &mut BOARD_ACCESS.get().lock().await;
// match serde_json::to_string(
// &tank_state.as_mqtt_info(&board.board_hal.get_config().tank, water_temp),
// ) {
// Ok(state) => {
// let _ = board
// .board_hal
// .get_esp()
// .mqtt_publish("/water", state.as_bytes());
// }
// Err(err) => {
// info!("Error publishing tankstate {}", err);
// }
// };
// }
// async fn publish_plant_states(timezone_time: &DateTime<Tz>, plantstate: &[PlantState; 8]) { async fn publish_tank_state(
// let board = &mut BOARD_ACCESS.get().lock().await; board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
// for (plant_id, (plant_state, plant_conf)) in plantstate tank_state: &TankState,
// .iter() water_temp: FatResult<f32>,
// .zip(&board.board_hal.get_config().plants.clone()) ) {
// .enumerate() let state = serde_json::to_string(
// { &tank_state.as_mqtt_info(&board.board_hal.get_config().tank, &water_temp),
// match serde_json::to_string(&plant_state.to_mqtt_info(plant_conf, timezone_time)) { )
// Ok(state) => { .unwrap();
// let plant_topic = format!("/plant{}", plant_id + 1); let _ = board.board_hal.get_esp().mqtt_publish("/water", &*state);
// let _ = board }
// .board_hal
// .get_esp() async fn publish_plant_states(
// .mqtt_publish(&plant_topic, state.as_bytes()) board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
// .await; timezone_time: &DateTime<Tz>,
// //TODO? reduce speed as else messages will be dropped plantstate: &[PlantState; 8],
// Timer::after_millis(200).await ) {
// } for (plant_id, (plant_state, plant_conf)) in plantstate
// Err(err) => { .iter()
// error!("Error publishing plant state {}", err); .zip(&board.board_hal.get_config().plants.clone())
// } .enumerate()
// }; {
// } let state =
// } serde_json::to_string(&plant_state.to_mqtt_info(plant_conf, timezone_time)).unwrap();
let plant_topic = format!("/plant{}", plant_id + 1);
let _ = board
.board_hal
.get_esp()
.mqtt_publish(&plant_topic, &state)
.await;
}
}
async fn publish_firmware_info( async fn publish_firmware_info(
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
@@ -867,30 +876,33 @@ async fn publish_firmware_info(
timezone_time: &String, timezone_time: &String,
) { ) {
let esp = board.board_hal.get_esp(); let esp = board.board_hal.get_esp();
let _ = esp.mqtt_publish("/firmware/address", ip_address).await;
let _ = esp let _ = esp
.mqtt_publish("/firmware/address", ip_address.as_bytes()) .mqtt_publish("/firmware/githash", &version.git_hash)
.await; .await;
let _ = esp let _ = esp
.mqtt_publish("/firmware/githash", version.git_hash.as_bytes()) .mqtt_publish("/firmware/buildtime", &version.build_time)
.await; .await;
let _ = esp let _ = esp.mqtt_publish("/firmware/last_online", timezone_time);
.mqtt_publish("/firmware/buildtime", version.build_time.as_bytes())
.await;
let _ = esp.mqtt_publish("/firmware/last_online", timezone_time.as_bytes());
let state = esp.get_ota_state(); let state = esp.get_ota_state();
let _ = esp let _ = esp.mqtt_publish("/firmware/ota_state", &state).await;
.mqtt_publish("/firmware/ota_state", state.as_bytes())
.await;
let slot = esp.get_ota_slot(); let slot = esp.get_ota_slot();
let _ = esp let _ = esp
.mqtt_publish("/firmware/ota_slot", format!("slot{slot}").as_bytes()) .mqtt_publish("/firmware/ota_slot", &format!("slot{slot}"))
.await; .await;
let _ = esp.mqtt_publish("/state", "online".as_bytes()).await; let _ = esp.mqtt_publish("/state", "online").await;
}
macro_rules! mk_static {
($t:ty,$val:expr) => {{
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
#[deny(unused_attributes)]
let x = STATIC_CELL.uninit().write(($val));
x
}};
} }
async fn try_connect_wifi_sntp_mqtt( async fn try_connect_wifi_sntp_mqtt(
board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>, board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>,
mut stack_store: &mut OptionLock<Stack<'static>>, stack_store: &mut OptionLock<Stack<'static>>,
) -> NetworkMode { ) -> NetworkMode {
let nw_conf = &board.board_hal.get_config().network.clone(); let nw_conf = &board.board_hal.get_config().network.clone();
match board.board_hal.get_esp().wifi(nw_conf).await { match board.board_hal.get_esp().wifi(nw_conf).await {
@@ -916,8 +928,9 @@ async fn try_connect_wifi_sntp_mqtt(
}; };
let mqtt_connected = if board.board_hal.get_config().network.mqtt_url.is_some() { let mqtt_connected = if board.board_hal.get_config().network.mqtt_url.is_some() {
let nw_config = &board.board_hal.get_config().network.clone(); let nw_config = board.board_hal.get_config().network.clone();
match board.board_hal.get_esp().mqtt(nw_config).await { let nw_config = mk_static!(NetworkConfig, nw_config);
match board.board_hal.get_esp().mqtt(nw_config, stack).await {
Ok(_) => { Ok(_) => {
info!("Mqtt connection ready"); info!("Mqtt connection ready");
true true
@@ -956,25 +969,23 @@ async fn pump_info(
let pump_info = PumpInfo { let pump_info = PumpInfo {
enabled: pump_active, enabled: pump_active,
pump_ineffective, pump_ineffective,
median_current_ma: median_current_ma, median_current_ma,
max_current_ma: max_current_ma, max_current_ma,
min_current_ma: min_current_ma, min_current_ma,
}; };
let pump_topic = format!("/pump{}", plant_id + 1); let pump_topic = format!("/pump{}", plant_id + 1);
match serde_json::to_string(&pump_info) { match serde_json::to_string(&pump_info) {
Ok(state) => { Ok(state) => {
let _ = BOARD_ACCESS BOARD_ACCESS
.get() .get()
.await .await
.lock() .lock()
.await .await
.board_hal .board_hal
.get_esp() .get_esp()
.mqtt_publish(&pump_topic, state.as_bytes()); .mqtt_publish(&pump_topic, &state)
//reduce speed as else messages will be dropped .await;
//TODO maybee not required for low level hal?
Timer::after_millis(200).await;
} }
Err(err) => { Err(err) => {
warn!("Error publishing pump state {}", err); warn!("Error publishing pump state {}", err);
@@ -991,9 +1002,7 @@ async fn publish_mppt_state(
current_ma: current.as_milliamperes() as u32, current_ma: current.as_milliamperes() as u32,
voltage_ma: voltage.as_millivolts() as u32, voltage_ma: voltage.as_millivolts() as u32,
}; };
if let Ok(serialized_solar_state_bytes) = if let Ok(serialized_solar_state_bytes) = serde_json::to_string(&solar_state) {
serde_json::to_string(&solar_state).map(|s| s.into_bytes())
{
let _ = board let _ = board
.board_hal .board_hal
.get_esp() .get_esp()
@@ -1013,9 +1022,9 @@ async fn publish_battery_state(
let value = match state { let value = match state {
Ok(state) => { Ok(state) => {
let json = serde_json::to_string(&state).unwrap().to_owned(); let json = serde_json::to_string(&state).unwrap().to_owned();
json.as_bytes().to_owned() json.to_owned()
} }
Err(_) => "error".as_bytes().to_owned(), Err(_) => "error".to_owned(),
}; };
{ {
let _ = board let _ = board
@@ -1082,7 +1091,7 @@ async fn wait_infinity(
Timer::after_millis(delay).await; Timer::after_millis(delay).await;
if wait_type == WaitType::MqttConfig && !STAY_ALIVE.load(Ordering::Relaxed) { if wait_type == WaitType::MqttConfig && !MQTT_STAY_ALIVE.load(Ordering::Relaxed) {
reboot_now.store(true, Ordering::Relaxed); reboot_now.store(true, Ordering::Relaxed);
} }
if reboot_now.load(Ordering::Relaxed) { if reboot_now.load(Ordering::Relaxed) {
@@ -1106,7 +1115,7 @@ async fn main(spawner: Spawner) -> ! {
logger::init_logger_from_env(); logger::init_logger_from_env();
//force init here! //force init here!
println!("Hal init"); println!("Hal init");
match BOARD_ACCESS.init(PlantHal::create(spawner).await.unwrap()) { match BOARD_ACCESS.init(PlantHal::create().await.unwrap()) {
Ok(_) => {} Ok(_) => {}
Err(_) => { Err(_) => {
panic!("Could not set hal to static") panic!("Could not set hal to static")

View File

@@ -118,7 +118,11 @@ fn map_range_moisture(
impl PlantState { impl PlantState {
pub async fn read_hardware_state(plant_id: usize, board: &mut HAL<'_>) -> Self { pub async fn read_hardware_state(plant_id: usize, board: &mut HAL<'_>) -> Self {
let sensor_a = if board.board_hal.get_config().plants[plant_id].sensor_a { let sensor_a = if board.board_hal.get_config().plants[plant_id].sensor_a {
match board.board_hal.measure_moisture_hz(plant_id, Sensor::A).await { match board
.board_hal
.measure_moisture_hz(plant_id, Sensor::A)
.await
{
Ok(raw) => match map_range_moisture( Ok(raw) => match map_range_moisture(
raw, raw,
board.board_hal.get_config().plants[plant_id].moisture_sensor_min_frequency, board.board_hal.get_config().plants[plant_id].moisture_sensor_min_frequency,
@@ -139,7 +143,11 @@ impl PlantState {
}; };
let sensor_b = if board.board_hal.get_config().plants[plant_id].sensor_b { let sensor_b = if board.board_hal.get_config().plants[plant_id].sensor_b {
match board.board_hal.measure_moisture_hz(plant_id, Sensor::B).await { match board
.board_hal
.measure_moisture_hz(plant_id, Sensor::B)
.await
{
Ok(raw) => match map_range_moisture( Ok(raw) => match map_range_moisture(
raw, raw,
board.board_hal.get_config().plants[plant_id].moisture_sensor_min_frequency, board.board_hal.get_config().plants[plant_id].moisture_sensor_min_frequency,
@@ -264,50 +272,50 @@ impl PlantState {
PlantWateringMode::TimerOnly => !self.pump_in_timeout(plant_conf, current_time), PlantWateringMode::TimerOnly => !self.pump_in_timeout(plant_conf, current_time),
} }
} }
//
// pub fn to_mqtt_info( pub fn to_mqtt_info(
// &self, &self,
// plant_conf: &PlantConfig, plant_conf: &PlantConfig,
// current_time: &DateTime<Tz>, current_time: &DateTime<Tz>,
// ) -> PlantInfo<'_> { ) -> PlantInfo<'_> {
// PlantInfo { PlantInfo {
// sensor_a: &self.sensor_a, sensor_a: &self.sensor_a,
// sensor_b: &self.sensor_b, sensor_b: &self.sensor_b,
// mode: plant_conf.mode, mode: plant_conf.mode,
// do_water: self.needs_to_be_watered(plant_conf, current_time), do_water: self.needs_to_be_watered(plant_conf, current_time),
// dry: if let Some(moisture_percent) = self.plant_moisture().0 { dry: if let Some(moisture_percent) = self.plant_moisture().0 {
// moisture_percent < plant_conf.target_moisture moisture_percent < plant_conf.target_moisture
// } else { } else {
// false false
// }, },
// cooldown: self.pump_in_timeout(plant_conf, current_time), cooldown: self.pump_in_timeout(plant_conf, current_time),
// out_of_work_hour: in_time_range( out_of_work_hour: in_time_range(
// current_time, current_time,
// plant_conf.pump_hour_start, plant_conf.pump_hour_start,
// plant_conf.pump_hour_end, plant_conf.pump_hour_end,
// ), ),
// consecutive_pump_count: self.pump.consecutive_pump_count, consecutive_pump_count: self.pump.consecutive_pump_count,
// pump_error: self.pump.is_err(plant_conf), pump_error: self.pump.is_err(plant_conf),
// last_pump: self last_pump: self
// .pump .pump
// .previous_pump .previous_pump
// .map(|t| t.with_timezone(&current_time.timezone())), .map(|t| t.with_timezone(&current_time.timezone())),
// next_pump: if matches!( next_pump: if matches!(
// plant_conf.mode, plant_conf.mode,
// PlantWateringMode::TimerOnly PlantWateringMode::TimerOnly
// | PlantWateringMode::TargetMoisture | PlantWateringMode::TargetMoisture
// | PlantWateringMode::MinMoisture | PlantWateringMode::MinMoisture
// ) { ) {
// self.pump.previous_pump.and_then(|last_pump| { self.pump.previous_pump.and_then(|last_pump| {
// last_pump last_pump
// .checked_add_signed(TimeDelta::minutes(plant_conf.pump_cooldown_min.into())) .checked_add_signed(TimeDelta::minutes(plant_conf.pump_cooldown_min.into()))
// .map(|t| t.with_timezone(&current_time.timezone())) .map(|t| t.with_timezone(&current_time.timezone()))
// }) })
// } else { } else {
// None None
// }, },
// } }
// } }
} }
#[derive(Debug, PartialEq, Serialize)] #[derive(Debug, PartialEq, Serialize)]
@@ -330,8 +338,8 @@ pub struct PlantInfo<'a> {
/// how often has the pump been watered without reaching target moisture /// how often has the pump been watered without reaching target moisture
consecutive_pump_count: u32, consecutive_pump_count: u32,
pump_error: Option<PumpError>, pump_error: Option<PumpError>,
// /// last time when the pump was active /// last time when the pump was active
// last_pump: Option<DateTime<Tz>>, last_pump: Option<DateTime<Tz>>,
// /// next time when pump should activate /// next time when pump should activate
// next_pump: Option<DateTime<Tz>>, next_pump: Option<DateTime<Tz>>,
} }

View 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 = 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))
}

View 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 disposition = 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", disposition.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).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 the file is deleted first; 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,
})
}

View File

@@ -0,0 +1,167 @@
use crate::fat_error::{FatError, FatResult};
use crate::hal::{esp_time, PLANT_COUNT};
use crate::log::LogMessage;
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 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(),
)?))
}

View 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))
}

View 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))
}

View File

@@ -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| {

View 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()))
}