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,
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<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
// 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<String> = 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<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;
};
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<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(
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
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 {
Ok(v) => v.to_string(),
Err(err) => {
@ -690,4 +759,4 @@ struct VersionInfo {
git_hash: String,
build_time: String,
partition: String,
}
}

View File

@ -119,7 +119,7 @@ impl TankState {
pub fn as_mqtt_info(
&self,
config: &TankConfig,
water_temp: Result<f32, anyhow::Error>,
water_temp: &anyhow::Result<f32>,
) -> TankInfo {
let mut tank_err: Option<TankError> = 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,
}
}

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::{
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),
)?))
}