use crate::config::{NetworkConfig, PlantControllerConfig}; use crate::hal::{GW_IP_ADDR_ENV, PLANT_COUNT}; use crate::log::{log, LogMessage}; use crate::STAY_ALIVE; use anyhow::{anyhow, bail, Context}; use chrono::{DateTime, Utc}; use serde::Serialize; use alloc::{string::String, vec::Vec}; use core::marker::PhantomData; use core::net::{IpAddr, Ipv4Addr}; use core::str::FromStr; use embassy_executor::{SendSpawner, Spawner}; use embassy_net::tcp::TcpSocket; use embassy_net::{IpListenEndpoint, Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4}; use embassy_time::{Duration, Instant, Timer}; use esp_bootloader_esp_idf::ota::OtaImageState; use esp_hal::gpio::Input; use esp_hal::rng::Rng; use esp_println::{print, println}; use esp_storage::FlashStorage; use esp_wifi::wifi::{ AccessPointConfiguration, Configuration, Interfaces, WifiController, WifiDevice, WifiEvent, WifiState, }; #[link_section = ".rtc.data"] static mut LAST_WATERING_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT]; #[link_section = ".rtc.data"] static mut CONSECUTIVE_WATERING_PLANT: [u32; PLANT_COUNT] = [0; PLANT_COUNT]; #[link_section = ".rtc.data"] static mut LOW_VOLTAGE_DETECTED: bool = false; #[link_section = ".rtc.data"] static mut RESTART_TO_CONF: bool = false; #[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> { dummy: PhantomData<&'a ()>, //mqtt_client: EspMqttClient<'a>, base_topic: heapless::String<64>, } pub struct Esp<'a> { pub rng: Rng, //first starter (ap or sta will take these) pub interfaces: Option>, pub controller: Option>, //only filled, if a useable mqtt client with working roundtrip could be established pub(crate) mqtt_client: Option>, pub boot_button: Input<'a>, pub(crate) wall_clock_offset: u64, pub storage: FlashStorage, pub slot: usize, pub next_slot: usize, pub ota_state: OtaImageState, } pub struct IpInfo { pub(crate) ip: IpAddr, netmask: IpAddr, gateway: IpAddr, } struct AccessPointInfo {} 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 }}; } static WIFI_CONTROLLER: static_cell::StaticCell = static_cell::StaticCell::new(); 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 mode_override_pressed(&mut self) -> bool { self.boot_button.is_low() } pub(crate) async fn sntp(&mut self, _max_wait_ms: u32) -> anyhow::Result> { //let sntp = sntp::EspSntp::new_default()?; //let mut counter = 0; //while sntp.get_sync_status() != SyncStatus::Completed { // self.delay.delay_ms(100); // counter += 100; // if counter > max_wait_ms { // bail!("Reached sntp timeout, aborting") // } //} //self.time() todo!(); } pub(crate) fn time(&mut self) -> DateTime { let wall_clock = Instant::now().as_millis() + self.wall_clock_offset; DateTime::from_timestamp_millis(wall_clock as i64).unwrap() } pub(crate) async fn wifi_scan(&mut self) -> anyhow::Result> { bail!("todo"); // self.wifi_driver.start_scan( // &ScanConfig { // scan_type: ScanType::Passive(Duration::from_secs(5)), // show_hidden: false, // ..Default::default() // }, // true, // )?; // anyhow::Ok(self.wifi_driver.get_scan_result()?) } pub(crate) fn last_pump_time(&self, plant: usize) -> Option> { let ts = unsafe { LAST_WATERING_TIMESTAMP }[plant]; DateTime::from_timestamp_millis(ts) } pub(crate) fn store_last_pump_time(&mut self, plant: usize, time: DateTime) { unsafe { LAST_WATERING_TIMESTAMP[plant] = time.timestamp_millis(); } } pub(crate) fn set_low_voltage_in_cycle(&mut self) { unsafe { LOW_VOLTAGE_DETECTED = true; } } pub(crate) fn clear_low_voltage_in_cycle(&mut self) { unsafe { LOW_VOLTAGE_DETECTED = false; } } pub(crate) fn low_voltage_in_cycle(&mut self) -> bool { unsafe { LOW_VOLTAGE_DETECTED } } pub(crate) fn store_consecutive_pump_count(&mut self, plant: usize, count: u32) { unsafe { CONSECUTIVE_WATERING_PLANT[plant] = count; } } pub(crate) fn consecutive_pump_count(&mut self, plant: usize) -> u32 { unsafe { CONSECUTIVE_WATERING_PLANT[plant] } } pub(crate) fn get_restart_to_conf(&mut self) -> bool { unsafe { RESTART_TO_CONF } } pub(crate) fn set_restart_to_conf(&mut self, to_conf: bool) { unsafe { RESTART_TO_CONF = to_conf; } } pub(crate) async fn wifi_ap(&mut self) -> anyhow::Result> { let _ssid = match self.load_config() { Ok(config) => config.network.ap_ssid.clone(), Err(_) => heapless::String::from_str("PlantCtrl Emergency Mode").unwrap(), }; let spawner = Spawner::for_current_executor().await; let device = self.interfaces.take().unwrap().ap; let gw_ip_addr_str = GW_IP_ADDR_ENV.unwrap_or("192.168.2.1"); let gw_ip_addr = Ipv4Addr::from_str(gw_ip_addr_str).expect("failed to parse gateway ip"); let config = embassy_net::Config::ipv4_static(StaticConfigV4 { address: Ipv4Cidr::new(gw_ip_addr, 24), gateway: Some(gw_ip_addr), dns_servers: Default::default(), }); let seed = (self.rng.random() as u64) << 32 | self.rng.random() as u64; // Init network stack let (stack, runner) = embassy_net::new( device, config, mk_static!(StackResources<3>, StackResources::<3>::new()), seed, ); let stack = mk_static!(Stack, stack); let controller = self.controller.take().unwrap(); spawner.spawn(connection(controller)).ok(); spawner.spawn(net_task(runner)).ok(); spawner.spawn(run_dhcp(stack.clone(), gw_ip_addr_str)).ok(); loop { if stack.is_link_up() { break; } Timer::after(Duration::from_millis(500)).await; } println!( "Connect to the AP `esp-wifi` and point your browser to http://{gw_ip_addr_str}:8080/" ); println!("DHCP is enabled so there's no need to configure a static IP, just in case:"); while !stack.is_config_up() { Timer::after(Duration::from_millis(100)).await } stack .config_v4() .inspect(|c| println!("ipv4 config: {c:?}")); anyhow::Ok(stack.clone()) } pub(crate) async 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; bail!("todo") // 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"); // } // } // log::info!("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 load_config(&mut self) -> anyhow::Result { bail!("todo"); // let cfg = File::open(Self::CONFIG_FILE)?; // let config: PlantControllerConfig = serde_json::from_reader(cfg)?; // anyhow::Ok(config) } pub(crate) async fn save_config( &mut self, _config: &PlantControllerConfig, ) -> anyhow::Result<()> { bail!("todo"); // let mut cfg = File::create(Self::CONFIG_FILE)?; // serde_json::to_writer(&mut cfg, &config)?; // log::info!("Wrote config config {:?}", config); // anyhow::Ok(()) } pub(crate) fn mount_file_system(&mut self) -> anyhow::Result<()> { bail!("fail"); // log(LogMessage::MountingFilesystem, 0, 0, "", ""); // let base_path = String::try_from("/spiffs")?; // let storage = String::try_from(Self::SPIFFS_PARTITION_NAME)?; //let conf = todo!(); //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, //}; //TODO //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(()) } async fn file_system_size(&mut self) -> anyhow::Result { bail!("fail"); // 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) async fn list_files(&self) -> FileList { return FileList { total: 0, used: 0, file_system_corrupt: None, files: Vec::new(), iter_error: None, }; // // 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().map(|it| 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) async fn delete_file(&self, _filename: &str) -> anyhow::Result<()> { bail!("todo"); // 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) async 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 { log::info!( "LAST_WATERING_TIMESTAMP[{}] = UTC {}", i, LAST_WATERING_TIMESTAMP[i] ); } for i in 0..PLANT_COUNT { log::info!( "CONSECUTIVE_WATERING_PLANT[{}] = {}", i, CONSECUTIVE_WATERING_PLANT[i] ); } } } } pub(crate) async 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") } 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"); } pub(crate) async fn mqtt_publish( &mut self, _subtopic: &str, _message: &[u8], ) -> anyhow::Result<()> { 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)? // } // } } } #[embassy_executor::task] async fn run_dhcp(stack: Stack<'static>, gw_ip_addr: &'static str) { use core::net::{Ipv4Addr, SocketAddrV4}; use edge_dhcp::{ io::{self, DEFAULT_SERVER_PORT}, server::{Server, ServerOptions}, }; use edge_nal::UdpBind; use edge_nal_embassy::{Udp, UdpBuffers}; let ip = Ipv4Addr::from_str(gw_ip_addr).expect("dhcp task failed to parse gw ip"); let mut buf = [0u8; 1500]; let mut gw_buf = [Ipv4Addr::UNSPECIFIED]; 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( Ipv4Addr::UNSPECIFIED, DEFAULT_SERVER_PORT, ))) .await .unwrap(); loop { _ = io::server::run( &mut Server::<_, 64>::new_with_et(ip), &ServerOptions::new(ip, Some(&mut gw_buf)), &mut bound_socket, &mut buf, ) .await .inspect_err(|e| log::warn!("DHCP server error: {e:?}")); Timer::after(Duration::from_millis(500)).await; } } #[embassy_executor::task] async fn connection(mut controller: WifiController<'static>) { println!("start connection task"); println!("Device capabilities: {:?}", controller.capabilities()); loop { match esp_wifi::wifi::wifi_state() { WifiState::ApStarted => { // wait until we're no longer connected controller.wait_for_event(WifiEvent::ApStop).await; Timer::after(Duration::from_millis(5000)).await } _ => {} } if !matches!(controller.is_started(), core::result::Result::Ok(true)) { let client_config = Configuration::AccessPoint(AccessPointConfiguration { ssid: "esp-wifi".try_into().unwrap(), ..Default::default() }); controller.set_configuration(&client_config).unwrap(); println!("Starting wifi"); controller.start_async().await.unwrap(); println!("Wifi started!"); } } } #[embassy_executor::task] async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) { runner.run().await }