From 3fe9aaeb6f3e8b040c2501712975a1d58007f284 Mon Sep 17 00:00:00 2001 From: Empire Phoenix Date: Tue, 27 May 2025 23:47:14 +0200 Subject: [PATCH] cleanup main and network state handling --- rust/src/main.rs | 331 +++++++++++++++++++------------- rust/src/tank.rs | 4 +- rust/src/util.rs | 10 - rust/src/webserver/webserver.rs | 4 +- 4 files changed, 204 insertions(+), 145 deletions(-) delete mode 100644 rust/src/util.rs diff --git a/rust/src/main.rs b/rust/src/main.rs index e4a6b93..8a0a643 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -2,9 +2,9 @@ use std::{ fmt::Display, sync::{atomic::AtomicBool, Arc, Mutex}, }; - -use anyhow::{bail, Result}; -use chrono::{DateTime, Datelike, Timelike}; +use std::sync::MutexGuard; +use anyhow::bail; +use chrono::{DateTime, Datelike, Timelike, Utc}; use chrono_tz::Tz; use chrono_tz::Tz::UTC; use esp_idf_hal::delay::Delay; @@ -20,14 +20,12 @@ use log::{log, LogMessage}; use once_cell::sync::Lazy; use plant_hal::{PlantCtrlBoard, PlantHal, PLANT_COUNT}; use serde::{Deserialize, Serialize}; - use crate::{config::PlantControllerConfig, webserver::webserver::httpd}; mod config; mod log; pub mod plant_hal; mod plant_state; mod tank; -pub mod util; use plant_state::PlantState; use tank::*; @@ -71,6 +69,13 @@ struct LightState { is_day: bool, } +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] +///mqtt stuct to track pump activities +struct PumpInfo{ + enabled: bool, + pump_ineffective: bool, +} + #[derive(Serialize, Deserialize, Debug, PartialEq)] /// humidity sensor error enum SensorError { @@ -79,7 +84,21 @@ enum SensorError { OpenCircuit { hz: f32, min: f32 }, } -fn safe_main() -> Result<()> { +#[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, +} + +fn safe_main() -> anyhow::Result<()> { // It is necessary to call this function once. Otherwise, some patches to the runtime // implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71 esp_idf_svc::sys::link_patches(); @@ -145,7 +164,7 @@ fn safe_main() -> Result<()> { "", ); - let mut cur = board + let cur = board .get_rtc_time() .or_else(|err| { println!("rtc module error: {:?}", err); @@ -214,55 +233,15 @@ fn safe_main() -> Result<()> { } }; - let mut wifi = false; - let mut mqtt = false; - let mut sntp = false; println!("attempting to connect wifi"); - let mut ip_address: Option = None; - if config.network.ssid.is_some() { - match board.wifi( - config.network.ssid.clone().unwrap(), - config.network.password.clone(), - 10000, - ) { - Ok(ip_info) => { - ip_address = Some(ip_info.ip.to_string()); - wifi = true; - - match board.sntp(1000 * 10) { - Ok(new_time) => { - println!("Using time from sntp"); - let _ = board.set_rtc_time(&new_time); - cur = new_time; - sntp = true; - } - Err(err) => { - println!("sntp error: {}", err); - board.general_fault(true); - } - } - if config.network.mqtt_url.is_some() { - match board.mqtt(&config) { - Ok(_) => { - println!("Mqtt connection ready"); - mqtt = true; - } - Err(err) => { - println!("Could not connect mqtt due to {}", err); - } - } - } - } - Err(_) => { - println!("Offline mode"); - board.general_fault(true); - } - } + let network_mode = if config.network.ssid.is_some() { + try_connect_wifi_sntp_mqtt(&mut board, &config) } else { println!("No wifi configured"); - } + NetworkMode::OFFLINE + }; - if !wifi && to_config { + if matches!(network_mode, NetworkMode::OFFLINE) && to_config { println!("Could not connect to station and config mode forced, switching to ap mode!"); match board.wifi_ap(Some(config.network.ap_ssid.clone())) { Ok(_) => { @@ -288,36 +267,17 @@ fn safe_main() -> Result<()> { timezone_time ); - if mqtt { - let ip_string = ip_address.unwrap_or("N/A".to_owned()); - let _ = board.mqtt_publish(&config, "/firmware/address", ip_string.as_bytes()); - let _ = board.mqtt_publish(&config, "/firmware/githash", version.git_hash.as_bytes()); - let _ = board.mqtt_publish( - &config, - "/firmware/buildtime", - version.build_time.as_bytes(), - ); - let _ = board.mqtt_publish( - &config, - "/firmware/last_online", - timezone_time.to_rfc3339().as_bytes(), - ); - let _ = board.mqtt_publish(&config, "/firmware/ota_state", ota_state_string.as_bytes()); - let _ = board.mqtt_publish( - &config, - "/firmware/partition_address", - format!("{:#06x}", address).as_bytes(), - ); - let _ = board.mqtt_publish(&config, "/state", "online".as_bytes()); - + if let NetworkMode::WIFI { ref ip_address, .. } = network_mode { + publish_firmware_info(version, address, ota_state_string, &mut board, &config, &ip_address, timezone_time); publish_battery_state(&mut board, &config); } + log( LogMessage::StartupInfo, - wifi as u32, - sntp as u32, - &mqtt.to_string(), + 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(), "", ); @@ -369,54 +329,18 @@ fn safe_main() -> Result<()> { let mut water_frozen = false; - //multisample should be moved to water_temperature_c - let mut attempt = 1; - let water_temp: Result = loop { - let temp = board.water_temperature_c(); - match &temp { - Ok(res) => { - println!("Water temp is {}", res); - break temp; - } - Err(err) => { - println!("Could not get water temp {} attempt {}", err, attempt) - } - } - if attempt == 5 { - break temp; - } - attempt += 1; - }; + let water_temp = obtain_tank_temperature(&mut board); if let Ok(res) = water_temp { if res < WATER_FROZEN_THRESH { water_frozen = true; } } - match serde_json::to_string(&tank_state.as_mqtt_info(&config.tank, water_temp)) { - Ok(state) => { - let _ = board.mqtt_publish(&config, "/water", state.as_bytes()); - } - Err(err) => { - println!("Error publishing tankstate {}", err); - } - }; + publish_tank_state(&mut board, &config, &tank_state, &water_temp); let plantstate: [PlantState; PLANT_COUNT] = core::array::from_fn(|i| PlantState::read_hardware_state(i, &mut board, &config.plants[i])); - for (plant_id, (plant_state, plant_conf)) in plantstate.iter().zip(&config.plants).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.mqtt_publish(&config, &plant_topic, state.as_bytes()); - //reduce speed as else messages will be dropped - Delay::new_default().delay_ms(200); - } - Err(err) => { - println!("Error publishing plant state {}", err); - } - }; - } + publish_plant_states(&mut board, &config, &timezone_time, &plantstate); let pump_required = plantstate .iter() @@ -426,24 +350,24 @@ fn safe_main() -> Result<()> { if pump_required { log(LogMessage::EnableMain, dry_run as u32, 0, "", ""); if !dry_run { - board.any_pump(true)?; // what does this do? Does it need to be reset? + board.any_pump(true)?; // enables main power output, eg for a central pump with valve setup or a main water valve for the risk affine } for (plant_id, (state, plant_config)) in plantstate.iter().zip(&config.plants).enumerate() { if state.needs_to_be_watered(plant_config, &timezone_time) { let pump_count = board.consecutive_pump_count(plant_id) + 1; board.store_consecutive_pump_count(plant_id, pump_count); - //TODO(judge) where to put this? - //if state.consecutive_pump_count > plant_config.max_consecutive_pump_count as u32 { - // log( - // log::LogMessage::ConsecutivePumpCountLimit, - // state.consecutive_pump_count as u32, - // plant_config.max_consecutive_pump_count as u32, - // &plant.to_string(), - // "", - // ); - // state.not_effective = true; - // board.fault(plant, true); - //} + + let pump_ineffective = pump_count > plant_config.max_consecutive_pump_count as u32; + if pump_ineffective { + log( + LogMessage::ConsecutivePumpCountLimit, + pump_count as u32, + plant_config.max_consecutive_pump_count as u32, + &(plant_id+1).to_string(), + "", + ); + board.fault(plant_id, true); + } log( LogMessage::PumpPlant, (plant_id + 1) as u32, @@ -454,17 +378,25 @@ fn safe_main() -> Result<()> { board.store_last_pump_time(plant_id, cur); board.last_pump_time(plant_id); //state.active = true; + + pump_info(&mut board, &config, plant_id, true, pump_ineffective); + if !dry_run { board.pump(plant_id, true)?; Delay::new_default().delay_ms(1000 * plant_config.pump_time_s as u32); board.pump(plant_id, false)?; } + pump_info(&mut board, &config, plant_id, false, pump_ineffective); + } 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.store_consecutive_pump_count(plant_id, 0); } } + if !dry_run { + board.any_pump(false)?; // disable main power output, eg for a central pump with valve setup or a main water valve for the risk affine + } } let is_day = board.is_day(); @@ -552,6 +484,143 @@ fn safe_main() -> Result<()> { board.deep_sleep(1000 * 1000 * 60 * deep_sleep_duration_minutes as u64); } +fn obtain_tank_temperature(board: &mut MutexGuard) -> anyhow::Result { + //multisample should be moved to water_temperature_c + let mut attempt = 1; + let water_temp: Result = loop { + let temp = board.water_temperature_c(); + match &temp { + Ok(res) => { + println!("Water temp is {}", res); + break temp; + } + Err(err) => { + println!("Could not get water temp {} attempt {}", err, attempt) + } + } + if attempt == 5 { + break temp; + } + attempt += 1; + }; + water_temp +} + +fn publish_tank_state(board: &mut MutexGuard, config: &PlantControllerConfig, tank_state: &TankState, water_temp: &anyhow::Result) { + match serde_json::to_string(&tank_state.as_mqtt_info(&config.tank, water_temp)) { + Ok(state) => { + let _ = board.mqtt_publish(&config, "/water", state.as_bytes()); + } + Err(err) => { + println!("Error publishing tankstate {}", err); + } + }; +} + +fn publish_plant_states(board: &mut MutexGuard, config: &PlantControllerConfig, timezone_time: &DateTime, plantstate: &[PlantState; 8]) { + for (plant_id, (plant_state, plant_conf)) in plantstate.iter().zip(&config.plants).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.mqtt_publish(&config, &plant_topic, state.as_bytes()); + //reduce speed as else messages will be dropped + Delay::new_default().delay_ms(200); + } + Err(err) => { + println!("Error publishing plant state {}", err); + } + }; + } +} + +fn publish_firmware_info(version: VersionInfo, address: u32, ota_state_string: &str, board: &mut MutexGuard, config: &PlantControllerConfig, ip_address: &String, timezone_time: DateTime) { + let _ = board.mqtt_publish(&config, "/firmware/address", ip_address.as_bytes()); + let _ = board.mqtt_publish(&config, "/firmware/githash", version.git_hash.as_bytes()); + let _ = board.mqtt_publish( + &config, + "/firmware/buildtime", + version.build_time.as_bytes(), + ); + let _ = board.mqtt_publish( + &config, + "/firmware/last_online", + timezone_time.to_rfc3339().as_bytes(), + ); + let _ = board.mqtt_publish(&config, "/firmware/ota_state", ota_state_string.as_bytes()); + let _ = board.mqtt_publish( + &config, + "/firmware/partition_address", + format!("{:#06x}", address).as_bytes(), + ); + let _ = board.mqtt_publish(&config, "/state", "online".as_bytes()); +} + +fn try_connect_wifi_sntp_mqtt(board: &mut MutexGuard, config: &PlantControllerConfig) -> NetworkMode{ + match board.wifi( + config.network.ssid.clone().unwrap(), + config.network.password.clone(), + 10000, + ) { + Ok(ip_info) => { + let sntp_mode: SntpMode = match board.sntp(1000 * 10) { + Ok(new_time) => { + println!("Using time from sntp"); + let _ = board.set_rtc_time(&new_time); + SntpMode::SYNC {current: new_time} + } + Err(err) => { + println!("sntp error: {}", err); + board.general_fault(true); + SntpMode::OFFLINE + } + }; + let mqtt_connected = if let Some(_) = config.network.mqtt_url { + match board.mqtt(&config) { + Ok(_) => { + println!("Mqtt connection ready"); + true + } + Err(err) => { + println!("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(_) => { + println!("Offline mode"); + board.general_fault(true); + NetworkMode::OFFLINE + } + } +} + +//TODO clean this up? better state +fn pump_info(board: &mut MutexGuard, config: &PlantControllerConfig, plant_id: usize, pump_active: bool, pump_ineffective: bool) { + let pump_info = PumpInfo { + enabled: pump_active, + pump_ineffective + }; + let pump_topic = format!("/pump{}", plant_id + 1); + match serde_json::to_string(&pump_info) { + Ok(state) => { + let _ = board.mqtt_publish(config, &pump_topic, state.as_bytes()); + //reduce speed as else messages will be dropped + Delay::new_default().delay_ms(200); + } + Err(err) => { + println!("Error publishing pump state {}", err); + } + }; +} + fn publish_battery_state( board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, config: &PlantControllerConfig, @@ -647,7 +716,7 @@ fn main() { } } -fn to_string(value: Result) -> String { +fn to_string(value: anyhow::Result) -> String { match value { Ok(v) => v.to_string(), Err(err) => { @@ -690,4 +759,4 @@ struct VersionInfo { git_hash: String, build_time: String, partition: String, -} +} \ No newline at end of file diff --git a/rust/src/tank.rs b/rust/src/tank.rs index f7c77a2..7c426ef 100644 --- a/rust/src/tank.rs +++ b/rust/src/tank.rs @@ -119,7 +119,7 @@ impl TankState { pub fn as_mqtt_info( &self, config: &TankConfig, - water_temp: Result, + water_temp: &anyhow::Result, ) -> TankInfo { let mut tank_err: Option = None; let left_ml = match self.left_ml(config) { @@ -151,7 +151,7 @@ impl TankState { .as_ref() .is_ok_and(|temp| *temp < WATER_FROZEN_THRESH), water_temp: water_temp.as_ref().copied().ok(), - temp_sensor_error: water_temp.err().map(|err| err.to_string()), + temp_sensor_error: water_temp.as_ref().err().map(|err| err.to_string()), percent, } } diff --git a/rust/src/util.rs b/rust/src/util.rs deleted file mode 100644 index 78f24a5..0000000 --- a/rust/src/util.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub trait LimitPrecision { - fn to_precision(self, precision: i32) -> Self; -} - -impl LimitPrecision for f32 { - fn to_precision(self, precision: i32) -> Self { - let factor = 10_f32.powi(precision); - (self * factor).round() / factor - } -} diff --git a/rust/src/webserver/webserver.rs b/rust/src/webserver/webserver.rs index 39dcce0..ea11f34 100644 --- a/rust/src/webserver/webserver.rs +++ b/rust/src/webserver/webserver.rs @@ -2,7 +2,7 @@ use crate::{ determine_tank_state, get_version, log::LogMessage, plant_hal::PLANT_COUNT, - plant_state::PlantState, util::LimitPrecision, BOARD_ACCESS, + plant_state::PlantState, BOARD_ACCESS, }; use anyhow::bail; use chrono::DateTime; @@ -273,7 +273,7 @@ fn tank_info( //should be multsampled let water_temp = board.water_temperature_c(); Ok(Some(serde_json::to_string( - &tank_info.as_mqtt_info(&config.tank, water_temp), + &tank_info.as_mqtt_info(&config.tank, &water_temp), )?)) }