use esp_hal::uart::Config as UartConfig; use lib_bms_protocol::BmsReadable; pub(crate) mod battery; // mod can_api; // replaced by external canapi crate pub mod esp; pub(crate) mod rtc; pub(crate) mod savegame_manager; mod shared_flash; mod v4_hal; mod water; use crate::alloc::string::ToString; use crate::hal::rtc::{BackupHeader, DS3231Module, RTCModuleInteraction}; use esp_hal::peripherals::Peripherals; use esp_hal::peripherals::ADC1; use esp_hal::peripherals::GPIO0; use esp_hal::peripherals::GPIO10; use esp_hal::peripherals::GPIO11; use esp_hal::peripherals::GPIO12; use esp_hal::peripherals::GPIO13; use esp_hal::peripherals::GPIO14; use esp_hal::peripherals::GPIO15; use esp_hal::peripherals::GPIO16; use esp_hal::peripherals::GPIO17; use esp_hal::peripherals::GPIO18; use esp_hal::peripherals::GPIO2; use esp_hal::peripherals::GPIO21; use esp_hal::peripherals::GPIO22; use esp_hal::peripherals::GPIO23; use esp_hal::peripherals::GPIO27; use esp_hal::peripherals::GPIO3; use esp_hal::peripherals::GPIO4; use esp_hal::peripherals::GPIO5; use esp_hal::peripherals::GPIO6; use esp_hal::peripherals::GPIO7; use esp_hal::peripherals::GPIO8; use esp_hal::peripherals::TWAI0; use lib_bms_protocol::ProtocolVersion; use crate::{ bail, config::{BatteryBoardVersion, PlantControllerConfig}, hal::{ battery::{BatteryInteraction, NoBatteryMonitor}, esp::Esp, }, log::log, log::LogMessage, }; use alloc::boxed::Box; use alloc::format; use alloc::sync::Arc; use async_trait::async_trait; use bincode::{Decode, Encode}; use canapi::SensorSlot; use chrono::{DateTime, FixedOffset, Utc}; use core::cell::RefCell; use ds323x::ic::DS3231; use ds323x::interface::I2cInterface; use ds323x::{DateTimeAccess, Ds323x}; use eeprom24x::addr_size::TwoBytes; 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_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::blocking_mutex::CriticalSectionMutex; use esp_bootloader_esp_idf::partitions::{ AppPartitionSubType, DataPartitionSubType, FlashRegion, PartitionEntry, PartitionTable, PartitionType, }; use esp_hal::clock::CpuClock; use esp_hal::gpio::{Input, InputConfig, Pull}; use measurements::{Current, Voltage}; use crate::fat_error::{ContextExt, FatError, FatResult}; use crate::hal::battery::WCHI2CSlave; use crate::hal::savegame_manager::SavegameManager; use crate::hal::water::TankSensor; use embassy_sync::mutex::Mutex; use embassy_sync::once_lock::OnceLock; use embedded_storage::nor_flash::RmwNorFlashStorage; use embedded_storage::ReadStorage; use esp_alloc as _; use esp_backtrace as _; use esp_bootloader_esp_idf::ota::{Ota, OtaImageState}; use esp_hal::delay::Delay; use esp_hal::i2c::master::{BusTimeout, Config, FsmTimeout, I2c, SoftwareTimeout}; use esp_hal::interrupt::software::SoftwareInterruptControl; use esp_hal::pcnt::unit::Unit; use esp_hal::pcnt::Pcnt; use esp_hal::rng::Rng; use esp_hal::rtc_cntl::{Rtc, SocResetReason}; use esp_hal::system::reset_reason; use esp_hal::time::Rate; use esp_hal::timer::timg::{MwdtStage, TimerGroup, Wdt}; use esp_hal::uart::Uart; use esp_hal::Blocking; use esp_radio::{init, Controller}; use esp_storage::FlashStorage; use log::{info, warn}; use portable_atomic::AtomicBool; use serde::{Deserialize, Serialize}; use shared_flash::MutexFlashStorage; //Only support for 8 right now! pub const PLANT_COUNT: usize = 8; pub static PROGRESS_ACTIVE: AtomicBool = AtomicBool::new(false); pub static WATCHDOG: OnceLock< embassy_sync::blocking_mutex::Mutex< CriticalSectionRawMutex, RefCell>, >, > = OnceLock::new(); const TANK_MULTI_SAMPLE: usize = 11; pub static I2C_DRIVER: OnceLock< embassy_sync::blocking_mutex::Mutex>>, > = OnceLock::new(); #[derive(Debug, PartialEq, Clone, Copy, Encode, Decode)] pub enum Sensor { A, B, } impl From for SensorSlot { fn from(val: Sensor) -> Self { match val { Sensor::A => SensorSlot::A, Sensor::B => SensorSlot::B, } } } pub struct PlantHal {} #[allow(clippy::upper_case_acronyms)] pub struct HAL<'a> { pub board_hal: Box + Send>, } #[async_trait(?Send)] pub trait BoardInteraction<'a> { fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError>; fn get_esp(&mut self) -> &mut Esp<'a>; fn get_config(&mut self) -> &PlantControllerConfig; fn get_battery_monitor(&mut self) -> &mut Box; fn get_rtc_module(&mut self) -> &mut Box; async fn get_time(&mut self) -> DateTime; async fn set_time(&mut self, time: &DateTime) -> FatResult<()>; async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError>; async fn deep_sleep(&mut self, duration_in_ms: u64) -> !; fn is_day(&self) -> bool; //should be multsampled async fn light(&mut self, enable: bool) -> FatResult<()>; async fn pump(&mut self, plant: usize, enable: bool) -> FatResult<()>; async fn pump_current(&mut self, plant: usize) -> FatResult; async fn fault(&mut self, plant: usize, enable: bool) -> FatResult<()>; async fn measure_moisture_hz(&mut self) -> Result; async fn general_fault(&mut self, enable: bool); async fn test(&mut self) -> FatResult<()>; fn set_config(&mut self, config: PlantControllerConfig); async fn get_mptt_voltage(&mut self) -> FatResult; async fn get_mptt_current(&mut self) -> FatResult; async fn can_power(&mut self, state: bool) -> FatResult<()>; async fn backup_config(&mut self, config: &PlantControllerConfig) -> FatResult<()>; async fn read_backup(&mut self) -> FatResult; async fn backup_info(&mut self) -> FatResult; // Return JSON string with autodetected sensors per plant. Default: not supported. async fn detect_sensors(&mut self, _request: Detection) -> FatResult { bail!("Autodetection is only available on v4 HAL with CAN bus"); } async fn progress(&mut self, counter: u32) { // Indicate progress is active to suppress default wait_infinity blinking PROGRESS_ACTIVE.store(true, core::sync::atomic::Ordering::Relaxed); // Feed watchdog during long-running webserver operations PlantHal::feed_watchdog(); let current = counter % PLANT_COUNT as u32; for led in 0..PLANT_COUNT { if let Err(err) = self.fault(led, current == led as u32).await { warn!("Fault on plant {led}: {err:?}"); } } let even = counter % 2 == 0; let _ = self.general_fault(even).await; } async fn clear_progress(&mut self) { for led in 0..PLANT_COUNT { if let Err(err) = self.fault(led, false).await { warn!("Fault on plant {led}: {err:?}"); } } let _ = self.general_fault(false).await; // Reset progress active flag so wait_infinity can resume blinking PROGRESS_ACTIVE.store(false, core::sync::atomic::Ordering::Relaxed); } } #[allow(dead_code)] pub struct FreePeripherals<'a> { pub gpio0: GPIO0<'a>, pub gpio2: GPIO2<'a>, pub gpio3: GPIO3<'a>, pub gpio4: GPIO4<'a>, pub gpio5: GPIO5<'a>, pub gpio6: GPIO6<'a>, pub gpio7: GPIO7<'a>, pub gpio8: GPIO8<'a>, // //config button here pub gpio10: GPIO10<'a>, pub gpio11: GPIO11<'a>, pub gpio12: GPIO12<'a>, pub gpio13: GPIO13<'a>, pub gpio14: GPIO14<'a>, pub gpio15: GPIO15<'a>, pub gpio16: GPIO16<'a>, pub gpio17: GPIO17<'a>, pub gpio18: GPIO18<'a>, // //i2c here pub gpio21: GPIO21<'a>, pub gpio22: GPIO22<'a>, pub gpio23: GPIO23<'a>, pub gpio27: GPIO27<'a>, pub twai: TWAI0<'a>, pub pcnt0: Unit<'a, 0>, pub pcnt1: Unit<'a, 1>, pub adc1: ADC1<'a>, } 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 }}; } impl PlantHal { pub async fn create() -> Result>, FatError> { let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); let peripherals: Peripherals = esp_hal::init(config); esp_alloc::heap_allocator!(size: 64 * 1024); esp_alloc::heap_allocator!(#[link_section = ".dram2_uninit"] size: 64000); let rtc_peripheral: Rtc = Rtc::new(peripherals.LPWR); let timg0 = TimerGroup::new(peripherals.TIMG0); let sw_int = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); esp_rtos::start(timg0.timer0, sw_int.software_interrupt0); // Initialize and enable the watchdog with 30 second timeout let mut wdt = timg0.wdt; wdt.set_timeout(MwdtStage::Stage0, esp_hal::time::Duration::from_secs(30)); wdt.enable(); WATCHDOG .init(embassy_sync::blocking_mutex::Mutex::new(RefCell::new(wdt))) .map_err(|_| FatError::String { error: "Watchdog already initialized".to_string(), })?; let boot_button = Input::new( peripherals.GPIO9, InputConfig::default().with_pull(Pull::None), ); // Reserve GPIO1 for deep sleep wake (configured just before entering sleep) let wake_gpio1 = peripherals.GPIO1; let rng = Rng::new(); let esp_wifi_ctrl = &*mk_static!( Controller<'static>, init().map_err(|e| FatError::String { error: format!("Could not init wifi controller: {:?}", e) })? ); let (controller, interfaces) = esp_radio::wifi::new(esp_wifi_ctrl, peripherals.WIFI, Default::default()).map_err( |e| FatError::String { error: format!("Could not init wifi: {:?}", e), }, )?; let pcnt_module = Pcnt::new(peripherals.PCNT); let free_pins = FreePeripherals { gpio0: peripherals.GPIO0, gpio2: peripherals.GPIO2, gpio3: peripherals.GPIO3, gpio4: peripherals.GPIO4, gpio5: peripherals.GPIO5, gpio6: peripherals.GPIO6, gpio7: peripherals.GPIO7, gpio8: peripherals.GPIO8, gpio10: peripherals.GPIO10, gpio11: peripherals.GPIO11, gpio12: peripherals.GPIO12, gpio13: peripherals.GPIO13, gpio14: peripherals.GPIO14, gpio15: peripherals.GPIO15, gpio16: peripherals.GPIO16, gpio17: peripherals.GPIO17, gpio18: peripherals.GPIO18, gpio21: peripherals.GPIO21, gpio22: peripherals.GPIO22, gpio23: peripherals.GPIO23, gpio27: peripherals.GPIO27, twai: peripherals.TWAI0, pcnt0: pcnt_module.unit0, pcnt1: pcnt_module.unit1, adc1: peripherals.ADC1, }; let tablebuffer = mk_static!( [u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN], [0u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN] ); let bullshit = MutexFlashStorage { inner: Arc::new(CriticalSectionMutex::new(RefCell::new(FlashStorage::new( peripherals.FLASH, )))), }; let flash_storage = mk_static!(MutexFlashStorage, bullshit.clone()); let flash_storage_2 = mk_static!(MutexFlashStorage, bullshit.clone()); let flash_storage_3 = mk_static!(MutexFlashStorage, bullshit.clone()); let pt = esp_bootloader_esp_idf::partitions::read_partition_table(flash_storage, tablebuffer)?; let ota_data = mk_static!( PartitionEntry, pt.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data( DataPartitionSubType::Ota, ))? .context("No OTA data partition found")? ); let ota_data = mk_static!( FlashRegion>, ota_data.as_embedded_storage(mk_static!( RmwNorFlashStorage<&mut MutexFlashStorage>, RmwNorFlashStorage::new(flash_storage_2, mk_static!([u8; 4096], [0_u8; 4096])) )) ); let state_0 = ota_state(AppPartitionSubType::Ota0, ota_data); let state_1 = ota_state(AppPartitionSubType::Ota1, ota_data); let mut ota = Ota::new(ota_data, 2)?; let running = get_current_slot(&pt, &mut ota)?; let target = next_partition(running)?; info!("Currently running OTA slot: {running:?}"); info!("Updates will be stored in OTA slot: {target:?}"); info!("Slot0 state: {state_0:?}"); info!("Slot1 state: {state_1:?}"); //get current_state and next_state here! let ota_target = match target { AppPartitionSubType::Ota0 => pt .find_partition(PartitionType::App(AppPartitionSubType::Ota0))? .context("Partition table invalid no ota0")?, AppPartitionSubType::Ota1 => pt .find_partition(PartitionType::App(AppPartitionSubType::Ota1))? .context("Partition table invalid no ota1")?, _ => { bail!("Invalid target partition"); } }; let ota_target = mk_static!(PartitionEntry, ota_target); let ota_target = mk_static!( FlashRegion, ota_target.as_embedded_storage(flash_storage) ); let data_partition = pt .find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data( DataPartitionSubType::LittleFs, ))? .context("Storage data partition not found")?; let data_partition = mk_static!(PartitionEntry, data_partition); let data = mk_static!( FlashRegion<'static, MutexFlashStorage>, data_partition.as_embedded_storage(flash_storage_3) ); let savegame = SavegameManager::new(data); info!( "Savegame storage initialized ({} slots × {} KB)", savegame_manager::SAVEGAME_SLOT_COUNT, savegame_manager::SAVEGAME_SLOT_SIZE / 1024 ); let uart0 = Uart::new(peripherals.UART0, UartConfig::default()).map_err(|_| FatError::String { error: "Uart creation failed".to_string(), })?; let ap = interfaces.ap; let sta = interfaces.sta; let mut esp = Esp { savegame, rng, controller: Arc::new(Mutex::new(controller)), interface_sta: Some(sta), interface_ap: Some(ap), boot_button, wake_gpio1, ota, ota_target, current: running, slot0_state: state_0, slot1_state: state_1, uart0, rtc: rtc_peripheral, }; //init,reset rtc memory depending on cause let mut init_rtc_store: bool = false; let mut to_config_mode: bool = false; let reasons = match reset_reason() { None => "unknown", Some(reason) => match reason { SocResetReason::ChipPowerOn => "power on", SocResetReason::CoreSDIO => "sdio reset", SocResetReason::CoreMwdt0 => "Watchdog Main", SocResetReason::CoreMwdt1 => "Watchdog 1", SocResetReason::CoreRtcWdt => "Watchdog RTC", SocResetReason::Cpu0Mwdt0 => "Watchdog MCpu0", SocResetReason::Cpu0Sw => "software reset cpu0", SocResetReason::SysRtcWdt => "Watchdog Sys rtc", SocResetReason::Cpu0Mwdt1 => "cpu0 mwdt1", SocResetReason::SysSuperWdt => "Watchdog Super", SocResetReason::Cpu0RtcWdt => { init_rtc_store = true; "Watchdog RTC cpu0" } SocResetReason::CoreSw => "software reset", SocResetReason::CoreDeepSleep => "deep sleep", SocResetReason::SysBrownOut => "sys brown out", SocResetReason::CoreEfuseCrc => "core efuse crc", SocResetReason::CoreUsbUart => { //TODO still required? or via button ignore? to_config_mode = true; to_config_mode = true; "core usb uart" } SocResetReason::CoreUsbJtag => "core usb jtag", SocResetReason::Cpu0JtagCpu => "cpu0 jtag cpu", }, }; log( LogMessage::ResetReason, init_rtc_store as u32, to_config_mode as u32, "", &format!("{reasons:?}"), ) .await; esp.init_rtc_deepsleep_memory(init_rtc_store, to_config_mode) .await; let config = esp.load_config().await; info!("Init rtc driver"); let sda = peripherals.GPIO20; let scl = peripherals.GPIO19; // Configure I2C with 1-second timeout // At 100 Hz I2C clock, one bus cycle = 10ms // For 1 second timeout: 100 bus cycles let i2c = I2c::new( peripherals.I2C0, Config::default() //.with_frequency(Rate::from_hz(100)) //1s at 100khz .with_timeout(BusTimeout::BusCycles(100_000)) .with_scl_main_st_timeout(FsmTimeout::new(21)?), )? .with_scl(scl) .with_sda(sda); let i2c_bus: embassy_sync::blocking_mutex::Mutex< CriticalSectionRawMutex, RefCell>, > = CriticalSectionMutex::new(RefCell::new(i2c)); I2C_DRIVER.init(i2c_bus).map_err(|_| FatError::String { error: "Could not init i2c driver".to_string(), })?; let i2c_bus = I2C_DRIVER.get().await; let rtc_device = I2cDevice::new(i2c_bus); let mut bms_device = I2cDevice::new(i2c_bus); let eeprom_device = I2cDevice::new(i2c_bus); let mut rtc: Ds323x< I2cInterface>>, DS3231, > = Ds323x::new_ds3231(rtc_device); info!("Init rtc eeprom driver"); let eeprom = Eeprom24x::new_24x32(eeprom_device, SlaveAddr::Alternative(true, true, true)); let rtc_time = rtc.datetime(); match rtc_time { Ok(tt) => { info!("Rtc Module reports time at UTC {tt}"); } Err(err) => { info!("Rtc Module could not be read {err:?}"); } } let storage: Storage< I2cDevice<'static, CriticalSectionRawMutex, I2c>, B32, TwoBytes, No, Delay, > = Storage::new(eeprom, Delay::new()); let rtc_module: Box = Box::new(DS3231Module { rtc, storage }) as Box; let hal = match config { Ok(config) => { let battery_interaction: Box = match config.hardware.battery { BatteryBoardVersion::Disabled => Box::new(NoBatteryMonitor {}), BatteryBoardVersion::WchI2cSlave => { let version = ProtocolVersion::read_from_i2c(&mut bms_device); let version_val = match version { Ok(v) => unsafe { core::mem::transmute::(v) }, Err(_) => 0, }; if version_val == 1 { Box::new(WCHI2CSlave { i2c: bms_device }) } else { //todo should be an error variant instead? Box::new(NoBatteryMonitor {}) } } }; let board_hal: Box = //match config.hardware.board { //BoardVersion::Initial => { // initial_hal::create_initial_board(free_pins, config, esp)? //} //BoardVersion::V4 => { v4_hal::create_v4(free_pins, esp, config, battery_interaction, rtc_module) .await?; //} //}; HAL { board_hal } } Err(err) => { log( LogMessage::ConfigModeMissingConfig, 0, 0, "", &err.to_string(), ) .await; HAL { board_hal: v4_hal::create_v4( free_pins, esp, PlantControllerConfig::default(), Box::new(NoBatteryMonitor {}), rtc_module, ) .await?, } } }; Ok(Mutex::new(hal)) } /// Feed the watchdog timer to prevent system reset pub fn feed_watchdog() { if let Some(wdt_mutex) = WATCHDOG.try_get() { wdt_mutex.lock(|cell| { cell.borrow_mut().feed(); }); } } } fn ota_state( slot: AppPartitionSubType, ota_data: &mut FlashRegion>, ) -> OtaImageState { // Read and log OTA states for both slots before constructing Ota // Each OTA select entry is 32 bytes: [seq:4][label:20][state:4][crc:4] // Offsets within the OTA data partition: slot0 @ 0x0000, slot1 @ 0x1000 let mut slot_buf = [0u8; 32]; if slot == AppPartitionSubType::Ota0 { let _ = ReadStorage::read(ota_data, 0x0000, &mut slot_buf); } else { let _ = ReadStorage::read(ota_data, 0x1000, &mut slot_buf); } let raw_state = u32::from_le_bytes(slot_buf[24..28].try_into().unwrap_or([0xff; 4])); OtaImageState::try_from(raw_state).unwrap_or(OtaImageState::Undefined) } fn get_current_slot( pt: &PartitionTable, ota: &mut Ota>, ) -> Result { let booted = pt.booted_partition()?.ok_or(FatError::OTAError)?; let booted_type = booted.partition_type(); let booted_ota_type = match booted_type { PartitionType::App(subtype) => subtype, _ => { bail!("Booted partition is not an app partition"); } }; let expected_partition = ota.current_app_partition()?; if expected_partition == booted_ota_type { info!("Booted partition matches expected partition"); } else { info!("Booted partition does not match expected partition, fixing ota entry"); ota.set_current_app_partition(booted_ota_type)?; } let fixed = ota.current_app_partition()?; let state = ota.current_ota_state(); info!("Expected partition: {expected_partition:?}, current partition: {booted_ota_type:?}, state: {state:?}"); if fixed != booted_ota_type { bail!( "Could not fix ota entry, booted partition is still not correct: {:?} != {:?}", booted_ota_type, fixed ); } Ok(booted_ota_type) } pub fn next_partition(current: AppPartitionSubType) -> FatResult { let next = match current { AppPartitionSubType::Ota0 => AppPartitionSubType::Ota1, AppPartitionSubType::Ota1 => AppPartitionSubType::Ota0, _ => { bail!("Current slot is not ota0 or ota1"); } }; Ok(next) } #[derive(Debug, Clone, Copy, PartialEq, Default, Serialize)] pub struct Moistures { pub sensor_a_hz: [Option; PLANT_COUNT], pub sensor_b_hz: [Option; PLANT_COUNT], } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct Detection { plant: [DetectionSensorResult; PLANT_COUNT], } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct DetectionSensorResult { sensor_a: bool, sensor_b: bool, }