use crate::config::{NetworkConfig, PlantControllerConfig}; use crate::hal::{CONSECUTIVE_WATERING_PLANT, LAST_WATERING_TIMESTAMP, LOW_VOLTAGE_DETECTED, PLANT_COUNT, RESTART_TO_CONF}; use crate::log::{log, LogMessage}; use anyhow::{anyhow, bail, Context}; use embedded_svc::ipv4::IpInfo; use embedded_svc::wifi::{AccessPointConfiguration, AuthMethod, ClientConfiguration, Configuration}; use esp_idf_hal::delay::Delay; use esp_idf_hal::gpio::PinDriver; use esp_idf_svc::mqtt::client::{EspMqttClient, LwtConfiguration, MqttClientConfiguration}; use esp_idf_svc::wifi::EspWifi; use esp_idf_sys::{esp_spiffs_info, vTaskDelay}; use std::ffi::CString; use std::fs; use std::fs::File; use std::path::Path; use std::result::Result::Ok as OkStd; use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::sync::atomic::AtomicBool; use std::time::Duration; use embedded_svc::mqtt::client::QoS::{AtLeastOnce, ExactlyOnce}; use esp_idf_hal::i2c::I2cDriver; use serde::Serialize; use crate::STAY_ALIVE; #[derive(Serialize, Debug)] pub struct FileInfo { filename: String, size: usize, } #[derive(Serialize, Debug)] pub struct FileList { total: usize, used: usize, files: Vec, file_system_corrupt: Option, iter_error: Option, } pub struct FileSystemSizeInfo { pub total_size: usize, pub used_size: usize, pub free_size: usize, } pub struct MqttClient<'a> { mqtt_client: EspMqttClient<'a>, base_topic: heapless::String<64> } pub struct ESP<'a> { pub(crate) mqtt_client: Option>, pub(crate) wifi_driver: EspWifi<'a>, pub(crate) boot_button: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, esp_idf_hal::gpio::Input>, pub(crate) delay: Delay } impl ESP<'_> { const SPIFFS_PARTITION_NAME: &'static str = "storage"; const CONFIG_FILE: &'static str = "/spiffs/config.cfg"; const BASE_PATH: &'static str = "/spiffs"; pub(crate) fn wifi_ap(&mut self) -> anyhow::Result<()> { let ssid = match self.get_config(){ Ok(config) => { config.network.ap_ssid.clone() } Err(_) => { heapless::String::from_str("PlantCtrl Emergency Mode").unwrap() } }; let apconfig = AccessPointConfiguration { ssid, auth_method: AuthMethod::None, ssid_hidden: false, ..Default::default() }; self.wifi_driver.set_configuration(&Configuration::AccessPoint(apconfig))?; self.wifi_driver.start()?; anyhow::Ok(()) } pub(crate) fn wifi( &mut self, network_config: &NetworkConfig ) -> anyhow::Result { let ssid = network_config.ssid.clone().ok_or(anyhow!("No ssid configured"))?; let password = network_config.password.clone(); let max_wait = network_config.max_wait; match password { Some(pw) => { //TODO expect error due to invalid pw or similar! //call this during configuration and check if works, revert to config mode if not self.wifi_driver.set_configuration(&Configuration::Client( ClientConfiguration { ssid, password: pw, ..Default::default() }, ))?; } None => { self.wifi_driver.set_configuration(&Configuration::Client( ClientConfiguration { ssid, auth_method: AuthMethod::None, ..Default::default() }, ))?; } } self.wifi_driver.start()?; self.wifi_driver.connect()?; let delay = Delay::new_default(); let mut counter = 0_u32; while !self.wifi_driver.is_connected()? { delay.delay_ms(250); counter += 250; if counter > max_wait { //ignore these errors, Wi-Fi will not be used this self.wifi_driver.disconnect().unwrap_or(()); self.wifi_driver.stop().unwrap_or(()); bail!("Did not manage wifi connection within timeout"); } } println!("Should be connected now, waiting for link to be up"); while !self.wifi_driver.is_up()? { delay.delay_ms(250); counter += 250; if counter > max_wait { //ignore these errors, Wi-Fi will not be used this self.wifi_driver.disconnect().unwrap_or(()); self.wifi_driver.stop().unwrap_or(()); bail!("Did not manage wifi connection within timeout"); } } //update freertos registers ;) let address = self.wifi_driver.sta_netif().get_ip_info()?; log(LogMessage::WifiInfo, 0, 0, "", &format!("{address:?}")); anyhow::Ok(address) } pub(crate) fn get_config(&mut self) -> anyhow::Result { let cfg = File::open(Self::CONFIG_FILE)?; let config: PlantControllerConfig = serde_json::from_reader(cfg)?; anyhow::Ok(config) } pub(crate) fn set_config(&mut self, config: &PlantControllerConfig) -> anyhow::Result<()> { let mut cfg = File::create(Self::CONFIG_FILE)?; serde_json::to_writer(&mut cfg, &config)?; println!("Wrote config config {:?}", config); anyhow::Ok(()) } pub(crate) fn delete_config(&self) -> anyhow::Result<()> { let config = Path::new(Self::CONFIG_FILE); if config.exists() { println!("Removing config"); fs::remove_file(config)? } anyhow::Ok(()) } pub(crate) fn mount_file_system(&mut self) -> anyhow::Result<()> { log(LogMessage::MountingFilesystem, 0, 0, "", ""); let base_path = CString::new("/spiffs")?; let storage = CString::new(Self::SPIFFS_PARTITION_NAME)?; let conf = esp_idf_sys::esp_vfs_spiffs_conf_t { base_path: base_path.as_ptr(), partition_label: storage.as_ptr(), max_files: 5, format_if_mount_failed: true, }; unsafe { esp_idf_sys::esp!(esp_idf_sys::esp_vfs_spiffs_register(&conf))?; } let free_space = self.file_system_size()?; log( LogMessage::FilesystemMount, free_space.free_size as u32, free_space.total_size as u32, &free_space.used_size.to_string(), "", ); anyhow::Ok(()) } fn file_system_size(&mut self) -> anyhow::Result { let storage = CString::new(Self::SPIFFS_PARTITION_NAME)?; let mut total_size = 0; let mut used_size = 0; unsafe { esp_idf_sys::esp!(esp_spiffs_info( storage.as_ptr(), &mut total_size, &mut used_size ))?; } anyhow::Ok(FileSystemSizeInfo { total_size, used_size, free_size: total_size - used_size, }) } pub(crate) fn list_files(&self) -> FileList { let storage = CString::new(Self::SPIFFS_PARTITION_NAME).unwrap(); let mut file_system_corrupt = None; let mut iter_error = None; let mut result = Vec::new(); let filepath = Path::new(Self::BASE_PATH); let read_dir = fs::read_dir(filepath); match read_dir { OkStd(read_dir) => { for item in read_dir { match item { OkStd(file) => { let f = FileInfo { filename: file.file_name().into_string().unwrap(), size: file .metadata() .and_then(|it| anyhow::Result::Ok(it.len())) .unwrap_or_default() as usize, }; result.push(f); } Err(err) => { iter_error = Some(format!("{err:?}")); break; } } } } Err(err) => { file_system_corrupt = Some(format!("{err:?}")); } } let mut total: usize = 0; let mut used: usize = 0; unsafe { esp_spiffs_info(storage.as_ptr(), &mut total, &mut used); } FileList { total, used, file_system_corrupt, files: result, iter_error, } } pub(crate) fn delete_file(&self, filename: &str) -> anyhow::Result<()> { let filepath = Path::new(Self::BASE_PATH).join(Path::new(filename)); match fs::remove_file(filepath) { OkStd(_) => anyhow::Ok(()), Err(err) => { bail!(format!("{err:?}")) } } } pub(crate) fn get_file_handle(&self, filename: &str, write: bool) -> anyhow::Result { let filepath = Path::new(Self::BASE_PATH).join(Path::new(filename)); anyhow::Ok(if write { File::create(filepath)? } else { File::open(filepath)? }) } pub(crate) fn init_rtc_deepsleep_memory(&self, init_rtc_store: bool, to_config_mode: bool){ if init_rtc_store { unsafe { LAST_WATERING_TIMESTAMP = [0; PLANT_COUNT]; CONSECUTIVE_WATERING_PLANT = [0; PLANT_COUNT]; LOW_VOLTAGE_DETECTED = false; crate::log::init(); RESTART_TO_CONF = to_config_mode; }; } else { unsafe { if to_config_mode { RESTART_TO_CONF = true; } log( LogMessage::RestartToConfig, RESTART_TO_CONF as u32, 0, "", "", ); log( LogMessage::LowVoltage, LOW_VOLTAGE_DETECTED as u32, 0, "", "", ); for i in 0..PLANT_COUNT { println!( "LAST_WATERING_TIMESTAMP[{}] = UTC {}", i, LAST_WATERING_TIMESTAMP[i] ); } for i in 0..PLANT_COUNT { println!( "CONSECUTIVE_WATERING_PLANT[{}] = {}", i, CONSECUTIVE_WATERING_PLANT[i] ); } } } } pub(crate) fn mqtt(&mut self, network_config: &NetworkConfig) -> anyhow::Result<()> { let base_topic = network_config .base_topic .as_ref() .context("missing base topic")?; if base_topic.is_empty() { bail!("Mqtt base_topic was empty") } let base_topic_copy = base_topic.clone(); let mqtt_url = network_config .mqtt_url .as_ref() .context("missing mqtt url")?; if mqtt_url.is_empty() { bail!("Mqtt url was empty") } 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)), //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); println!("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); println!("Mqtt disconnected"); } esp_idf_svc::mqtt::client::EventPayload::Error(esp_error) => { println!("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); println!("Mqtt error"); } esp_idf_svc::mqtt::client::EventPayload::BeforeConnect => { println!("Mqtt before connect") } esp_idf_svc::mqtt::client::EventPayload::Subscribed(_) => { println!("Mqtt subscribed") } esp_idf_svc::mqtt::client::EventPayload::Unsubscribed(_) => { println!("Mqtt unsubscribed") } esp_idf_svc::mqtt::client::EventPayload::Published(_) => { println!("Mqtt published") } esp_idf_svc::mqtt::client::EventPayload::Deleted(_) => { println!("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 => { println!("Mqtt connection callback received, progressing"); match mqtt_connected_event_ok.load(std::sync::atomic::Ordering::Relaxed) { true => { println!("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 => { println!("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) fn mqtt_publish( &mut self, subtopic: &str, message: &[u8], ) -> anyhow::Result<()> { if self.mqtt_client.is_none() { return anyhow::Ok(()); } if !subtopic.starts_with("/") { println!("Subtopic without / at start {}", subtopic); bail!("Subtopic without / at start {}", subtopic); } if subtopic.len() > 192 { println!("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() { println!("Some error assembling full_topic 1"); bail!("Some error assembling full_topic 1") }; if full_topic.push_str(subtopic).is_err() { println!("Some error assembling full_topic 2"); bail!("Some error assembling full_topic 2") }; let publish = client.mqtt_client.publish(&full_topic, ExactlyOnce, true, message); Delay::new(10).delay_ms(50); match publish { OkStd(message_id) => { println!( "Published mqtt topic {} with message {:#?} msgid is {:?}", full_topic, String::from_utf8_lossy(message), message_id ); anyhow::Ok(()) } Err(err) => { println!( "Error during mqtt send on topic {} with message {:#?} error is {:?}", full_topic, String::from_utf8_lossy(message), err ); Err(err)? } } } }