cleanup main and network state handling

This commit is contained in:
Empire Phoenix 2025-05-27 23:47:14 +02:00
parent 4f4d15e4a4
commit 3fe9aaeb6f
4 changed files with 204 additions and 145 deletions

View File

@ -2,9 +2,9 @@ use std::{
fmt::Display, fmt::Display,
sync::{atomic::AtomicBool, Arc, Mutex}, sync::{atomic::AtomicBool, Arc, Mutex},
}; };
use std::sync::MutexGuard;
use anyhow::{bail, Result}; use anyhow::bail;
use chrono::{DateTime, Datelike, Timelike}; use chrono::{DateTime, Datelike, Timelike, Utc};
use chrono_tz::Tz; use chrono_tz::Tz;
use chrono_tz::Tz::UTC; use chrono_tz::Tz::UTC;
use esp_idf_hal::delay::Delay; use esp_idf_hal::delay::Delay;
@ -20,14 +20,12 @@ use log::{log, LogMessage};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use plant_hal::{PlantCtrlBoard, PlantHal, PLANT_COUNT}; use plant_hal::{PlantCtrlBoard, PlantHal, PLANT_COUNT};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{config::PlantControllerConfig, webserver::webserver::httpd}; use crate::{config::PlantControllerConfig, webserver::webserver::httpd};
mod config; mod config;
mod log; mod log;
pub mod plant_hal; pub mod plant_hal;
mod plant_state; mod plant_state;
mod tank; mod tank;
pub mod util;
use plant_state::PlantState; use plant_state::PlantState;
use tank::*; use tank::*;
@ -71,6 +69,13 @@ struct LightState {
is_day: bool, 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)] #[derive(Serialize, Deserialize, Debug, PartialEq)]
/// humidity sensor error /// humidity sensor error
enum SensorError { enum SensorError {
@ -79,7 +84,21 @@ enum SensorError {
OpenCircuit { hz: f32, min: f32 }, OpenCircuit { hz: f32, min: f32 },
} }
fn safe_main() -> Result<()> { #[derive(Serialize, Debug, PartialEq)]
enum SntpMode {
OFFLINE,
SYNC{
current: DateTime<Utc>
}
}
#[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 // 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 // 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(); esp_idf_svc::sys::link_patches();
@ -145,7 +164,7 @@ fn safe_main() -> Result<()> {
"", "",
); );
let mut cur = board let cur = board
.get_rtc_time() .get_rtc_time()
.or_else(|err| { .or_else(|err| {
println!("rtc module error: {:?}", 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"); println!("attempting to connect wifi");
let mut ip_address: Option<String> = None; let network_mode = if config.network.ssid.is_some() {
if config.network.ssid.is_some() { try_connect_wifi_sntp_mqtt(&mut board, &config)
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);
}
}
} else { } else {
println!("No wifi configured"); 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!"); println!("Could not connect to station and config mode forced, switching to ap mode!");
match board.wifi_ap(Some(config.network.ap_ssid.clone())) { match board.wifi_ap(Some(config.network.ap_ssid.clone())) {
Ok(_) => { Ok(_) => {
@ -288,36 +267,17 @@ fn safe_main() -> Result<()> {
timezone_time timezone_time
); );
if mqtt { if let NetworkMode::WIFI { ref ip_address, .. } = network_mode {
let ip_string = ip_address.unwrap_or("N/A".to_owned()); publish_firmware_info(version, address, ota_state_string, &mut board, &config, &ip_address, timezone_time);
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());
publish_battery_state(&mut board, &config); publish_battery_state(&mut board, &config);
} }
log( log(
LogMessage::StartupInfo, LogMessage::StartupInfo,
wifi as u32, matches!(network_mode, NetworkMode::WIFI { .. }) as u32,
sntp as u32, matches!(network_mode, NetworkMode::WIFI { sntp: SntpMode::SYNC { .. }, .. }) as u32,
&mqtt.to_string(), matches!(network_mode, NetworkMode::WIFI { mqtt: true, .. }).to_string().as_str(),
"", "",
); );
@ -369,54 +329,18 @@ fn safe_main() -> Result<()> {
let mut water_frozen = false; let mut water_frozen = false;
//multisample should be moved to water_temperature_c let water_temp = obtain_tank_temperature(&mut board);
let mut attempt = 1;
let water_temp: Result<f32, anyhow::Error> = 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;
};
if let Ok(res) = water_temp { if let Ok(res) = water_temp {
if res < WATER_FROZEN_THRESH { if res < WATER_FROZEN_THRESH {
water_frozen = true; water_frozen = true;
} }
} }
match serde_json::to_string(&tank_state.as_mqtt_info(&config.tank, water_temp)) { publish_tank_state(&mut board, &config, &tank_state, &water_temp);
Ok(state) => {
let _ = board.mqtt_publish(&config, "/water", state.as_bytes());
}
Err(err) => {
println!("Error publishing tankstate {}", err);
}
};
let plantstate: [PlantState; PLANT_COUNT] = let plantstate: [PlantState; PLANT_COUNT] =
core::array::from_fn(|i| PlantState::read_hardware_state(i, &mut board, &config.plants[i])); 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() { publish_plant_states(&mut board, &config, &timezone_time, &plantstate);
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);
}
};
}
let pump_required = plantstate let pump_required = plantstate
.iter() .iter()
@ -426,24 +350,24 @@ fn safe_main() -> Result<()> {
if pump_required { if pump_required {
log(LogMessage::EnableMain, dry_run as u32, 0, "", ""); log(LogMessage::EnableMain, dry_run as u32, 0, "", "");
if !dry_run { 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() { for (plant_id, (state, plant_config)) in plantstate.iter().zip(&config.plants).enumerate() {
if state.needs_to_be_watered(plant_config, &timezone_time) { if state.needs_to_be_watered(plant_config, &timezone_time) {
let pump_count = board.consecutive_pump_count(plant_id) + 1; let pump_count = board.consecutive_pump_count(plant_id) + 1;
board.store_consecutive_pump_count(plant_id, pump_count); 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 { let pump_ineffective = pump_count > plant_config.max_consecutive_pump_count as u32;
// log( if pump_ineffective {
// log::LogMessage::ConsecutivePumpCountLimit, log(
// state.consecutive_pump_count as u32, LogMessage::ConsecutivePumpCountLimit,
// plant_config.max_consecutive_pump_count as u32, pump_count as u32,
// &plant.to_string(), plant_config.max_consecutive_pump_count as u32,
// "", &(plant_id+1).to_string(),
// ); "",
// state.not_effective = true; );
// board.fault(plant, true); board.fault(plant_id, true);
//} }
log( log(
LogMessage::PumpPlant, LogMessage::PumpPlant,
(plant_id + 1) as u32, (plant_id + 1) as u32,
@ -454,17 +378,25 @@ fn safe_main() -> Result<()> {
board.store_last_pump_time(plant_id, cur); board.store_last_pump_time(plant_id, cur);
board.last_pump_time(plant_id); board.last_pump_time(plant_id);
//state.active = true; //state.active = true;
pump_info(&mut board, &config, plant_id, true, pump_ineffective);
if !dry_run { if !dry_run {
board.pump(plant_id, true)?; board.pump(plant_id, true)?;
Delay::new_default().delay_ms(1000 * plant_config.pump_time_s as u32); Delay::new_default().delay_ms(1000 * plant_config.pump_time_s as u32);
board.pump(plant_id, false)?; 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) { } else if !state.pump_in_timeout(plant_config, &timezone_time) {
// plant does not need to be watered and is not in timeout // plant does not need to be watered and is not in timeout
// -> reset consecutive pump count // -> reset consecutive pump count
board.store_consecutive_pump_count(plant_id, 0); 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(); 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); board.deep_sleep(1000 * 1000 * 60 * deep_sleep_duration_minutes as u64);
} }
fn obtain_tank_temperature(board: &mut MutexGuard<PlantCtrlBoard>) -> anyhow::Result<f32> {
//multisample should be moved to water_temperature_c
let mut attempt = 1;
let water_temp: Result<f32, anyhow::Error> = 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<PlantCtrlBoard>, config: &PlantControllerConfig, tank_state: &TankState, water_temp: &anyhow::Result<f32>) {
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<PlantCtrlBoard>, config: &PlantControllerConfig, timezone_time: &DateTime<Tz>, 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<PlantCtrlBoard>, config: &PlantControllerConfig, ip_address: &String, timezone_time: DateTime<Tz>) {
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<PlantCtrlBoard>, 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<PlantCtrlBoard>, 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( fn publish_battery_state(
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
config: &PlantControllerConfig, config: &PlantControllerConfig,
@ -647,7 +716,7 @@ fn main() {
} }
} }
fn to_string<T: Display>(value: Result<T>) -> String { fn to_string<T: Display>(value: anyhow::Result<T>) -> String {
match value { match value {
Ok(v) => v.to_string(), Ok(v) => v.to_string(),
Err(err) => { Err(err) => {
@ -690,4 +759,4 @@ struct VersionInfo {
git_hash: String, git_hash: String,
build_time: String, build_time: String,
partition: String, partition: String,
} }

View File

@ -119,7 +119,7 @@ impl TankState {
pub fn as_mqtt_info( pub fn as_mqtt_info(
&self, &self,
config: &TankConfig, config: &TankConfig,
water_temp: Result<f32, anyhow::Error>, water_temp: &anyhow::Result<f32>,
) -> TankInfo { ) -> TankInfo {
let mut tank_err: Option<TankError> = None; let mut tank_err: Option<TankError> = None;
let left_ml = match self.left_ml(config) { let left_ml = match self.left_ml(config) {
@ -151,7 +151,7 @@ impl TankState {
.as_ref() .as_ref()
.is_ok_and(|temp| *temp < WATER_FROZEN_THRESH), .is_ok_and(|temp| *temp < WATER_FROZEN_THRESH),
water_temp: water_temp.as_ref().copied().ok(), 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, percent,
} }
} }

View File

@ -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
}
}

View File

@ -2,7 +2,7 @@
use crate::{ use crate::{
determine_tank_state, get_version, log::LogMessage, plant_hal::PLANT_COUNT, 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 anyhow::bail;
use chrono::DateTime; use chrono::DateTime;
@ -273,7 +273,7 @@ fn tank_info(
//should be multsampled //should be multsampled
let water_temp = board.water_temperature_c(); let water_temp = board.water_temperature_c();
Ok(Some(serde_json::to_string( Ok(Some(serde_json::to_string(
&tank_info.as_mqtt_info(&config.tank, water_temp), &tank_info.as_mqtt_info(&config.tank, &water_temp),
)?)) )?))
} }