mqtt via mcutie
This commit is contained in:
		| @@ -159,7 +159,7 @@ option-lock = { version = "0.3.1", default-features = false } | ||||
|  | ||||
| #stay in sync with mcutie version here! | ||||
| 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"] } | ||||
|  | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| use alloc::string::String; | ||||
| use core::str::FromStr; | ||||
| use crate::hal::PLANT_COUNT; | ||||
| use crate::plant_state::PlantWateringMode; | ||||
| use alloc::string::String; | ||||
| use core::str::FromStr; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] | ||||
| @@ -10,10 +10,10 @@ pub struct NetworkConfig { | ||||
|     pub ap_ssid: heapless::String<32>, | ||||
|     pub ssid: Option<heapless::String<32>>, | ||||
|     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 mqtt_user: Option<heapless::String<32>>, | ||||
|     pub mqtt_password: Option<heapless::String<64>>, | ||||
|     pub mqtt_user: Option<String>, | ||||
|     pub mqtt_password: Option<String>, | ||||
|     pub max_wait: u32, | ||||
| } | ||||
| impl Default for NetworkConfig { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| use crate::hal::Box; | ||||
| use crate::fat_error::{FatError, FatResult}; | ||||
| use crate::hal::Box; | ||||
| use alloc::string::String; | ||||
| use async_trait::async_trait; | ||||
| use bq34z100::{Bq34z100g1, Bq34z100g1Driver, Flags}; | ||||
| @@ -43,14 +43,6 @@ pub enum BatteryError { | ||||
|     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)] | ||||
| pub enum BatteryState { | ||||
|     Unknown, | ||||
|   | ||||
| @@ -7,26 +7,23 @@ use serde::Serialize; | ||||
|  | ||||
| use crate::fat_error::{ContextExt, FatError, FatResult}; | ||||
| use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem; | ||||
| use alloc::borrow::ToOwned; | ||||
| use alloc::string::ToString; | ||||
| use alloc::sync::Arc; | ||||
| use alloc::{format, string::String, vec::Vec}; | ||||
| use core::marker::PhantomData; | ||||
| use core::net::{IpAddr, Ipv4Addr, SocketAddr}; | ||||
| use core::str::FromStr; | ||||
| use core::sync::atomic::Ordering; | ||||
| use core::sync::atomic::Ordering::Relaxed; | ||||
| use edge_dhcp::io::server::run; | ||||
| use embassy_executor::Spawner; | ||||
| use embassy_net::udp::UdpSocket; | ||||
| use embassy_net::{DhcpConfig, Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4}; | ||||
| use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; | ||||
| use embassy_sync::mutex::{Mutex, MutexGuard}; | ||||
| use embassy_sync::once_lock::OnceLock; | ||||
| use embassy_time::{Duration, Timer}; | ||||
| use embedded_storage::nor_flash::ReadNorFlash; | ||||
| use esp_bootloader_esp_idf::ota::{Ota, OtaImageState}; | ||||
| 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::rtc_cntl::{ | ||||
|     sleep::{TimerWakeupSource, WakeupLevel}, | ||||
| @@ -37,11 +34,15 @@ use esp_println::println; | ||||
| use esp_storage::FlashStorage; | ||||
| use esp_wifi::wifi::{ | ||||
|     AccessPointConfiguration, AccessPointInfo, AuthMethod, ClientConfiguration, Configuration, | ||||
|     Interfaces, ScanConfig, ScanTypeConfig, WifiController, WifiDevice, WifiState, | ||||
|     ScanConfig, ScanTypeConfig, WifiController, WifiDevice, WifiState, | ||||
| }; | ||||
| use littlefs2::fs::Filesystem; | ||||
| use littlefs2_core::{FileType, PathBuf, SeekFrom}; | ||||
| use log::{info, warn}; | ||||
| use mcutie::{ | ||||
|     Error, McutieBuilder, McutieReceiver, McutieTask, MqttMessage, PublishDisplay, Publishable, | ||||
|     QoS, Topic, | ||||
| }; | ||||
| use portable_atomic::AtomicBool; | ||||
| use smoltcp::socket::udp::PacketMetadata; | ||||
| use smoltcp::wire::DnsQueryType; | ||||
| @@ -59,6 +60,11 @@ static mut RESTART_TO_CONF: i8 = 0; | ||||
| const CONFIG_FILE: &str = "config.json"; | ||||
| 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)] | ||||
| pub struct FileInfo { | ||||
|     filename: String, | ||||
| @@ -72,18 +78,6 @@ pub struct FileList { | ||||
|     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)] | ||||
| struct Timestamp { | ||||
|     stamp: DateTime<Utc>, | ||||
| @@ -127,9 +121,6 @@ pub struct Esp<'a> { | ||||
|     pub interface_ap: Option<WifiDevice<'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>, | ||||
|  | ||||
|     // 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. | ||||
| unsafe impl Send for Esp<'_> {} | ||||
|  | ||||
| pub struct IpInfo { | ||||
|     pub(crate) ip: IpAddr, | ||||
|     netmask: IpAddr, | ||||
|     gateway: IpAddr, | ||||
| } | ||||
|  | ||||
| macro_rules! mk_static { | ||||
|     ($t:ty,$val:expr) => {{ | ||||
|         static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new(); | ||||
| @@ -164,7 +149,7 @@ macro_rules! mk_static { | ||||
|  | ||||
| impl Esp<'_> { | ||||
|     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; | ||||
|         access.remove(&*file)?; | ||||
|         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 { | ||||
|             Ok(config) => config.network.ap_ssid.as_str().to_string(), | ||||
|             Err(_) => "PlantCtrl Emergency Mode".to_string(), | ||||
| @@ -482,7 +467,7 @@ impl Esp<'_> { | ||||
|         let (stack, runner) = embassy_net::new( | ||||
|             device, | ||||
|             config, | ||||
|             mk_static!(StackResources<4>, StackResources::<4>::new()), | ||||
|             mk_static!(StackResources<8>, StackResources::<8>::new()), | ||||
|             seed, | ||||
|         ); | ||||
|         let stack = mk_static!(Stack, stack); | ||||
| @@ -572,6 +557,8 @@ impl Esp<'_> { | ||||
|             } | ||||
|             Timer::after(Duration::from_millis(100)).await | ||||
|         } | ||||
|  | ||||
|         info!("Connected WIFI, dhcp: {:?}", stack.config_v4()); | ||||
|         Ok(stack.clone()) | ||||
|     } | ||||
|  | ||||
| @@ -602,9 +589,6 @@ impl Esp<'_> { | ||||
|             let ext1 = esp_hal::rtc_cntl::sleep::Ext1WakeupSource::new(&mut wake_pins); | ||||
|             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> { | ||||
| @@ -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 | ||||
|             .base_topic | ||||
|             .as_ref() | ||||
| @@ -716,7 +704,12 @@ impl Esp<'_> { | ||||
|         if base_topic.is_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 | ||||
|             .mqtt_url | ||||
|             .as_ref() | ||||
| @@ -725,202 +718,223 @@ impl Esp<'_> { | ||||
|             bail!("Mqtt url was empty") | ||||
|         } | ||||
|  | ||||
|         bail!("todo"); | ||||
|         // | ||||
|         // let last_will_topic = format!("{}/state", base_topic); | ||||
|         // let mqtt_client_config = MqttClientConfiguration { | ||||
|         //     lwt: Some(LwtConfiguration { | ||||
|         //         topic: &last_will_topic, | ||||
|         //         payload: "lost".as_bytes(), | ||||
|         //         qos: AtLeastOnce, | ||||
|         //         retain: true, | ||||
|         //     }), | ||||
|         //     client_id: Some("plantctrl"), | ||||
|         //     keep_alive_interval: Some(Duration::from_secs(60 * 60 * 2)), | ||||
|         //     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"); | ||||
|         let last_will_topic = format!("{}/state", base_topic); | ||||
|         let round_trip_topic = format!("{}/internal/roundtrip", base_topic); | ||||
|         let stay_alive_topic = format!("{}/stay_alive", base_topic); | ||||
|  | ||||
|         let mut builder: McutieBuilder<'_, String, PublishDisplay<String, &str>, 0> = | ||||
|             McutieBuilder::new(stack, "plant ctrl", mqtt_url); | ||||
|         if network_config.mqtt_user.is_some() && network_config.mqtt_password.is_some() { | ||||
|             builder = builder.with_authentication( | ||||
|                 network_config.mqtt_user.as_ref().unwrap().as_str(), | ||||
|                 network_config.mqtt_password.as_ref().unwrap().as_str(), | ||||
|             ); | ||||
|             info!("With authentification"); | ||||
|         } | ||||
|  | ||||
|         let lwt = Topic::General(last_will_topic); | ||||
|         let lwt = mk_static!(Topic<String>, lwt); | ||||
|         let lwt = lwt.with_display("lost").retain(true).qos(QoS::AtLeastOnce); | ||||
|         builder = builder.with_last_will(lwt); | ||||
|         //TODO make configurable | ||||
|         builder = builder.with_device_id("plantctrl"); | ||||
|  | ||||
|         let builder: McutieBuilder<'_, String, PublishDisplay<String, &str>, 2> = builder | ||||
|             .with_subscriptions([ | ||||
|                 Topic::General(round_trip_topic.clone()), | ||||
|                 Topic::General(stay_alive_topic.clone()), | ||||
|             ]); | ||||
|  | ||||
|         let keep_alive = Duration::from_secs(60 * 60 * 2).as_secs() as u16; | ||||
|         let (receiver, task) = builder.build(keep_alive); | ||||
|  | ||||
|         let spawner = Spawner::for_current_executor().await; | ||||
|         spawner.spawn(mqtt_incoming_task( | ||||
|             receiver, | ||||
|             round_trip_topic.clone(), | ||||
|             stay_alive_topic.clone(), | ||||
|         ))?; | ||||
|         spawner.spawn(mqtt_runner(task))?; | ||||
|  | ||||
|         LOG_ACCESS | ||||
|             .lock() | ||||
|             .await | ||||
|             .log(LogMessage::StayAlive, 0, 0, "", &stay_alive_topic) | ||||
|             .await; | ||||
|  | ||||
|         LOG_ACCESS | ||||
|             .lock() | ||||
|             .await | ||||
|             .log(LogMessage::MqttInfo, 0, 0, "", mqtt_url) | ||||
|             .await; | ||||
|  | ||||
|         let mqtt_timeout = 15000; | ||||
|         let timeout = { | ||||
|             let guard = TIME_ACCESS.get().await.lock().await; | ||||
|             guard.current_time_us() | ||||
|         } + mqtt_timeout as u64 * 1000; | ||||
|         while !MQTT_CONNECTED_EVENT_RECEIVED.load(Ordering::Relaxed) { | ||||
|             let cur = TIME_ACCESS.get().await.lock().await.current_time_us(); | ||||
|             if cur > timeout { | ||||
|                 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_publish(&mut self, _subtopic: &str, _message: &[u8]) -> FatResult<()> { | ||||
|         bail!("todo"); | ||||
|         // | ||||
|         // if self.mqtt_client.is_none() { | ||||
|         //     return anyhow::Ok(()); | ||||
|         // } | ||||
|         // if !subtopic.starts_with("/") { | ||||
|         //     log::info!("Subtopic without / at start {}", subtopic); | ||||
|         //     bail!("Subtopic without / at start {}", subtopic); | ||||
|         // } | ||||
|         // if subtopic.len() > 192 { | ||||
|         //     log::info!("Subtopic exceeds 192 chars {}", subtopic); | ||||
|         //     bail!("Subtopic exceeds 192 chars {}", subtopic); | ||||
|         // } | ||||
|         // let client = self.mqtt_client.as_mut().unwrap(); | ||||
|         // let mut full_topic: heapless::String<256> = heapless::String::new(); | ||||
|         // if full_topic.push_str(client.base_topic.as_str()).is_err() { | ||||
|         //     log::info!("Some error assembling full_topic 1"); | ||||
|         //     bail!("Some error assembling full_topic 1") | ||||
|         // }; | ||||
|         // if full_topic.push_str(subtopic).is_err() { | ||||
|         //     log::info!("Some error assembling full_topic 2"); | ||||
|         //     bail!("Some error assembling full_topic 2") | ||||
|         // }; | ||||
|         // let publish = client | ||||
|         //     .mqtt_client | ||||
|         //     .publish(&full_topic, ExactlyOnce, true, message); | ||||
|         // Timer::after_millis(10).await; | ||||
|         // match publish { | ||||
|         //     OkStd(message_id) => { | ||||
|         //         log::info!( | ||||
|         //             "Published mqtt topic {} with message {:#?} msgid is {:?}", | ||||
|         //             full_topic, | ||||
|         //             String::from_utf8_lossy(message), | ||||
|         //             message_id | ||||
|         //         ); | ||||
|         //         anyhow::Ok(()) | ||||
|         //     } | ||||
|         //     Err(err) => { | ||||
|         //         log::info!( | ||||
|         //             "Error during mqtt send on topic {} with message {:#?} error is {:?}", | ||||
|         //             full_topic, | ||||
|         //             String::from_utf8_lossy(message), | ||||
|         //             err | ||||
|         //         ); | ||||
|         //         Err(err)? | ||||
|         //     } | ||||
|         // } | ||||
|  | ||||
|     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] | ||||
| @@ -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 unbound_socket = Udp::new(stack, &buffers); | ||||
|     let mut bound_socket = unbound_socket | ||||
|         .bind(core::net::SocketAddr::V4(SocketAddrV4::new( | ||||
|         .bind(SocketAddr::V4(SocketAddrV4::new( | ||||
|             Ipv4Addr::UNSPECIFIED, | ||||
|             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; | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[embassy_executor::task(pool_size = 2)] | ||||
| async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) { | ||||
|     runner.run().await | ||||
| } | ||||
|   | ||||
| @@ -4,6 +4,7 @@ mod initial_hal; | ||||
| mod little_fs2storage_adapter; | ||||
| pub(crate) mod rtc; | ||||
| mod v3_hal; | ||||
| mod v3_shift_register; | ||||
| mod v4_hal; | ||||
| mod v4_sensor; | ||||
| mod water; | ||||
| @@ -68,11 +69,8 @@ use eeprom24x::page_size::B32; | ||||
| use eeprom24x::unique_serial::No; | ||||
| use eeprom24x::{Eeprom24x, SlaveAddr, Storage}; | ||||
| 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::CriticalSectionMutex; | ||||
| use esp_bootloader_esp_idf::partitions::{ | ||||
|     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 { | ||||
|     pub async fn create( | ||||
|         spawner: Spawner, | ||||
|     ) -> Result<Mutex<CriticalSectionRawMutex, HAL<'static>>, FatError> { | ||||
|     pub async fn create() -> Result<Mutex<CriticalSectionRawMutex, HAL<'static>>, FatError> { | ||||
|         let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); | ||||
|         let peripherals: Peripherals = esp_hal::init(config); | ||||
|  | ||||
| @@ -394,7 +388,6 @@ impl PlantHal { | ||||
|             interface_ap: Some(ap), | ||||
|             boot_button, | ||||
|             wake_gpio1, | ||||
|             mqtt_client: None, | ||||
|             ota, | ||||
|             ota_next, | ||||
|         }; | ||||
| @@ -548,9 +541,6 @@ impl PlantHal { | ||||
|                         v4_hal::create_v4(free_pins, esp, config, battery_interaction, rtc_module) | ||||
|                             .await? | ||||
|                     } | ||||
|                     _ => { | ||||
|                         bail!("Unknown board version"); | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 HAL { board_hal } | ||||
|   | ||||
| @@ -2,10 +2,10 @@ use crate::bail; | ||||
| use crate::fat_error::FatError; | ||||
| use crate::hal::esp::{hold_disable, hold_enable}; | ||||
| use crate::hal::rtc::RTCModuleInteraction; | ||||
| use crate::hal::v3_shift_register::ShiftRegister40; | ||||
| use crate::hal::water::TankSensor; | ||||
| use crate::hal::{BoardInteraction, FreePeripherals, Sensor, PLANT_COUNT, TIME_ACCESS}; | ||||
| use crate::log::{LogMessage, LOG_ACCESS}; | ||||
| use crate::sipo::ShiftRegister40; | ||||
| use crate::{ | ||||
|     config::PlantControllerConfig, | ||||
|     hal::{battery::BatteryInteraction, esp::Esp}, | ||||
| @@ -15,7 +15,6 @@ use alloc::format; | ||||
| use alloc::string::ToString; | ||||
| use async_trait::async_trait; | ||||
| use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; | ||||
| use embassy_sync::blocking_mutex::CriticalSectionMutex; | ||||
| use embassy_sync::mutex::Mutex; | ||||
| use embassy_time::Timer; | ||||
| use embedded_hal::digital::OutputPin as _; | ||||
| @@ -267,7 +266,7 @@ impl<'a> BoardInteraction<'a> for V3<'a> { | ||||
|             //self.signal_counter.counter_clear()?; | ||||
|             //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()?; | ||||
|             } | ||||
|  | ||||
| @@ -298,7 +297,7 @@ impl<'a> BoardInteraction<'a> for V3<'a> { | ||||
|  | ||||
|             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_1 = &mut shift_register.decompose()[MS_1]; | ||||
|                 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; | ||||
|             //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()[SENSOR_ON].set_low()?; | ||||
|             } | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| //! Serial-in parallel-out shift register
 | ||||
| 
 | ||||
| #![allow(warnings)] | ||||
| use core::cell::RefCell; | ||||
| use core::convert::Infallible; | ||||
| use core::iter::Iterator; | ||||
| @@ -10,7 +10,6 @@ use async_trait::async_trait; | ||||
| use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice; | ||||
| use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; | ||||
| use embassy_time::Timer; | ||||
| use esp_hal::analog::adc::{Adc, AdcConfig, Attenuation}; | ||||
| use esp_hal::{twai, Blocking}; | ||||
| //use embedded_hal_bus::i2c::MutexDevice; | ||||
| use crate::bail; | ||||
| @@ -137,10 +136,6 @@ pub struct V4<'a> { | ||||
|     extra2: Output<'a>, | ||||
| } | ||||
|  | ||||
| struct InputOutput<'a> { | ||||
|     pin: Flex<'a>, | ||||
| } | ||||
|  | ||||
| pub(crate) async fn create_v4( | ||||
|     peripherals: FreePeripherals<'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) -> ! { | ||||
|         self.awake.set_low(); | ||||
|         //self.charger.power_save(); | ||||
|         self.charger.power_save(); | ||||
|         let rtc = TIME_ACCESS.get().await.lock().await; | ||||
|         self.esp.deep_sleep(duration_in_ms, rtc); | ||||
|     } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| use crate::bail; | ||||
| use crate::hal::{ADC1, TANK_MULTI_SAMPLE}; | ||||
| use crate::fat_error::FatError; | ||||
| use crate::hal::{ADC1, TANK_MULTI_SAMPLE}; | ||||
| use embassy_time::Timer; | ||||
| use esp_hal::analog::adc::{Adc, AdcConfig, AdcPin, Attenuation}; | ||||
| use esp_hal::delay::Delay; | ||||
| @@ -9,7 +9,6 @@ use esp_hal::pcnt::unit::Unit; | ||||
| use esp_hal::peripherals::GPIO5; | ||||
| use esp_hal::Blocking; | ||||
| use esp_println::println; | ||||
| use littlefs2::object_safe::DynStorage; | ||||
| use onewire::{ds18b20, Device, DeviceSearch, OneWire, DS18B20}; | ||||
|  | ||||
| pub struct TankSensor<'a> { | ||||
| @@ -34,7 +33,7 @@ impl<'a> TankSensor<'a> { | ||||
|  | ||||
|         let mut adc1_config = AdcConfig::new(); | ||||
|         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); | ||||
|  | ||||
| @@ -154,9 +153,7 @@ impl<'a> TankSensor<'a> { | ||||
|  | ||||
|         let mut store = [0_u16; TANK_MULTI_SAMPLE]; | ||||
|         for multisample in 0..TANK_MULTI_SAMPLE { | ||||
|             let mut asy = (&mut self.tank_channel); | ||||
|  | ||||
|             let value = asy.read_oneshot(&mut self.tank_pin); | ||||
|             let value = self.tank_channel.read_oneshot(&mut self.tank_pin); | ||||
|             //force yield | ||||
|             Timer::after_millis(10).await; | ||||
|             store[multisample] = value.unwrap(); | ||||
|   | ||||
| @@ -111,7 +111,10 @@ impl LogArray { | ||||
|         limit_length(txt_short, &mut txt_short_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 template: &str = message_key.into(); | ||||
| @@ -196,7 +199,7 @@ pub enum LogMessage { | ||||
|     StayAlive, | ||||
|     #[strum(serialize = "Connecting mqtt ${txt_short} with id ${txt_long}")] | ||||
|     MqttInfo, | ||||
|     #[strum(serialize = "Received stay alive with value ${txt_short}")] | ||||
|     #[strum(serialize = "Received stay alive with value ${number_a}")] | ||||
|     MqttStayAliveRec, | ||||
|     #[strum(serialize = "Unknown topic recieved ${txt_long}")] | ||||
|     UnknownTopic, | ||||
|   | ||||
							
								
								
									
										292
									
								
								rust/src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										292
									
								
								rust/src/main.rs
									
									
									
									
									
								
							| @@ -13,17 +13,18 @@ | ||||
| esp_bootloader_esp_idf::esp_app_desc!(); | ||||
| use esp_backtrace as _; | ||||
|  | ||||
| use crate::config::PlantConfig; | ||||
| use crate::config::{NetworkConfig, PlantConfig}; | ||||
| use crate::fat_error::FatResult; | ||||
| use crate::hal::esp::MQTT_STAY_ALIVE; | ||||
| use crate::hal::{esp_time, TIME_ACCESS}; | ||||
| use crate::log::LOG_ACCESS; | ||||
| use crate::tank::{determine_tank_state, TankError, WATER_FROZEN_THRESH}; | ||||
| use crate::webserver::httpd; | ||||
| use crate::tank::{determine_tank_state, TankError, TankState, WATER_FROZEN_THRESH}; | ||||
| use crate::webserver::http_server; | ||||
| use crate::{ | ||||
|     config::BoardVersion::INITIAL, | ||||
|     hal::{PlantHal, HAL, PLANT_COUNT}, | ||||
| }; | ||||
| use ::log::{error, info, warn}; | ||||
| use ::log::{info, warn}; | ||||
| use alloc::borrow::ToOwned; | ||||
| use alloc::string::{String, ToString}; | ||||
| use alloc::sync::Arc; | ||||
| @@ -65,7 +66,6 @@ mod fat_error; | ||||
| mod hal; | ||||
| mod log; | ||||
| mod plant_state; | ||||
| mod sipo; | ||||
| mod tank; | ||||
| mod webserver; | ||||
|  | ||||
| @@ -74,8 +74,6 @@ extern crate alloc; | ||||
|  | ||||
| pub static BOARD_ACCESS: OnceLock<Mutex<CriticalSectionRawMutex, HAL<'static>>> = OnceLock::new(); | ||||
|  | ||||
| pub static STAY_ALIVE: AtomicBool = AtomicBool::new(false); | ||||
|  | ||||
| #[derive(Serialize, Deserialize, Debug, PartialEq)] | ||||
| enum WaitType { | ||||
|     MissingConfig, | ||||
| @@ -167,7 +165,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { | ||||
|     let cur = match board.board_hal.get_rtc_module().get_rtc_time().await { | ||||
|         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); | ||||
|             } | ||||
|             value | ||||
| @@ -229,17 +227,17 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { | ||||
|         info!("no mode override"); | ||||
|     } | ||||
|  | ||||
|     if (board.board_hal.get_config().hardware.board == INITIAL | ||||
|         && board.board_hal.get_config().network.ssid.is_none()) | ||||
|     if board.board_hal.get_config().hardware.board == INITIAL | ||||
|         && board.board_hal.get_config().network.ssid.is_none() | ||||
|     { | ||||
|         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)); | ||||
|         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; | ||||
|     } | ||||
|  | ||||
| @@ -248,7 +246,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { | ||||
|         try_connect_wifi_sntp_mqtt(&mut board, &mut stack).await | ||||
|     } else { | ||||
|         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; | ||||
|         NetworkMode::OFFLINE | ||||
|     }; | ||||
| @@ -258,7 +256,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { | ||||
|  | ||||
|         let res = { | ||||
|             let esp = board.board_hal.get_esp(); | ||||
|             esp.wifi_ap(true).await | ||||
|             esp.wifi_ap().await | ||||
|         }; | ||||
|         match res { | ||||
|             Ok(ap_stack) => { | ||||
| @@ -277,7 +275,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { | ||||
|         }), | ||||
|         None => UTC, // Fallback to UTC if no timezone is set | ||||
|     }; | ||||
|     let _timezone = Tz::UTC; | ||||
|     let _timezone = UTC; | ||||
|  | ||||
|     let timezone_time = cur.with_timezone(&timezone); | ||||
|     info!( | ||||
| @@ -318,7 +316,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { | ||||
|         info!("executing config mode override"); | ||||
|         //config upload will trigger reboot! | ||||
|         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; | ||||
|     } else { | ||||
|         LOG_ACCESS | ||||
| @@ -396,11 +394,11 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { | ||||
|             _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(1, &mut board).await, | ||||
|         PlantState::read_hardware_state(2, &mut board).await, | ||||
| @@ -411,7 +409,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { | ||||
|         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 | ||||
|     //     .iter() | ||||
| @@ -568,12 +566,12 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { | ||||
|         info!("Lightstate is {:?}", light_state); | ||||
|     } | ||||
|  | ||||
|     match serde_json::to_string(&light_state) { | ||||
|     match &serde_json::to_string(&light_state) { | ||||
|         Ok(state) => { | ||||
|             let _ = board | ||||
|                 .board_hal | ||||
|                 .get_esp() | ||||
|                 .mqtt_publish("/light", state.as_bytes()) | ||||
|                 .mqtt_publish("/light", state) | ||||
|                 .await; | ||||
|         } | ||||
|         Err(err) => { | ||||
| @@ -587,25 +585,25 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { | ||||
|             let _ = board | ||||
|                 .board_hal | ||||
|                 .get_esp() | ||||
|                 .mqtt_publish("/deepsleep", "low Volt 12h".as_bytes()).await; | ||||
|                 .mqtt_publish("/deepsleep", "low Volt 12h").await; | ||||
|             12 * 60 | ||||
|         } else if is_day { | ||||
|             let _ = board | ||||
|                 .board_hal | ||||
|                 .get_esp() | ||||
|                 .mqtt_publish("/deepsleep", "normal 20m".as_bytes()).await; | ||||
|                 .mqtt_publish("/deepsleep", "normal 20m").await; | ||||
|             20 | ||||
|         } else { | ||||
|             let _ = board | ||||
|                 .board_hal | ||||
|                 .get_esp() | ||||
|                 .mqtt_publish("/deepsleep", "night 1h".as_bytes()).await; | ||||
|                 .mqtt_publish("/deepsleep", "night 1h").await; | ||||
|             60 | ||||
|         }; | ||||
|     let _ = board | ||||
|         .board_hal | ||||
|         .get_esp() | ||||
|         .mqtt_publish("/state", "sleep".as_bytes()) | ||||
|         .mqtt_publish("/state", "sleep") | ||||
|         .await; | ||||
|  | ||||
|     info!("Go to sleep for {} minutes", deep_sleep_duration_minutes); | ||||
| @@ -616,18 +614,17 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { | ||||
|     //TODO | ||||
|     //mark_app_valid(); | ||||
|  | ||||
|     let stay_alive_mqtt = STAY_ALIVE.load(Ordering::Relaxed); | ||||
|  | ||||
|     let stay_alive = stay_alive_mqtt; | ||||
|     let stay_alive = MQTT_STAY_ALIVE.load(Ordering::Relaxed); | ||||
|     info!("Check stay alive, current state is {}", stay_alive); | ||||
|  | ||||
|     if stay_alive { | ||||
|         info!("Go to stay alive move"); | ||||
|         let reboot_now = Arc::new(AtomicBool::new(false)); | ||||
|         //TODO | ||||
|         //let _webserver = httpd(reboot_now.clone()); | ||||
|         let _webserver = http_server(reboot_now.clone(), stack.take().unwrap()); | ||||
|         wait_infinity(board, WaitType::MqttConfig, reboot_now.clone()).await; | ||||
|     } 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 | ||||
| @@ -654,7 +651,6 @@ pub async fn do_secure_pump( | ||||
|         Timer::after_millis(10).await; | ||||
|         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 = 1; | ||||
|             flow_collector[step] = flow_value; | ||||
|             let flow_value_ml = flow_value as f32 * board.board_hal.get_config().tank.ml_per_pulse; | ||||
|  | ||||
| @@ -736,7 +732,7 @@ pub async fn do_secure_pump( | ||||
|                         error = true; | ||||
|                         break; | ||||
|                     } else { | ||||
|                         //eg v3 without a sensor ends here, do not spam | ||||
|                         //e.g., v3 without a sensor ends here, do not spam | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| @@ -744,9 +740,8 @@ pub async fn do_secure_pump( | ||||
|             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 = 12; | ||||
|     let flow_value_ml = final_flow_value as f32 * board.board_hal.get_config().tank.ml_per_pulse; | ||||
|     info!( | ||||
|         "Final flow value is {} with {} ml", | ||||
| @@ -764,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 | ||||
|     // let tank_state = determine_tank_state(&mut board); | ||||
|     // | ||||
|     // if tank_state.is_enabled() { | ||||
|     //     if let Some(err) = tank_state.got_error(&board.board_hal.get_config().tank) { | ||||
|     //         match err { | ||||
|     //             TankError::SensorDisabled => { /* unreachable */ } | ||||
|     //             TankError::SensorMissing(raw_value_mv) => log( | ||||
|     //                 LogMessage::TankSensorMissing, | ||||
|     //                 raw_value_mv as u32, | ||||
|     //                 0, | ||||
|     //                 "", | ||||
|     //                 "", | ||||
|     //             ).await, | ||||
|     //             TankError::SensorValueError { value, min, max } => log( | ||||
|     //                 LogMessage::TankSensorValueRangeError, | ||||
|     //                 min as u32, | ||||
|     //                 max as u32, | ||||
|     //                 &format!("{}", value), | ||||
|     //                 "", | ||||
|     //             ).await, | ||||
|     //             TankError::BoardError(err) => { | ||||
|     //                 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(LogMessage::TankWaterLevelLow, 0, 0, "", "").await; | ||||
|     //         board.board_hal.general_fault(true).await; | ||||
|     //     } | ||||
|     // } | ||||
|     let tank_state = determine_tank_state(board).await; | ||||
|  | ||||
|     if tank_state.is_enabled() { | ||||
|         if let Some(err) = tank_state.got_error(&board.board_hal.get_config().tank) { | ||||
|             match err { | ||||
|                 TankError::SensorDisabled => { /* unreachable */ } | ||||
|                 TankError::SensorMissing(raw_value_mv) => { | ||||
|                     LOG_ACCESS | ||||
|                         .lock() | ||||
|                         .await | ||||
|                         .log( | ||||
|                             LogMessage::TankSensorMissing, | ||||
|                             raw_value_mv as u32, | ||||
|                             0, | ||||
|                             "", | ||||
|                             "", | ||||
|                         ) | ||||
|                         .await | ||||
|                 } | ||||
|                 TankError::SensorValueError { value, min, max } => { | ||||
|                     LOG_ACCESS | ||||
|                         .lock() | ||||
|                         .await | ||||
|                         .log( | ||||
|                             LogMessage::TankSensorValueRangeError, | ||||
|                             min as u32, | ||||
|                             max as u32, | ||||
|                             &format!("{}", value), | ||||
|                             "", | ||||
|                         ) | ||||
|                         .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 { | ||||
|         let _ = board | ||||
|             .board_hal | ||||
| @@ -818,48 +835,39 @@ async fn update_charge_indicator(board: &mut MutexGuard<'_, CriticalSectionRawMu | ||||
|         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]) { | ||||
| //     let board = &mut BOARD_ACCESS.get().lock().await; | ||||
| //     for (plant_id, (plant_state, plant_conf)) in plantstate | ||||
| //         .iter() | ||||
| //         .zip(&board.board_hal.get_config().plants.clone()) | ||||
| //         .enumerate() | ||||
| //     { | ||||
| //         match serde_json::to_string(&plant_state.to_mqtt_info(plant_conf, timezone_time)) { | ||||
| //             Ok(state) => { | ||||
| //                 let plant_topic = format!("/plant{}", plant_id + 1); | ||||
| //                 let _ = board | ||||
| //                     .board_hal | ||||
| //                     .get_esp() | ||||
| //                     .mqtt_publish(&plant_topic, state.as_bytes()) | ||||
| //                     .await; | ||||
| //                 //TODO? reduce speed as else messages will be dropped | ||||
| //                 Timer::after_millis(200).await | ||||
| //             } | ||||
| //             Err(err) => { | ||||
| //                 error!("Error publishing plant state {}", err); | ||||
| //             } | ||||
| //         }; | ||||
| //     } | ||||
| // } | ||||
| async fn publish_tank_state( | ||||
|     board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, | ||||
|     tank_state: &TankState, | ||||
|     water_temp: FatResult<f32>, | ||||
| ) { | ||||
|     let state = serde_json::to_string( | ||||
|         &tank_state.as_mqtt_info(&board.board_hal.get_config().tank, &water_temp), | ||||
|     ) | ||||
|     .unwrap(); | ||||
|     let _ = board.board_hal.get_esp().mqtt_publish("/water", &*state); | ||||
| } | ||||
|  | ||||
| async fn publish_plant_states( | ||||
|     board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, | ||||
|     timezone_time: &DateTime<Tz>, | ||||
|     plantstate: &[PlantState; 8], | ||||
| ) { | ||||
|     for (plant_id, (plant_state, plant_conf)) in plantstate | ||||
|         .iter() | ||||
|         .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( | ||||
|     board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, | ||||
| @@ -868,30 +876,33 @@ async fn publish_firmware_info( | ||||
|     timezone_time: &String, | ||||
| ) { | ||||
|     let esp = board.board_hal.get_esp(); | ||||
|     let _ = esp.mqtt_publish("/firmware/address", ip_address).await; | ||||
|     let _ = esp | ||||
|         .mqtt_publish("/firmware/address", ip_address.as_bytes()) | ||||
|         .mqtt_publish("/firmware/githash", &version.git_hash) | ||||
|         .await; | ||||
|     let _ = esp | ||||
|         .mqtt_publish("/firmware/githash", version.git_hash.as_bytes()) | ||||
|         .mqtt_publish("/firmware/buildtime", &version.build_time) | ||||
|         .await; | ||||
|     let _ = esp | ||||
|         .mqtt_publish("/firmware/buildtime", version.build_time.as_bytes()) | ||||
|         .await; | ||||
|     let _ = esp.mqtt_publish("/firmware/last_online", timezone_time.as_bytes()); | ||||
|     let _ = esp.mqtt_publish("/firmware/last_online", timezone_time); | ||||
|     let state = esp.get_ota_state(); | ||||
|     let _ = esp | ||||
|         .mqtt_publish("/firmware/ota_state", state.as_bytes()) | ||||
|         .await; | ||||
|     let _ = esp.mqtt_publish("/firmware/ota_state", &state).await; | ||||
|     let slot = esp.get_ota_slot(); | ||||
|     let _ = esp | ||||
|         .mqtt_publish("/firmware/ota_slot", format!("slot{slot}").as_bytes()) | ||||
|         .mqtt_publish("/firmware/ota_slot", &format!("slot{slot}")) | ||||
|         .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( | ||||
|     board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>, | ||||
|     mut stack_store: &mut OptionLock<Stack<'static>>, | ||||
|     stack_store: &mut OptionLock<Stack<'static>>, | ||||
| ) -> NetworkMode { | ||||
|     let nw_conf = &board.board_hal.get_config().network.clone(); | ||||
|     match board.board_hal.get_esp().wifi(nw_conf).await { | ||||
| @@ -917,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 nw_config = &board.board_hal.get_config().network.clone(); | ||||
|                 match board.board_hal.get_esp().mqtt(nw_config).await { | ||||
|                 let nw_config = board.board_hal.get_config().network.clone(); | ||||
|                 let nw_config = mk_static!(NetworkConfig, nw_config); | ||||
|                 match board.board_hal.get_esp().mqtt(nw_config, stack).await { | ||||
|                     Ok(_) => { | ||||
|                         info!("Mqtt connection ready"); | ||||
|                         true | ||||
| @@ -957,25 +969,23 @@ async fn pump_info( | ||||
|     let pump_info = PumpInfo { | ||||
|         enabled: pump_active, | ||||
|         pump_ineffective, | ||||
|         median_current_ma: median_current_ma, | ||||
|         max_current_ma: max_current_ma, | ||||
|         min_current_ma: min_current_ma, | ||||
|         median_current_ma, | ||||
|         max_current_ma, | ||||
|         min_current_ma, | ||||
|     }; | ||||
|     let pump_topic = format!("/pump{}", plant_id + 1); | ||||
|  | ||||
|     match serde_json::to_string(&pump_info) { | ||||
|         Ok(state) => { | ||||
|             let _ = BOARD_ACCESS | ||||
|             BOARD_ACCESS | ||||
|                 .get() | ||||
|                 .await | ||||
|                 .lock() | ||||
|                 .await | ||||
|                 .board_hal | ||||
|                 .get_esp() | ||||
|                 .mqtt_publish(&pump_topic, state.as_bytes()); | ||||
|             //reduce speed as else messages will be dropped | ||||
|             //TODO maybee not required for low level hal? | ||||
|             Timer::after_millis(200).await; | ||||
|                 .mqtt_publish(&pump_topic, &state) | ||||
|                 .await; | ||||
|         } | ||||
|         Err(err) => { | ||||
|             warn!("Error publishing pump state {}", err); | ||||
| @@ -992,9 +1002,7 @@ async fn publish_mppt_state( | ||||
|         current_ma: current.as_milliamperes() as u32, | ||||
|         voltage_ma: voltage.as_millivolts() as u32, | ||||
|     }; | ||||
|     if let Ok(serialized_solar_state_bytes) = | ||||
|         serde_json::to_string(&solar_state).map(|s| s.into_bytes()) | ||||
|     { | ||||
|     if let Ok(serialized_solar_state_bytes) = serde_json::to_string(&solar_state) { | ||||
|         let _ = board | ||||
|             .board_hal | ||||
|             .get_esp() | ||||
| @@ -1014,9 +1022,9 @@ async fn publish_battery_state( | ||||
|     let value = match state { | ||||
|         Ok(state) => { | ||||
|             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 | ||||
| @@ -1083,7 +1091,7 @@ async fn wait_infinity( | ||||
|  | ||||
|         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); | ||||
|         } | ||||
|         if reboot_now.load(Ordering::Relaxed) { | ||||
| @@ -1107,7 +1115,7 @@ async fn main(spawner: Spawner) -> ! { | ||||
|     logger::init_logger_from_env(); | ||||
|     //force init here! | ||||
|     println!("Hal init"); | ||||
|     match BOARD_ACCESS.init(PlantHal::create(spawner).await.unwrap()) { | ||||
|     match BOARD_ACCESS.init(PlantHal::create().await.unwrap()) { | ||||
|         Ok(_) => {} | ||||
|         Err(_) => { | ||||
|             panic!("Could not set hal to static") | ||||
|   | ||||
| @@ -118,7 +118,11 @@ fn map_range_moisture( | ||||
| impl PlantState { | ||||
|     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 { | ||||
|             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( | ||||
|                     raw, | ||||
|                     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 { | ||||
|             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( | ||||
|                     raw, | ||||
|                     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), | ||||
|         } | ||||
|     } | ||||
|     // | ||||
|     // pub fn to_mqtt_info( | ||||
|     //     &self, | ||||
|     //     plant_conf: &PlantConfig, | ||||
|     //     current_time: &DateTime<Tz>, | ||||
|     // ) -> PlantInfo<'_> { | ||||
|     //     PlantInfo { | ||||
|     //         sensor_a: &self.sensor_a, | ||||
|     //         sensor_b: &self.sensor_b, | ||||
|     //         mode: plant_conf.mode, | ||||
|     //         do_water: self.needs_to_be_watered(plant_conf, current_time), | ||||
|     //         dry: if let Some(moisture_percent) = self.plant_moisture().0 { | ||||
|     //             moisture_percent < plant_conf.target_moisture | ||||
|     //         } else { | ||||
|     //             false | ||||
|     //         }, | ||||
|     //         cooldown: self.pump_in_timeout(plant_conf, current_time), | ||||
|     //         out_of_work_hour: in_time_range( | ||||
|     //             current_time, | ||||
|     //             plant_conf.pump_hour_start, | ||||
|     //             plant_conf.pump_hour_end, | ||||
|     //         ), | ||||
|     //         consecutive_pump_count: self.pump.consecutive_pump_count, | ||||
|     //         pump_error: self.pump.is_err(plant_conf), | ||||
|     //         last_pump: self | ||||
|     //             .pump | ||||
|     //             .previous_pump | ||||
|     //             .map(|t| t.with_timezone(¤t_time.timezone())), | ||||
|     //         next_pump: if matches!( | ||||
|     //             plant_conf.mode, | ||||
|     //             PlantWateringMode::TimerOnly | ||||
|     //                 | PlantWateringMode::TargetMoisture | ||||
|     //                 | PlantWateringMode::MinMoisture | ||||
|     //         ) { | ||||
|     //             self.pump.previous_pump.and_then(|last_pump| { | ||||
|     //                 last_pump | ||||
|     //                     .checked_add_signed(TimeDelta::minutes(plant_conf.pump_cooldown_min.into())) | ||||
|     //                     .map(|t| t.with_timezone(¤t_time.timezone())) | ||||
|     //             }) | ||||
|     //         } else { | ||||
|     //             None | ||||
|     //         }, | ||||
|     //     } | ||||
|     // } | ||||
|  | ||||
|     pub fn to_mqtt_info( | ||||
|         &self, | ||||
|         plant_conf: &PlantConfig, | ||||
|         current_time: &DateTime<Tz>, | ||||
|     ) -> PlantInfo<'_> { | ||||
|         PlantInfo { | ||||
|             sensor_a: &self.sensor_a, | ||||
|             sensor_b: &self.sensor_b, | ||||
|             mode: plant_conf.mode, | ||||
|             do_water: self.needs_to_be_watered(plant_conf, current_time), | ||||
|             dry: if let Some(moisture_percent) = self.plant_moisture().0 { | ||||
|                 moisture_percent < plant_conf.target_moisture | ||||
|             } else { | ||||
|                 false | ||||
|             }, | ||||
|             cooldown: self.pump_in_timeout(plant_conf, current_time), | ||||
|             out_of_work_hour: in_time_range( | ||||
|                 current_time, | ||||
|                 plant_conf.pump_hour_start, | ||||
|                 plant_conf.pump_hour_end, | ||||
|             ), | ||||
|             consecutive_pump_count: self.pump.consecutive_pump_count, | ||||
|             pump_error: self.pump.is_err(plant_conf), | ||||
|             last_pump: self | ||||
|                 .pump | ||||
|                 .previous_pump | ||||
|                 .map(|t| t.with_timezone(¤t_time.timezone())), | ||||
|             next_pump: if matches!( | ||||
|                 plant_conf.mode, | ||||
|                 PlantWateringMode::TimerOnly | ||||
|                     | PlantWateringMode::TargetMoisture | ||||
|                     | PlantWateringMode::MinMoisture | ||||
|             ) { | ||||
|                 self.pump.previous_pump.and_then(|last_pump| { | ||||
|                     last_pump | ||||
|                         .checked_add_signed(TimeDelta::minutes(plant_conf.pump_cooldown_min.into())) | ||||
|                         .map(|t| t.with_timezone(¤t_time.timezone())) | ||||
|                 }) | ||||
|             } else { | ||||
|                 None | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq, Serialize)] | ||||
| @@ -330,8 +338,8 @@ pub struct PlantInfo<'a> { | ||||
|     /// how often has the pump been watered without reaching target moisture | ||||
|     consecutive_pump_count: u32, | ||||
|     pump_error: Option<PumpError>, | ||||
|     // /// last time when the pump was active | ||||
|     // last_pump: Option<DateTime<Tz>>, | ||||
|     // /// next time when pump should activate | ||||
|     // next_pump: Option<DateTime<Tz>>, | ||||
|     /// last time when the pump was active | ||||
|     last_pump: Option<DateTime<Tz>>, | ||||
|     /// next time when pump should activate | ||||
|     next_pump: Option<DateTime<Tz>>, | ||||
| } | ||||
|   | ||||
| @@ -119,7 +119,7 @@ where | ||||
|     let mut offset = 0_usize; | ||||
|     let mut buf = [0_u8; 32]; | ||||
|  | ||||
|     let mut checksum = crate::hal::rtc::X25.digest(); | ||||
|     let mut checksum = X25.digest(); | ||||
|  | ||||
|     let mut counter = 0; | ||||
|     loop { | ||||
|   | ||||
| @@ -48,7 +48,7 @@ where | ||||
|             Some(200) | ||||
|         } | ||||
|         Method::Get => { | ||||
|             let disp = format!("attachment; filename=\"{filename}\""); | ||||
|             let disposition = format!("attachment; filename=\"{filename}\""); | ||||
|             let size = { | ||||
|                 let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|                 board | ||||
| @@ -63,7 +63,7 @@ where | ||||
|                 Some("OK"), | ||||
|                 &[ | ||||
|                     ("Content-Type", "application/octet-stream"), | ||||
|                     ("Content-Disposition", disp.as_str()), | ||||
|                     ("Content-Disposition", disposition.as_str()), | ||||
|                     ("Content-Length", &format!("{}", size)), | ||||
|                     ("Access-Control-Allow-Origin", "*"), | ||||
|                     ("Access-Control-Allow-Headers", "*"), | ||||
| @@ -75,7 +75,7 @@ where | ||||
|             let mut chunk = 0; | ||||
|             loop { | ||||
|                 let mut board = BOARD_ACCESS.get().await.lock().await; | ||||
|                 board.board_hal.progress(chunk as u32).await; | ||||
|                 board.board_hal.progress(chunk).await; | ||||
|                 let read_chunk = board | ||||
|                     .board_hal | ||||
|                     .get_esp() | ||||
| @@ -107,7 +107,7 @@ where | ||||
|         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 | ||||
|                 //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() | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| use crate::fat_error::{FatError, FatResult}; | ||||
| use crate::hal::{esp_time, PLANT_COUNT}; | ||||
| use crate::log::{LogMessage, LOG_ACCESS}; | ||||
| use crate::log::LogMessage; | ||||
| use crate::plant_state::{MoistureSensorState, PlantState}; | ||||
| use crate::tank::determine_tank_state; | ||||
| use crate::{get_version, BOARD_ACCESS}; | ||||
| @@ -11,7 +11,6 @@ use chrono_tz::Tz; | ||||
| use core::str::FromStr; | ||||
| use edge_http::io::server::Connection; | ||||
| use embedded_io_async::{Read, Write}; | ||||
| use log::info; | ||||
| use serde::Serialize; | ||||
|  | ||||
| #[derive(Serialize, Debug)] | ||||
|   | ||||
		Reference in New Issue
	
	Block a user