#![no_std] #![no_main] #![feature(never_type)] #![feature(string_from_utf8_lossy_owned)] #![feature(impl_trait_in_assoc_type)] #![deny( clippy::mem_forget, reason = "mem::forget is generally not safe to do with esp_hal types, especially those \ holding buffers for the duration of a data transfer." )] esp_bootloader_esp_idf::esp_app_desc!(); use esp_backtrace as _; use crate::config::PlantConfig; 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::fat_error::FatResult; use crate::{ config::BoardVersion::INITIAL, hal::{PlantHal, HAL, PLANT_COUNT}, }; use ::log::{info, warn}; use alloc::borrow::ToOwned; use alloc::string::{String, ToString}; use alloc::sync::Arc; use alloc::{format, vec}; use chrono::{DateTime, Datelike, Timelike, Utc}; use chrono_tz::Tz::{self, UTC}; use core::sync::atomic::{AtomicBool, Ordering}; use embassy_executor::Spawner; use embassy_net::Stack; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::mutex::{Mutex, MutexGuard}; use embassy_sync::once_lock::OnceLock; use embassy_time::Timer; use esp_hal::rom::ets_delay_us; use esp_hal::system::software_reset; use esp_println::{logger, println}; use hal::battery::BatteryState; use log::LogMessage; use plant_state::PlantState; use serde::{Deserialize, Serialize}; #[no_mangle] extern "C" fn custom_halt() -> ! { println!("Fatal error occurred. Restarting in 10 seconds..."); for _delay in 0..30 { ets_delay_us(1_000_000); } println!("resetting"); //give serial transmit time to finish ets_delay_us(500_000); software_reset() } //use tank::*; mod fat_error; mod config; mod hal; mod log; mod plant_state; mod sipo; mod tank; mod webserver; extern crate alloc; //mod webserver; pub static BOARD_ACCESS: OnceLock>> = OnceLock::new(); pub static STAY_ALIVE: AtomicBool = AtomicBool::new(false); #[derive(Serialize, Deserialize, Debug, PartialEq)] enum WaitType { MissingConfig, ConfigButton, MqttConfig, } #[derive(Serialize, Deserialize, Debug, PartialEq)] struct Solar { current_ma: u32, voltage_ma: u32, } impl WaitType { fn blink_pattern(&self) -> u64 { match self { WaitType::MissingConfig => 500_u64, WaitType::ConfigButton => 100_u64, WaitType::MqttConfig => 200_u64, } } } #[derive(Serialize, Deserialize, Debug, PartialEq, Default)] /// Light State tracking data for mqtt struct LightState { /// is enabled in config enabled: bool, /// led is on active: bool, /// led should not be on at this time of day out_of_work_hour: bool, /// the battery is low so do not use led battery_low: bool, /// the sun is up is_day: bool, } #[derive(Serialize, Deserialize, Debug, PartialEq, Default)] ///mqtt struct to track pump activities struct PumpInfo { enabled: bool, pump_ineffective: bool, median_current_ma: u16, max_current_ma: u16, min_current_ma: u16, } #[derive(Serialize)] pub struct PumpResult { median_current_ma: u16, max_current_ma: u16, min_current_ma: u16, error: bool, flow_value_ml: f32, flow_value_count: i16, pump_time_s: u16, } #[derive(Serialize, Debug, PartialEq)] enum SntpMode { OFFLINE, SYNC { current: DateTime }, } #[derive(Serialize, Debug, PartialEq)] enum NetworkMode { WIFI { sntp: SntpMode, mqtt: bool, ip_address: String, }, OFFLINE, } async fn safe_main(spawner: Spawner) -> FatResult<()> { info!("Startup Rust"); let mut to_config = false; let mut board = BOARD_ACCESS.get().await.lock().await; let version = get_version(&mut board).await; info!( "Version using git has {} build on {}", version.git_hash, version.build_time ); board.board_hal.general_fault(false).await; let cur = match board.board_hal.get_rtc_module().get_rtc_time().await { Ok(value) => { TIME_ACCESS .get() .await .set_current_time_us(value.timestamp_micros() as u64); value } Err(err) => { info!("rtc module error: {:?}", err); board.board_hal.general_fault(true).await; esp_time().await } }; //check if we know the time current > 2020 (plausibility checks, this code is newer than 2020) if cur.year() < 2020 { to_config = true; LOG_ACCESS .lock() .await .log(LogMessage::YearInplausibleForceConfig, 0, 0, "", "") .await; } info!("cur is {}", cur); update_charge_indicator(&mut board).await; if board.board_hal.get_esp().get_restart_to_conf() { LOG_ACCESS .lock() .await .log(LogMessage::ConfigModeSoftwareOverride, 0, 0, "", "") .await; for _i in 0..2 { board.board_hal.general_fault(true).await; Timer::after_millis(100).await; board.board_hal.general_fault(false).await; Timer::after_millis(100).await; } to_config = true; board.board_hal.general_fault(true).await; board.board_hal.get_esp().set_restart_to_conf(false); } else if board.board_hal.get_esp().mode_override_pressed() { board.board_hal.general_fault(true).await; LOG_ACCESS .lock() .await .log(LogMessage::ConfigModeButtonOverride, 0, 0, "", "") .await; for _i in 0..5 { board.board_hal.general_fault(true).await; Timer::after_millis(100).await; board.board_hal.general_fault(false).await; Timer::after_millis(100).await; } if board.board_hal.get_esp().mode_override_pressed() { board.board_hal.general_fault(true).await; to_config = true; } else { board.board_hal.general_fault(false).await; } } else { info!("no mode override"); } 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().await?; let reboot_now = Arc::new(AtomicBool::new(false)); println!("starting webserver"); spawner.spawn(httpd(reboot_now.clone(), stack))?; wait_infinity(board, WaitType::MissingConfig, reboot_now.clone()).await; } info!("attempting to connect wifi "); let mut stack = Option::None; let network_mode = if board.board_hal.get_config().network.ssid.is_some() { 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 handles for sure; Timer::after_millis(100).await; NetworkMode::OFFLINE }; if matches!(network_mode, NetworkMode::OFFLINE) && to_config { info!("Could not connect to station and config mode forced, switching to ap mode!"); let res = { let esp = board.board_hal.get_esp(); esp.wifi_ap().await }; match res { Ok(ap_stack) => { stack = Some(ap_stack); info!("Started ap, continuing") } Err(err) => info!("Could not start config override ap mode due to {}", err), } } let tz = & board.board_hal.get_config().timezone; let timezone = match tz { Some(tz_str) => tz_str.parse::().unwrap_or_else(|_| { info!("Invalid timezone '{}', falling back to UTC", tz_str); UTC }), None => UTC, // Fallback to UTC if no timezone is set }; let _timezone = Tz::UTC; let timezone_time = cur.with_timezone(&timezone); info!( "Running logic at utc {} and {} {}", cur, timezone.name(), timezone_time ); if let NetworkMode::WIFI { ref ip_address, .. } = network_mode { publish_firmware_info(&mut board, version, ip_address, &timezone_time.to_rfc3339()).await; publish_battery_state(&mut board).await; let _ = publish_mppt_state(&mut board).await; } LOG_ACCESS .lock() .await .log( LogMessage::StartupInfo, matches!(network_mode, NetworkMode::WIFI { .. }) as u32, matches!( network_mode, NetworkMode::WIFI { sntp: SntpMode::SYNC { .. }, .. } ) as u32, matches!(network_mode, NetworkMode::WIFI { mqtt: true, .. }) .to_string() .as_str(), "", ) .await; if to_config { //check if client or ap mode and init Wi-Fi 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.unwrap()))?; wait_infinity(board, WaitType::ConfigButton, reboot_now.clone()).await; } else { LOG_ACCESS .lock() .await .log(LogMessage::NormalRun, 0, 0, "", "") .await; } let _dry_run = false; let tank_state = determine_tank_state(&mut 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; } } let mut _water_frozen = false; let water_temp: FatResult = match board.board_hal.get_tank_sensor() { Ok(sensor) => sensor.water_temperature_c().await, Err(e) => Err(e), }; if let Ok(res) = water_temp { if res < WATER_FROZEN_THRESH { _water_frozen = true; } } //publish_tank_state(&tank_state, &water_temp).await; board = BOARD_ACCESS.get().await.lock().await; 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, PlantState::read_hardware_state(3, &mut board).await, PlantState::read_hardware_state(4, &mut board).await, PlantState::read_hardware_state(5, &mut board).await, PlantState::read_hardware_state(6, &mut board).await, PlantState::read_hardware_state(7, &mut board).await, ]; //publish_plant_states(&timezone_time.clone(), &plantstate).await; // let pump_required = plantstate // .iter() // .zip(&board.board_hal.get_config().plants) // .any(|(it, conf)| it.needs_to_be_watered(conf, &timezone_time)) // && !water_frozen; // if pump_required { // log(LogMessage::EnableMain, dry_run as u32, 0, "", ""); // for (plant_id, (state, plant_config)) in plantstate // .iter() // .zip(&board.board_hal.get_config().plants.clone()) // .enumerate() // { // if state.needs_to_be_watered(plant_config, &timezone_time) { // let pump_count = board.board_hal.get_esp().consecutive_pump_count(plant_id) + 1; // board // .board_hal // .get_esp() // .store_consecutive_pump_count(plant_id, pump_count); // // let pump_ineffective = pump_count > plant_config.max_consecutive_pump_count as u32; // if pump_ineffective { // log( // LogMessage::ConsecutivePumpCountLimit, // pump_count, // plant_config.max_consecutive_pump_count as u32, // &(plant_id + 1).to_string(), // "", // ); // board.board_hal.fault(plant_id, true).await?; // } // log( // LogMessage::PumpPlant, // (plant_id + 1) as u32, // plant_config.pump_time_s as u32, // &dry_run.to_string(), // "", // ); // board // .board_hal // .get_esp() // .store_last_pump_time(plant_id, cur); // board.board_hal.get_esp().last_pump_time(plant_id); // //state.active = true; // // pump_info(plant_id, true, pump_ineffective, 0, 0, 0, false).await; // // let result = do_secure_pump(plant_id, plant_config, dry_run).await?; // board.board_hal.pump(plant_id, false).await?; // pump_info( // plant_id, // false, // pump_ineffective, // result.median_current_ma, // result.max_current_ma, // result.min_current_ma, // result.error, // ) // .await; // } else if !state.pump_in_timeout(plant_config, &timezone_time) { // // plant does not need to be watered and is not in timeout // // -> reset consecutive pump count // board // .board_hal // .get_esp() // .store_consecutive_pump_count(plant_id, 0); // } // } // } let is_day = board.board_hal.is_day(); let state_of_charge = board .board_hal .get_battery_monitor() .state_charge_percent() .await .unwrap_or(0.); // try to load full battery state if failed the battery state is unknown let battery_state = board .board_hal .get_battery_monitor() .get_battery_state() .await .unwrap_or(BatteryState::Unknown); let mut light_state = LightState { enabled: board.board_hal.get_config().night_lamp.enabled, ..Default::default() }; if light_state.enabled { light_state.is_day = is_day; light_state.out_of_work_hour = !in_time_range( &timezone_time, board .board_hal .get_config() .night_lamp .night_lamp_hour_start, board.board_hal.get_config().night_lamp.night_lamp_hour_end, ); if state_of_charge < board .board_hal .get_config() .night_lamp .low_soc_cutoff .into() { board.board_hal.get_esp().set_low_voltage_in_cycle(); } else if state_of_charge > board .board_hal .get_config() .night_lamp .low_soc_restore .into() { board.board_hal.get_esp().clear_low_voltage_in_cycle(); } light_state.battery_low = board.board_hal.get_esp().low_voltage_in_cycle(); if !light_state.out_of_work_hour { if board .board_hal .get_config() .night_lamp .night_lamp_only_when_dark { if !light_state.is_day { if light_state.battery_low { board.board_hal.light(false).await?; } else { light_state.active = true; board.board_hal.light(true).await?; } } } else if light_state.battery_low { board.board_hal.light(false).await?; } else { light_state.active = true; board.board_hal.light(true).await?; } } else { light_state.active = false; board.board_hal.light(false).await?; } info!("Lightstate is {:?}", light_state); } match serde_json::to_string(&light_state) { Ok(state) => { let _ = board .board_hal .get_esp() .mqtt_publish("/light", state.as_bytes()) .await; } Err(err) => { info!("Error publishing lightstate {}", err); } }; let deep_sleep_duration_minutes: u32 = // if battery soc is unknown assume battery has enough change if state_of_charge < 10.0 && !matches!(battery_state, BatteryState::Unknown) { let _ = board .board_hal .get_esp() .mqtt_publish("/deepsleep", "low Volt 12h".as_bytes()).await; 12 * 60 } else if is_day { let _ = board .board_hal .get_esp() .mqtt_publish("/deepsleep", "normal 20m".as_bytes()).await; 20 } else { let _ = board .board_hal .get_esp() .mqtt_publish("/deepsleep", "night 1h".as_bytes()).await; 60 }; let _ = board .board_hal .get_esp() .mqtt_publish("/state", "sleep".as_bytes()) .await; //determine next event //is light out of work trigger soon? //is battery low ?? //is deep sleep //TODO //mark_app_valid(); let stay_alive_mqtt = STAY_ALIVE.load(Ordering::Relaxed); let stay_alive = stay_alive_mqtt; 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()); wait_infinity(board, WaitType::MqttConfig, reboot_now.clone()).await; } else { board.board_hal.get_esp().set_restart_to_conf(false); board .board_hal .deep_sleep(1000 * 1000 * 60 * deep_sleep_duration_minutes as u64) .await; } } pub async fn do_secure_pump( board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'_>>, plant_id: usize, plant_config: &PlantConfig, dry_run: bool, ) -> FatResult { let mut current_collector = vec![0_u16; plant_config.pump_time_s.into()]; let mut flow_collector = vec![0_i16; plant_config.pump_time_s.into()]; let mut error = false; let mut first_error = true; let mut pump_time_s = 0; if !dry_run { board .board_hal .get_tank_sensor()? .reset_flow_meter(); board .board_hal .get_tank_sensor()? .start_flow_meter(); board.board_hal.pump(plant_id, true).await?; 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; info!( "Flow value is {} ml, limit is {} ml raw sensor {}", flow_value_ml, plant_config.pump_limit_ml, flow_value ); if flow_value_ml > plant_config.pump_limit_ml as f32 { info!("Flow value is reached, stopping"); break; } let current = board.board_hal.pump_current(plant_id).await; match current { Ok(current) => { let current_ma = current.as_milliamperes() as u16; current_collector[step] = current_ma; let high_current = current_ma > plant_config.max_pump_current_ma; if high_current { if first_error { LOG_ACCESS .lock() .await .log( LogMessage::PumpOverCurrent, plant_id as u32 + 1, current_ma as u32, plant_config.max_pump_current_ma.to_string().as_str(), step.to_string().as_str(), ) .await; board.board_hal.general_fault(true).await; board.board_hal.fault(plant_id, true).await?; if !plant_config.ignore_current_error { error = true; break; } first_error = false; } } let low_current = current_ma < plant_config.min_pump_current_ma; if low_current { if first_error { LOG_ACCESS .lock() .await .log( LogMessage::PumpOpenLoopCurrent, plant_id as u32 + 1, current_ma as u32, plant_config.min_pump_current_ma.to_string().as_str(), step.to_string().as_str(), ) .await; board.board_hal.general_fault(true).await; board.board_hal.fault(plant_id, true).await?; if !plant_config.ignore_current_error { error = true; break; } first_error = false; } } } Err(err) => { if !plant_config.ignore_current_error { info!("Error getting pump current: {}", err); LOG_ACCESS .lock() .await .log( LogMessage::PumpMissingSensorCurrent, plant_id as u32, 0, "", "", ) .await; error = true; break; } else { //eg v3 without a sensor ends here, do not spam } } } Timer::after_millis(1000).await; pump_time_s += 1; } } board.board_hal.get_tank_sensor().unwrap().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", final_flow_value, flow_value_ml ); current_collector.sort(); Ok(PumpResult { median_current_ma: current_collector[current_collector.len() / 2], max_current_ma: current_collector[current_collector.len() - 1], min_current_ma: current_collector[0], flow_value_ml, flow_value_count: final_flow_value, pump_time_s, error, }) } async fn update_charge_indicator(board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'_>>) { //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; // } // } if let Ok(current) = board.board_hal.get_mptt_current().await { let _ = board .board_hal .set_charge_indicator(current.as_milliamperes() > 20_f64); } //fallback to battery controller and ask it instead else if let Ok(charging) = board .board_hal .get_battery_monitor() .average_current_milli_ampere() .await { let _ = board.board_hal.set_charge_indicator(charging > 20); } else { //who knows let _ = board.board_hal.set_charge_indicator(false); } } // // async fn publish_tank_state(tank_state: &TankState, water_temp: &anyhow::Result) { // 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, 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_firmware_info( board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, version: VersionInfo, ip_address: &String, timezone_time: &String, ) { let esp = board.board_hal.get_esp(); let _ = esp .mqtt_publish("/firmware/address", ip_address.as_bytes()) .await; let _ = esp .mqtt_publish("/firmware/githash", version.git_hash.as_bytes()) .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 state = esp.get_ota_state(); let _ = esp .mqtt_publish("/firmware/ota_state", state.as_bytes()) .await; let slot = esp.get_ota_slot(); let _ = esp .mqtt_publish("/firmware/ota_slot", format!("slot{slot}").as_bytes()) .await; let _ = esp.mqtt_publish("/state", "online".as_bytes()).await; } async fn try_connect_wifi_sntp_mqtt(board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>, stack_store:Option>) -> NetworkMode { let nw_conf = &board.board_hal.get_config().network.clone(); match board.board_hal.get_esp().wifi(nw_conf).await { Ok(ip_info) => { let sntp_mode: SntpMode = match board.board_hal.get_esp().sntp(1000 * 10).await { Ok(new_time) => { info!("Using time from sntp"); let _ = board.board_hal.get_rtc_module().set_rtc_time(&new_time); SntpMode::SYNC { current: new_time } } Err(err) => { warn!("sntp error: {}", err); board.board_hal.general_fault(true).await; SntpMode::OFFLINE } }; 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 { Ok(_) => { info!("Mqtt connection ready"); true } Err(err) => { warn!("Could not connect mqtt due to {}", err); false } } } else { false }; NetworkMode::WIFI { sntp: sntp_mode, mqtt: mqtt_connected, ip_address: ip_info.ip.to_string(), } } Err(err) => { info!("Offline mode due to {}", err); board.board_hal.general_fault(true).await; NetworkMode::OFFLINE } } } async fn pump_info( plant_id: usize, pump_active: bool, pump_ineffective: bool, median_current_ma: u16, max_current_ma: u16, min_current_ma: u16, _error: bool, ) { 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, }; let pump_topic = format!("/pump{}", plant_id + 1); match serde_json::to_string(&pump_info) { Ok(state) => { let _ = 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; } Err(err) => { warn!("Error publishing pump state {}", err); } }; } async fn publish_mppt_state( board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, ) -> FatResult<()> { let current = board.board_hal.get_mptt_current().await?; let voltage = board.board_hal.get_mptt_voltage().await?; let solar_state = Solar { 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()) { let _ = board .board_hal .get_esp() .mqtt_publish("/mppt", &serialized_solar_state_bytes); } Ok(()) } async fn publish_battery_state( board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, ) -> () { let state = board .board_hal .get_battery_monitor() .get_battery_state() .await; let value = match state { Ok(state) => { let json = serde_json::to_string(&state).unwrap().to_owned(); json.as_bytes().to_owned() } Err(_) => "error".as_bytes().to_owned(), }; { let _ = board .board_hal .get_esp() .mqtt_publish("/battery", &*value) .await; } } async fn wait_infinity( board: MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, wait_type: WaitType, reboot_now: Arc, ) -> ! { //since we force to have the lock when entering, we can release it to ensure the caller does not forget to dispose of it drop(board); let delay = wait_type.blink_pattern(); let mut led_count = 8; let mut pattern_step = 0; loop { { let mut board = BOARD_ACCESS.get().await.lock().await; update_charge_indicator(&mut board).await; match wait_type { WaitType::MissingConfig => { // Keep existing behavior: circular filling pattern led_count %= 8; led_count += 1; for i in 0..8 { let _ = board.board_hal.fault(i, i < led_count); } } WaitType::ConfigButton => { // Alternating pattern: 1010 1010 -> 0101 0101 pattern_step = (pattern_step + 1) % 2; for i in 0..8 { let _ = board.board_hal.fault(i, (i + pattern_step) % 2 == 0); } } WaitType::MqttConfig => { // Moving dot pattern pattern_step = (pattern_step + 1) % 8; for i in 0..8 { let _ = board.board_hal.fault(i, i == pattern_step); } } } board.board_hal.general_fault(true).await; } Timer::after_millis(delay).await; { let mut board = BOARD_ACCESS.get().await.lock().await; board.board_hal.general_fault(false).await; // Clear all LEDs for i in 0..8 { let _ = board.board_hal.fault(i, false); } } Timer::after_millis(delay).await; if wait_type == WaitType::MqttConfig && !STAY_ALIVE.load(Ordering::Relaxed) { reboot_now.store(true, Ordering::Relaxed); } if reboot_now.load(Ordering::Relaxed) { //ensure clean http answer Timer::after_millis(500).await; BOARD_ACCESS .get() .await .lock() .await .board_hal .deep_sleep(0) .await; } } } #[esp_hal_embassy::main] async fn main(spawner: Spawner) -> ! { // intialize embassy logger::init_logger_from_env(); //force init here! println!("Hal init"); match BOARD_ACCESS.init(PlantHal::create(spawner).await.unwrap()) { Ok(_) => {} Err(_) => { panic!("Could not set hal to static") } } println!("Hal init done, starting logic"); match safe_main(spawner).await { // this should not get triggered, safe_main should not return but go into deep sleep or reboot Ok(_) => { panic!("Main app finished, but should never do, restarting"); } // if safe_main exists with an error, rollback to a known good ota version Err(err) => { panic!("Failed main {}", err); } } } pub fn in_time_range(cur: &DateTime, start: u8, end: u8) -> bool { let current_hour = cur.hour() as u8; //eg 10-14 if start < end { current_hour > start && current_hour < end } else { //eg 20-05 current_hour > start || current_hour < end } } async fn get_version( board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, ) -> VersionInfo { let branch = env!("VERGEN_GIT_BRANCH").to_owned(); let hash = &env!("VERGEN_GIT_SHA")[0..8]; let board = board.board_hal.get_esp(); let ota_slot = board.get_ota_slot(); VersionInfo { git_hash: branch + "@" + hash, build_time: env!("VERGEN_BUILD_TIMESTAMP").to_owned(), partition: ota_slot, } } #[derive(Serialize, Debug)] struct VersionInfo { git_hash: String, build_time: String, partition: String, }