use crate::bail; use crate::config::{NetworkConfig, PlantControllerConfig}; use crate::hal::PLANT_COUNT; use crate::log::{log, LogMessage}; use alloc::vec; use chrono::{DateTime, Utc}; use esp_hal::Blocking; use esp_hal::uart::Uart; use serde::Serialize; use crate::fat_error::{ContextExt, FatError, FatResult}; use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem; use alloc::string::ToString; use alloc::sync::Arc; use alloc::{format, string::String, vec::Vec}; use core::net::{IpAddr, Ipv4Addr, SocketAddr}; use core::str::FromStr; use core::sync::atomic::Ordering; use embassy_executor::Spawner; use embassy_net::{DhcpConfig, IpAddress, Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4}; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::mutex::{Mutex, MutexGuard}; use embassy_time::{Duration, Timer, WithTimeout}; use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash, RmwNorFlashStorage}; use esp_bootloader_esp_idf::ota::OtaImageState::Valid; use esp_bootloader_esp_idf::ota::{Ota, OtaImageState}; use esp_bootloader_esp_idf::partitions::{AppPartitionSubType, FlashRegion}; use esp_hal::gpio::{Input, RtcPinWithResistors}; use esp_hal::rng::Rng; use esp_hal::rtc_cntl::{ sleep::{TimerWakeupSource, WakeupLevel}, Rtc, }; use esp_hal::system::software_reset; use esp_println::println; use esp_radio::wifi::ap::{AccessPointConfig, AccessPointInfo}; use esp_radio::wifi::scan::{ScanConfig, ScanTypeConfig}; use esp_radio::wifi::sta::StationConfig; use esp_radio::wifi::{AuthenticationMethod, Config, Interface, WifiController}; use esp_storage::FlashStorage; use littlefs2::fs::Filesystem; use littlefs2_core::{FileType, PathBuf, SeekFrom}; use log::{info, warn, error}; use portable_atomic::AtomicBool; use super::shared_flash::MutexFlashStorage; use crate::network::{net_task, run_dhcp}; #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))] static mut LAST_WATERING_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT]; #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))] static mut CONSECUTIVE_WATERING_PLANT: [u32; PLANT_COUNT] = [0; PLANT_COUNT]; #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))] static mut LOW_VOLTAGE_DETECTED: i8 = 0; #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))] static mut RESTART_TO_CONF: i8 = 0; #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))] static mut LAST_CORROSION_PROTECTION_CHECK_DAY: i8 = -1; const CONFIG_FILE: &str = "config.json"; #[derive(Serialize, Debug)] pub struct FileInfo { filename: String, size: usize, } #[derive(Serialize, Debug)] pub struct FileList { total: usize, used: usize, files: Vec, } // Minimal esp-idf equivalent for gpio_hold on esp32c6 via ROM functions extern "C" { fn gpio_pad_hold(gpio_num: u32); fn gpio_pad_unhold(gpio_num: u32); } #[inline(always)] pub fn hold_enable(gpio_num: u8) { unsafe { gpio_pad_hold(gpio_num as u32) } } #[inline(always)] pub fn hold_disable(gpio_num: u8) { unsafe { gpio_pad_unhold(gpio_num as u32) } } pub struct Esp<'a> { pub fs: Arc>>, pub rng: Rng, //first starter (ap or sta will take these) pub interface_sta: Option>, pub interface_ap: Option>, pub controller: Arc>>, pub boot_button: Input<'a>, // RTC-capable GPIO used as external wake source (store the raw peripheral) pub wake_gpio1: esp_hal::peripherals::GPIO1<'static>, pub uart0: Uart<'a, Blocking>, pub rtc: Rtc<'a>, pub ota: Ota<'static, RmwNorFlashStorage<'static, &'static mut MutexFlashStorage>>, pub ota_target: &'static mut FlashRegion<'static, MutexFlashStorage>, pub current: AppPartitionSubType, pub slot0_state: OtaImageState, pub slot1_state: OtaImageState, } // SAFETY: On this target we never move Esp across OS threads; the firmware runs single-core // cooperative tasks with Embassy. All interior mutability of non-Send peripherals is gated // behind &mut self or embassy_sync Mutex with CriticalSectionRawMutex, which does not rely on // thread scheduling. Therefore it is sound to mark Esp as Send to satisfy trait object bounds // (e.g., Box). If you add fields that are accessed from multiple // CPU cores/threads, reconsider this. unsafe impl Send for Esp<'_> {} impl Esp<'_> { pub fn get_time(&self) -> DateTime { DateTime::from_timestamp_micros(self.rtc.current_time_us() as i64) .unwrap_or(DateTime::UNIX_EPOCH) } pub fn set_time(&mut self, time: DateTime) { self.rtc.set_current_time_us(time.timestamp_micros() as u64); } pub(crate) async fn read_serial_line(&mut self) -> FatResult> { let mut buf = [0u8; 1]; let mut line = String::new(); loop { match self.uart0.read_buffered(&mut buf) { Ok(read) => { if read == 0 { return Ok(None); } let c = buf[0] as char; if c == '\n' { return Ok(Some(line)); } line.push(c); } Err(error) => { if line.is_empty() { return Ok(None); } else { error!("Error reading serial line: {error:?}"); // If we already have some data, we should probably wait a bit or just return what we have? // But the protocol expects a full line or message. // For simplicity in config mode, we can block here or just return None if nothing is there yet. // However, if we started receiving, we should probably finish or timeout. continue; } } } } } pub(crate) async fn delete_file(&self, filename: String) -> FatResult<()> { let file = PathBuf::try_from(filename.as_str())?; let access = self.fs.lock().await; access.remove(&*file)?; Ok(()) } pub(crate) async fn write_file( &mut self, filename: String, offset: u32, buf: &[u8], ) -> Result<(), FatError> { let file = PathBuf::try_from(filename.as_str())?; let access = self.fs.lock().await; access.open_file_with_options_and_then( |options| options.read(true).write(true).create(true), &*file, |file| { file.seek(SeekFrom::Start(offset))?; file.write(buf)?; Ok(()) }, )?; Ok(()) } pub async fn get_size(&mut self, filename: String) -> FatResult { let file = PathBuf::try_from(filename.as_str())?; let access = self.fs.lock().await; let data = access.metadata(&*file)?; Ok(data.len()) } pub(crate) async fn get_file( &mut self, filename: String, chunk: u32, ) -> FatResult<([u8; 512], usize)> { use littlefs2::io::Error as lfs2Error; let file = PathBuf::try_from(filename.as_str())?; let access = self.fs.lock().await; let mut buf = [0_u8; 512]; let mut read = 0; let offset = chunk * buf.len() as u32; access.open_file_with_options_and_then( |options| options.read(true), &*file, |file| { let length = file.len()? as u32; if length == 0 { Err(lfs2Error::IO) } else if length > offset { file.seek(SeekFrom::Start(offset))?; read = file.read(&mut buf)?; Ok(()) } else { //exactly at end, do nothing Ok(()) } }, )?; Ok((buf, read)) } pub(crate) async fn write_ota(&mut self, offset: u32, buf: &[u8]) -> Result<(), FatError> { let _ = check_erase(self.ota_target, offset, offset + 4096); info!("erasing and writing block 0x{offset:x}"); self.ota_target.erase(offset, offset + 4096)?; let mut temp = vec![0; buf.len()]; let read_back = temp.as_mut_slice(); //change to nor flash, align writes! self.ota_target.write(offset, buf)?; self.ota_target.read(offset, read_back)?; if buf != read_back { info!("Expected {buf:?} but got {read_back:?}"); bail!( "Flash error, read back does not match write buffer at offset {:x}", offset ) } Ok(()) } pub(crate) async fn finalize_ota(&mut self) -> Result<(), FatError> { let current = self.ota.current_app_partition()?; if self.ota.current_ota_state()? != Valid { info!("Validating current slot {current:?} as it was able to ota"); self.ota.set_current_ota_state(Valid)?; } let next = match current { AppPartitionSubType::Ota0 => AppPartitionSubType::Ota1, AppPartitionSubType::Ota1 => AppPartitionSubType::Ota0, _ => { bail!("Invalid current slot {current:?} for ota"); } }; self.ota.set_current_app_partition(next)?; info!("switched slot"); self.ota.set_current_ota_state(OtaImageState::New)?; info!("switched state for new partition"); let state_new = self.ota.current_ota_state()?; info!("state on new partition now {state_new:?}"); self.set_restart_to_conf(true); Ok(()) } // let current = ota.current_slot()?; // println!( // "current image state {:?} (only relevant if the bootloader was built with auto-rollback support)", // ota.current_ota_state() // ); // println!("current {:?} - next {:?}", current, current.next()); // let ota_state = ota.current_ota_state()?; pub(crate) fn mode_override_pressed(&mut self) -> bool { self.boot_button.is_low() } pub(crate) async fn wifi_scan(&mut self) -> FatResult> { info!("start wifi scan"); let mut lock = self.controller.try_lock()?; info!("start wifi scan lock"); let scan_config = ScanConfig::default().with_scan_type(ScanTypeConfig::Active { min: esp_hal::time::Duration::from_millis(0), max: esp_hal::time::Duration::from_millis(0), }); let rv = lock.scan_async(&scan_config).await?; info!("end wifi scan lock"); Ok(rv) } 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 = 1; } } pub(crate) fn clear_low_voltage_in_cycle(&mut self) { unsafe { LOW_VOLTAGE_DETECTED = 0; } } pub(crate) fn low_voltage_in_cycle(&mut self) -> bool { unsafe { LOW_VOLTAGE_DETECTED == 1 } } 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 == 1 } } pub(crate) fn set_restart_to_conf(&mut self, to_conf: bool) { unsafe { if to_conf { RESTART_TO_CONF = 1; } else { RESTART_TO_CONF = 0; } } } pub fn deep_sleep_ms(&mut self, duration_in_ms: u64) -> ! { // Mark the current OTA image as valid if we reached here while in pending verify. if let Ok(cur) = self.ota.current_ota_state() { if cur == OtaImageState::PendingVerify { info!("Marking OTA image as valid"); if let Err(err) = self.ota.set_current_ota_state(Valid) { error!("Could not set image to valid: {:?}", err); } } } else { info!("No OTA image to mark as valid"); } if duration_in_ms == 0 { software_reset(); } else { let timer = TimerWakeupSource::new(core::time::Duration::from_millis(duration_in_ms)); let mut wake_pins: [(&mut dyn RtcPinWithResistors, WakeupLevel); 1] = [(&mut self.wake_gpio1, WakeupLevel::Low)]; let ext1 = esp_hal::rtc_cntl::sleep::Ext1WakeupSource::new(&mut wake_pins); self.rtc.sleep_deep(&[&timer, &ext1]); } } pub(crate) async fn load_config(&mut self) -> FatResult { let cfg = PathBuf::try_from(CONFIG_FILE).unwrap(); let data = self.fs.lock().await.read::<4096>(&cfg)?; let config: PlantControllerConfig = serde_json::from_slice(&data)?; return Ok(config); } pub(crate) async fn save_config(&mut self, config: Vec) -> FatResult<()> { let filesystem = self.fs.lock().await; let cfg = PathBuf::try_from(CONFIG_FILE)?; filesystem.write(&cfg, &*config)?; Ok(()) } pub(crate) async fn list_files(&self) -> FatResult { let path = PathBuf::new(); let fs = self.fs.lock().await; let free_size = fs.available_space()?; let total_size = fs.total_space(); let mut result = FileList { total: total_size, used: total_size - free_size, files: Vec::new(), }; fs.read_dir_and_then(&path, |dir| { for entry in dir { let e = entry?; if e.file_type() == FileType::File { result.files.push(FileInfo { filename: e.path().to_string(), size: e.metadata().len(), }); } } Ok(()) })?; Ok(result) } pub(crate) async 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 = 0; if to_config_mode { RESTART_TO_CONF = 1 } else { RESTART_TO_CONF = 0; } LAST_CORROSION_PROTECTION_CHECK_DAY = -1; }; } else { unsafe { if to_config_mode { RESTART_TO_CONF = 1; } log( LogMessage::RestartToConfig, RESTART_TO_CONF as u32, 0, "", "", ); log( LogMessage::LowVoltage, LOW_VOLTAGE_DETECTED as u32, 0, "", "", ); // is executed before main, no other code will alter these values during printing #[allow(static_mut_refs)] for (i, time) in LAST_WATERING_TIMESTAMP.iter().enumerate() { info!("LAST_WATERING_TIMESTAMP[{i}] = UTC {time}"); } // is executed before main, no other code will alter these values during printing #[allow(static_mut_refs)] for (i, item) in CONSECUTIVE_WATERING_PLANT.iter().enumerate() { info!("CONSECUTIVE_WATERING_PLANT[{i}] = {item}"); } } } } }