#![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." )] //TODO insert version here and read it in other parts, also read this for the ota webview esp_bootloader_esp_idf::esp_app_desc!(); use esp_backtrace as _; use crate::config::{NetworkConfig, PlantConfig}; use crate::fat_error::FatResult; use crate::hal::esp::MQTT_STAY_ALIVE; use crate::hal::{esp_time, TIME_ACCESS}; use crate::hal::PROGRESS_ACTIVE; use crate::log::{log, LOG_ACCESS}; use crate::tank::{determine_tank_state, TankError, TankState, WATER_FROZEN_THRESH}; use crate::webserver::http_server; 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 option_lock::OptionLock; 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 config; mod fat_error; mod hal; mod log; mod plant_state; mod tank; mod webserver; extern crate alloc; //mod webserver; pub static BOARD_ACCESS: OnceLock>> = OnceLock::new(); #[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) => { { let guard = TIME_ACCESS.get().await.lock().await; guard.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(http_server(reboot_now.clone(), stack))?; wait_infinity(board, WaitType::MissingConfig, reboot_now.clone()).await; } let mut stack: OptionLock = OptionLock::empty(); 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 the case of Wi-Fi this is already handled due to connect timings; 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.replace(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 = 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(http_server(reboot_now.clone(), stack.take().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; } } info!("Water temp is {}", water_temp.as_ref().unwrap_or(&0.)); publish_tank_state(&mut board, &tank_state, water_temp).await; let moisture = board.board_hal.measure_moisture_hz().await?; let plantstate: [PlantState; PLANT_COUNT] = [ PlantState::read_hardware_state(moisture,0, &mut board).await, PlantState::read_hardware_state(moisture,1, &mut board).await, PlantState::read_hardware_state(moisture,2, &mut board).await, PlantState::read_hardware_state(moisture,3, &mut board).await, PlantState::read_hardware_state(moisture,4, &mut board).await, PlantState::read_hardware_state(moisture,5, &mut board).await, PlantState::read_hardware_state(moisture,6, &mut board).await, PlantState::read_hardware_state(moisture,7, &mut board).await, ]; publish_plant_states(&mut board, &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_ACCESS .lock() .await .log(LogMessage::EnableMain, dry_run as u32, 0, "", "") .await; 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(), "", ) .await; 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(), "", ) .await; 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(&mut board, plant_id, plant_config, dry_run).await?; //stop pump regardless of prior result//todo refactor to inner? 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); } } } info!("state of charg"); 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); info!("Battery state is {:?}", battery_state); 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(); info!("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(); info!("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) .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").await; 12 * 60 } else if is_day { let _ = board .board_hal .get_esp() .mqtt_publish("/deepsleep", "normal 20m").await; 20 } else { let _ = board .board_hal .get_esp() .mqtt_publish("/deepsleep", "night 1h").await; 60 }; let _ = board .board_hal .get_esp() .mqtt_publish("/state", "sleep") .await; info!("Go to sleep for {} minutes", deep_sleep_duration_minutes); //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); info!("Check stay alive, current state is {}", stay_alive); if stay_alive { let reboot_now = Arc::new(AtomicBool::new(false)); let _webserver = http_server(reboot_now.clone(), stack.take().unwrap()); wait_infinity(board, WaitType::MqttConfig, reboot_now.clone()).await; } else { //TODO wait for all mqtt publishes? Timer::after_millis(5000).await; 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(); 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( 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( 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( LogMessage::PumpMissingSensorCurrent, plant_id as u32, 0, "", "", ) .await; error = true; break; } else { //e.g., v3 without a sensor ends here, do not spam } } } Timer::after_millis(1000).await; pump_time_s += 1; } } board.board_hal.get_tank_sensor()?.stop_flow_meter(); let final_flow_value = board.board_hal.get_tank_sensor()?.get_flow_meter_value(); 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<'static, CriticalSectionRawMutex, HAL<'static>>, ) { //we have mppt controller, ask it for charging current 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( board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, tank_state: &TankState, water_temp: FatResult, ) { let state = serde_json::to_string( &tank_state.as_mqtt_info(&board.board_hal.get_config().tank, &water_temp), ) .unwrap(); let _ = board.board_hal.get_esp().mqtt_publish("/water", &*state); } async fn publish_plant_states( board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, timezone_time: &DateTime, plantstate: &[PlantState; 8], ) { for (plant_id, (plant_state, plant_conf)) in plantstate .iter() .zip(&board.board_hal.get_config().plants.clone()) .enumerate() { let state = serde_json::to_string(&plant_state.to_mqtt_info(plant_conf, timezone_time)).unwrap(); let plant_topic = format!("/plant{}", plant_id + 1); let _ = board .board_hal .get_esp() .mqtt_publish(&plant_topic, &state) .await; } } 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).await; let _ = esp .mqtt_publish("/firmware/state", format!("{:?}", &version).as_str()) .await; let _ = esp.mqtt_publish("/firmware/last_online", timezone_time); let _ = esp.mqtt_publish("/state", "online").await; } 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 }}; } async fn try_connect_wifi_sntp_mqtt( board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>, stack_store: &mut OptionLock>, ) -> NetworkMode { let nw_conf = &board.board_hal.get_config().network.clone(); match board.board_hal.get_esp().wifi(nw_conf).await { Ok(stack) => { stack_store.replace(stack); let sntp_mode: SntpMode = match board .board_hal .get_esp() .sntp(1000 * 10, stack.clone()) .await { Ok(new_time) => { info!("Using time from sntp {}", new_time.to_rfc3339()); 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(); let nw_config = mk_static!(NetworkConfig, nw_config); match board.board_hal.get_esp().mqtt(nw_config, stack).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: stack.hardware_address().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, max_current_ma, min_current_ma, }; let pump_topic = format!("/pump{}", plant_id + 1); match serde_json::to_string(&pump_info) { Ok(state) => { BOARD_ACCESS .get() .await .lock() .await .board_hal .get_esp() .mqtt_publish(&pump_topic, &state) .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) { 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.to_owned() } Err(_) => "error".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; // Skip default blink code when a progress display is active if !PROGRESS_ACTIVE.load(Ordering::Relaxed) { 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).await; } } 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).await; } } 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).await; } } } board.board_hal.general_fault(true).await; } } Timer::after_millis(delay).await; { let mut board = BOARD_ACCESS.get().await.lock().await; // Skip clearing LEDs when progress is active to avoid interrupting the progress display if !PROGRESS_ACTIVE.load(Ordering::Relaxed) { board.board_hal.general_fault(false).await; // Clear all LEDs for i in 0..8 { let _ = board.board_hal.fault(i, false).await; } } } Timer::after_millis(delay).await; if wait_type == WaitType::MqttConfig && !MQTT_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! match BOARD_ACCESS.init(PlantHal::create().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(); VersionInfo { git_hash: branch + "@" + hash, build_time: env!("VERGEN_BUILD_TIMESTAMP").to_owned(), current: format!("{:?}", board.current), slot0_state: format!("{:?}", board.slot0_state), slot1_state: format!("{:?}", board.slot1_state), } } #[derive(Serialize, Debug)] struct VersionInfo { git_hash: String, build_time: String, current: String, slot0_state: String, slot1_state: String, }