refactor/plant-state-handling #11
| @@ -77,7 +77,7 @@ serde = { version = "1.0.192",  features = ["derive"] } | ||||
| serde_json = "1.0.108" | ||||
|  | ||||
| #timezone | ||||
| chrono = { version = "0.4.23", default-features = false , features = ["iana-time-zone" , "alloc"] } | ||||
| chrono = { version = "0.4.23", default-features = false , features = ["iana-time-zone" , "alloc", "serde"] } | ||||
| chrono-tz = {version="0.8.0", default-features = false , features = [ "filter-by-regex" ]} | ||||
| eeprom24x = "0.7.2" | ||||
| url = "2.5.3" | ||||
|   | ||||
| @@ -3,39 +3,39 @@ use std::process::Command; | ||||
| use vergen::EmitBuilder; | ||||
| fn main() { | ||||
|     println!("cargo:rerun-if-changed=./src/src_webpack"); | ||||
|     Command::new("rm") | ||||
|         .arg("./src/webserver/bundle.js") | ||||
|         .output() | ||||
|         .unwrap(); | ||||
|  | ||||
|     match Command::new("cmd").spawn() { | ||||
|         Ok(_) => { | ||||
|             println!("Assuming build on windows"); | ||||
|             let output = Command::new("cmd") | ||||
|                 .arg("/K") | ||||
|                 .arg("npx") | ||||
|                 .arg("webpack") | ||||
|                 .current_dir("./src_webpack") | ||||
|                 .output() | ||||
|                 .unwrap(); | ||||
|             println!("status: {}", output.status); | ||||
|             println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); | ||||
|             println!("stderr: {}", String::from_utf8_lossy(&output.stderr)); | ||||
|             assert!(output.status.success()); | ||||
|         Command::new("rm") | ||||
|             .arg("./src/webserver/bundle.js") | ||||
|             .output() | ||||
|             .unwrap(); | ||||
|      | ||||
|         match Command::new("cmd").spawn() { | ||||
|             Ok(_) => { | ||||
|                 println!("Assuming build on windows"); | ||||
|                 let output = Command::new("cmd") | ||||
|                     .arg("/K") | ||||
|                     .arg("npx") | ||||
|                     .arg("webpack") | ||||
|                     .current_dir("./src_webpack") | ||||
|                     .output() | ||||
|                     .unwrap(); | ||||
|                 println!("status: {}", output.status); | ||||
|                 println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); | ||||
|                 println!("stderr: {}", String::from_utf8_lossy(&output.stderr)); | ||||
|                 assert!(output.status.success()); | ||||
|             } | ||||
|             Err(_) => { | ||||
|                 println!("Assuming build on linux"); | ||||
|                 let output = Command::new("npx") | ||||
|                     .arg("webpack") | ||||
|                     .current_dir("./src_webpack") | ||||
|                     .output() | ||||
|                     .unwrap(); | ||||
|                 println!("status: {}", output.status); | ||||
|                 println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); | ||||
|                 println!("stderr: {}", String::from_utf8_lossy(&output.stderr)); | ||||
|                 assert!(output.status.success()); | ||||
|             } | ||||
|         } | ||||
|         Err(_) => { | ||||
|             println!("Assuming build on linux"); | ||||
|             let output = Command::new("npx") | ||||
|                 .arg("webpack") | ||||
|                 .current_dir("./src_webpack") | ||||
|                 .output() | ||||
|                 .unwrap(); | ||||
|             println!("status: {}", output.status); | ||||
|             println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); | ||||
|             println!("stderr: {}", String::from_utf8_lossy(&output.stderr)); | ||||
|             assert!(output.status.success()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     embuild::espidf::sysenv::output(); | ||||
|     let _ = EmitBuilder::builder().all_git().all_build().emit(); | ||||
|   | ||||
| @@ -2,6 +2,7 @@ use std::str::FromStr; | ||||
|  | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| use crate::plant_state::PlantWateringMode; | ||||
| use crate::PLANT_COUNT; | ||||
|  | ||||
| #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] | ||||
| @@ -83,8 +84,8 @@ pub struct PlantControllerConfig { | ||||
| #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] | ||||
| #[serde(default)] | ||||
| pub struct PlantConfig { | ||||
|     pub mode: Mode, | ||||
|     pub target_moisture: u8, | ||||
|     pub mode: PlantWateringMode, | ||||
|     pub target_moisture: f32, | ||||
|     pub pump_time_s: u16, | ||||
|     pub pump_cooldown_min: u16, | ||||
|     pub pump_hour_start: u8, | ||||
| @@ -96,8 +97,8 @@ pub struct PlantConfig { | ||||
| impl Default for PlantConfig { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             mode: Mode::OFF, | ||||
|             target_moisture: 40, | ||||
|             mode: PlantWateringMode::OFF, | ||||
|             target_moisture: 40., | ||||
|             pump_time_s: 30, | ||||
|             pump_cooldown_min: 60, | ||||
|             pump_hour_start: 9, | ||||
| @@ -108,11 +109,3 @@ impl Default for PlantConfig { | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] | ||||
| pub enum Mode { | ||||
|     OFF, | ||||
|     TargetMoisture, | ||||
|     TimerOnly, | ||||
|     TimerAndDeadzone, | ||||
| } | ||||
|   | ||||
							
								
								
									
										292
									
								
								rust/src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										292
									
								
								rust/src/main.rs
									
									
									
									
									
								
							| @@ -7,7 +7,7 @@ use anyhow::{bail, Result}; | ||||
| use chrono::{DateTime, Datelike, TimeDelta, Timelike, Utc}; | ||||
| use chrono_tz::{Europe::Berlin, Tz}; | ||||
|  | ||||
| use config::Mode; | ||||
| use config::PlantWateringMode; | ||||
| use esp_idf_hal::delay::Delay; | ||||
| use esp_idf_sys::{ | ||||
|     esp_ota_get_app_partition_count, esp_ota_get_running_partition, esp_ota_get_state_partition, | ||||
| @@ -28,8 +28,9 @@ mod log; | ||||
| pub mod plant_hal; | ||||
| mod plant_state; | ||||
| mod tank; | ||||
| pub mod util; | ||||
|  | ||||
| use plant_state::{PlantInfo, PlantStateMQTT}; | ||||
| use plant_state::{PlantInfo, PlantState}; | ||||
| use tank::*; | ||||
|  | ||||
| const TIME_ZONE: Tz = Berlin; | ||||
| @@ -406,18 +407,10 @@ fn safe_main() -> anyhow::Result<()> { | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     let mut plantstate: [PlantInfo; PLANT_COUNT] = core::array::from_fn(|_| PlantInfo { | ||||
|         ..Default::default() | ||||
|     }); | ||||
|     determine_plant_state( | ||||
|         &mut plantstate, | ||||
|         timezone_time, | ||||
|         &tank_state, | ||||
|         &config, | ||||
|         &mut board, | ||||
|     ); | ||||
|     let mut plantstate: [PlantState; PLANT_COUNT] = | ||||
|         core::array::from_fn(|i| PlantState::read_hardware_state(i, &mut board, &config.plants[i])); | ||||
|  | ||||
|     let pump_required = plantstate.iter().any(|it| it.do_water) && !water_frozen; | ||||
|     let pump_required = plantstate.iter().any(|it| it.needs_to_be_watered(&config.plants[i], &timezone_time)) && !water_frozen; | ||||
|     if pump_required { | ||||
|         log(log::LogMessage::EnableMain, dry_run as u32, 0, "", ""); | ||||
|         if !dry_run { | ||||
| @@ -568,279 +561,6 @@ fn publish_battery_state( | ||||
|     }; | ||||
| } | ||||
|  | ||||
| fn determine_state_target_moisture_for_plant( | ||||
|     board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, | ||||
|     plant: usize, | ||||
|     state: &mut PlantInfo, | ||||
|     config: &PlantControllerConfig, | ||||
|     tank_state: &TankState, | ||||
|     cur: DateTime<Tz>, | ||||
| ) { | ||||
|     let plant_config = &config.plants[plant]; | ||||
|     if plant_config.mode == Mode::OFF { | ||||
|         return; | ||||
|     } | ||||
|     match board.measure_moisture_hz(plant, plant_hal::Sensor::A) { | ||||
|         Ok(a) => { | ||||
|             state.a_raw = Some(a); | ||||
|             let mapped = map_range_moisture(a as f32); | ||||
|             match mapped { | ||||
|                 Ok(result) => state.a = Some(result), | ||||
|                 Err(err) => { | ||||
|                     state.sensor_error_a = Some(err); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         Err(_) => { | ||||
|             state.sensor_error_a = Some(SensorError::Unknown); | ||||
|         } | ||||
|     } | ||||
|     if plant_config.sensor_b { | ||||
|         match board.measure_moisture_hz(plant, plant_hal::Sensor::B) { | ||||
|             Ok(b) => { | ||||
|                 state.b_raw = Some(b); | ||||
|                 let mapped = map_range_moisture(b as f32); | ||||
|                 match mapped { | ||||
|                     Ok(result) => state.b = Some(result), | ||||
|                     Err(err) => { | ||||
|                         state.sensor_error_b = Some(err); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             Err(_) => { | ||||
|                 state.sensor_error_b = Some(SensorError::Unknown); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     //FIXME how to average analyze whatever? | ||||
|     let a_low = state.a.is_some() && state.a.unwrap() < plant_config.target_moisture; | ||||
|     let b_low = state.b.is_some() && state.b.unwrap() < plant_config.target_moisture; | ||||
|  | ||||
|     if a_low || b_low { | ||||
|         state.dry = true; | ||||
|         match tank_state.enough_water(&config.tank) { | ||||
|             Err(_tank_err) => { | ||||
|                 if !config.tank.tank_allow_pumping_if_sensor_error { | ||||
|                     state.no_water = true; | ||||
|                 } | ||||
|             } | ||||
|             // when no tank error, if plant should be watered depends on if enough water is in tank | ||||
|             // no_water behaves inversly to enough_water | ||||
|             Ok(enough_water) => state.no_water = !enough_water, | ||||
|         } | ||||
|     } | ||||
|     let duration = TimeDelta::try_minutes(plant_config.pump_cooldown_min as i64).unwrap(); | ||||
|     let last_pump = board.last_pump_time(plant); | ||||
|     match last_pump { | ||||
|         Some(last_pump) => { | ||||
|             let next_pump = last_pump + duration; | ||||
|             if next_pump > cur { | ||||
|                 let local_time = next_pump.with_timezone(&TIME_ZONE); | ||||
|                 state.next_pump = Some(local_time); | ||||
|                 state.cooldown = true; | ||||
|             } | ||||
|         } | ||||
|         None => { | ||||
|             println!( | ||||
|                 "Could not restore last pump for plant {}, restoring", | ||||
|                 plant + 1 | ||||
|             ); | ||||
|             board.store_last_pump_time(plant, DateTime::from_timestamp_millis(0).unwrap()); | ||||
|             state.pump_error = true; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if !in_time_range( | ||||
|         &cur, | ||||
|         plant_config.pump_hour_start, | ||||
|         plant_config.pump_hour_end, | ||||
|     ) { | ||||
|         state.out_of_work_hour = true; | ||||
|     } | ||||
|     if state.dry && !state.no_water && !state.cooldown && !state.out_of_work_hour { | ||||
|         state.do_water = true; | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn determine_state_timer_only_for_plant( | ||||
|     board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, | ||||
|     plant: usize, | ||||
|     state: &mut PlantInfo, | ||||
|     config: &PlantControllerConfig, | ||||
|     tank_state: &TankState, | ||||
|     cur: DateTime<Tz>, | ||||
| ) { | ||||
|     let plant_config = &config.plants[plant]; | ||||
|     let duration = TimeDelta::try_minutes(plant_config.pump_cooldown_min as i64).unwrap(); | ||||
|  | ||||
|     let last_pump = board.last_pump_time(plant); | ||||
|     match last_pump { | ||||
|         Some(last_pump) => { | ||||
|             let next_pump = last_pump + duration; | ||||
|             if next_pump > cur { | ||||
|                 let europe_time = next_pump.with_timezone(&TIME_ZONE); | ||||
|                 state.next_pump = Some(europe_time); | ||||
|                 state.cooldown = true; | ||||
|             } else { | ||||
|                 match tank_state.enough_water(&config.tank) { | ||||
|                     Err(_tank_err) => { | ||||
|                         if !config.tank.tank_allow_pumping_if_sensor_error { | ||||
|                             state.do_water = true; | ||||
|                         } | ||||
|                     } | ||||
|                     Ok(enough_water) => { | ||||
|                         state.no_water = !enough_water; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         None => { | ||||
|             println!( | ||||
|                 "Could not restore last pump for plant {}, restoring", | ||||
|                 plant + 1 | ||||
|             ); | ||||
|             board.store_last_pump_time(plant, DateTime::from_timestamp_millis(0).unwrap()); | ||||
|             state.pump_error = true; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn determine_state_timer_and_deadzone_for_plant( | ||||
|     board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, | ||||
|     plant: usize, | ||||
|     state: &mut PlantInfo, | ||||
|     config: &PlantControllerConfig, | ||||
|     tank_state: &TankState, | ||||
|     cur: DateTime<Tz>, | ||||
| ) { | ||||
|     let plant_config = &config.plants[plant]; | ||||
|     let duration = TimeDelta::try_minutes(plant_config.pump_cooldown_min as i64).unwrap(); | ||||
|  | ||||
|     let last_pump = board.last_pump_time(plant); | ||||
|     match last_pump { | ||||
|         Some(last_pump) => { | ||||
|             let next_pump = last_pump + duration; | ||||
|             if next_pump > cur { | ||||
|                 let europe_time = next_pump.with_timezone(&TIME_ZONE); | ||||
|                 state.next_pump = Some(europe_time); | ||||
|                 state.cooldown = true; | ||||
|             } | ||||
|             if !in_time_range( | ||||
|                 &cur, | ||||
|                 plant_config.pump_hour_start, | ||||
|                 plant_config.pump_hour_end, | ||||
|             ) { | ||||
|                 state.out_of_work_hour = true; | ||||
|             } | ||||
|             if !state.cooldown && !state.out_of_work_hour { | ||||
|                 match tank_state.enough_water(&config.tank) { | ||||
|                     Err(_tank_err) => { | ||||
|                         if !config.tank.tank_allow_pumping_if_sensor_error { | ||||
|                             state.do_water = true; | ||||
|                         } | ||||
|                     } | ||||
|                     Ok(enough_water) => { | ||||
|                         state.no_water = !enough_water; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         None => { | ||||
|             println!( | ||||
|                 "Could not restore last pump for plant {}, restoring", | ||||
|                 plant + 1 | ||||
|             ); | ||||
|             board.store_last_pump_time(plant, DateTime::from_timestamp_millis(0).unwrap()); | ||||
|             state.pump_error = true; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn determine_plant_state( | ||||
|     plantstate: &mut [PlantInfo; PLANT_COUNT], | ||||
|     cur: DateTime<Tz>, | ||||
|     tank_state: &TankState, | ||||
|     config: &PlantControllerConfig, | ||||
|     board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, | ||||
| ) { | ||||
|     for plant in 0..PLANT_COUNT { | ||||
|         let state = &mut plantstate[plant]; | ||||
|         let plant_config = &config.plants[plant]; | ||||
|         match plant_config.mode { | ||||
|             config::Mode::OFF => {} | ||||
|             config::Mode::TargetMoisture => { | ||||
|                 determine_state_target_moisture_for_plant( | ||||
|                     board, plant, state, config, tank_state, cur, | ||||
|                 ); | ||||
|             } | ||||
|             config::Mode::TimerOnly => { | ||||
|                 determine_state_timer_only_for_plant(board, plant, state, config, tank_state, cur); | ||||
|             } | ||||
|             config::Mode::TimerAndDeadzone => { | ||||
|                 determine_state_timer_and_deadzone_for_plant( | ||||
|                     board, plant, state, config, tank_state, cur, | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if state.sensor_error_a.is_some() || state.sensor_error_b.is_some() { | ||||
|             board.fault(plant, true); | ||||
|         } | ||||
|         if !state.dry { | ||||
|             state.consecutive_pump_count = 0; | ||||
|             board.store_consecutive_pump_count(plant, 0); | ||||
|         } | ||||
|         println!("Plant {} state is {:?}", plant, state); | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn update_plant_state( | ||||
|     plantstate: &mut [PlantInfo; PLANT_COUNT], | ||||
|     board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, | ||||
|     config: &PlantControllerConfig, | ||||
| ) { | ||||
|     for plant in 0..PLANT_COUNT { | ||||
|         let state = &plantstate[plant]; | ||||
|         let plant_config = &config.plants[plant]; | ||||
|  | ||||
|         let mode = format!("{:?}", plant_config.mode); | ||||
|  | ||||
|         let plant_dto = PlantStateMQTT { | ||||
|             a: &sensor_to_string( | ||||
|                 &state.a, | ||||
|                 &state.sensor_error_a, | ||||
|                 plant_config.mode != Mode::OFF, | ||||
|             ), | ||||
|             a_raw: &state.a_raw.unwrap_or(0).to_string(), | ||||
|             b: &sensor_to_string(&state.b, &state.sensor_error_b, plant_config.sensor_b), | ||||
|             b_raw: &state.b_raw.unwrap_or(0).to_string(), | ||||
|             active: state.active, | ||||
|             mode: &mode, | ||||
|             last_pump: &time_to_string_utc(board.last_pump_time(plant)), | ||||
|             next_pump: &time_to_string(state.next_pump), | ||||
|             consecutive_pump_count: state.consecutive_pump_count, | ||||
|             cooldown: state.cooldown, | ||||
|             dry: state.dry, | ||||
|             not_effective: state.not_effective, | ||||
|             out_of_work_hour: state.out_of_work_hour, | ||||
|             pump_error: state.pump_error, | ||||
|         }; | ||||
|  | ||||
|         match serde_json::to_string(&plant_dto) { | ||||
|             Ok(state) => { | ||||
|                 let plant_topic = format!("/plant{}", plant + 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 lightstate {}", err); | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn wait_infinity(wait_type: WaitType, reboot_now: Arc<AtomicBool>) -> ! { | ||||
|     let delay = wait_type.blink_pattern(); | ||||
|  | ||||
|   | ||||
| @@ -1,68 +1,110 @@ | ||||
| use chrono::{DateTime, Utc}; | ||||
| use chrono::{DateTime, TimeDelta, Utc}; | ||||
| use chrono_tz::Tz; | ||||
| use serde::Serialize; | ||||
| use measurements::humidity; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| use crate::{config, plant_hal}; | ||||
| use crate::{ | ||||
|     config::{self, PlantConfig}, | ||||
|     plant_hal::{self, PLANT_COUNT}, | ||||
| }; | ||||
|  | ||||
| const MOIST_SENSOR_MAX_FREQUENCY: u32 = 5500; // 60kHz (500Hz margin) | ||||
| const MOIST_SENSOR_MIN_FREQUENCY: u32 = 150; // this is really really dry, think like cactus levels | ||||
|  | ||||
| pub enum HumiditySensorError{ | ||||
|     ShortCircuit{hz: f32, max: f32}, | ||||
|     OpenLoop{hz: f32, min: f32} | ||||
| pub enum HumiditySensorError { | ||||
|     ShortCircuit { hz: f32, max: f32 }, | ||||
|     OpenLoop { hz: f32, min: f32 }, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq)] | ||||
| pub enum HumiditySensorState { | ||||
|     Disabled, | ||||
|     HumidityValue{raw_hz: u32, moisture_percent: f32}, | ||||
|     HumidityValue { raw_hz: u32, moisture_percent: f32 }, | ||||
|     SensorError(HumiditySensorError), | ||||
|     BoardError(String) | ||||
|     BoardError(String), | ||||
| } | ||||
|  | ||||
| impl HumiditySensorState { | ||||
|     pub fn is_err(&self) -> bool { | ||||
|         matches!(self, Self::SensorError(_)) || matches!(self, Self::BoardError(_)) | ||||
|     } | ||||
|  | ||||
|     pub fn moisture_percent(&self) -> Option<f32> { | ||||
|         if let HumiditySensorState::HumidityValue { | ||||
|             raw_hz, | ||||
|             moisture_percent, | ||||
|         } = self | ||||
|         { | ||||
|             Some(moisture_percent) | ||||
|         } else { | ||||
|             None | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub enum PumpError {} | ||||
| impl HumiditySensorState {} | ||||
|  | ||||
| #[derive(Debug, PartialEq)] | ||||
| pub enum PumpError { | ||||
|     PumpNotWorking { | ||||
|         failed_attempts: usize, | ||||
|         max_allowed_failures: usize, | ||||
|     }, | ||||
| } | ||||
|  | ||||
| pub struct PumpState { | ||||
|     consecutive_pump_count: u32, | ||||
|     previous_pump: Option<DateTime<Utc>> | ||||
|     previous_pump: Option<DateTime<Utc>>, | ||||
| } | ||||
|  | ||||
| pub enum PlantError{} | ||||
| #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] | ||||
| pub enum PlantWateringMode { | ||||
|     OFF, | ||||
|     TargetMoisture, | ||||
|     TimerOnly, | ||||
| } | ||||
|  | ||||
| pub enum PlantError {} | ||||
|  | ||||
| pub struct PlantState { | ||||
|     sensor_a: HumiditySensorState, | ||||
|     sensor_b: HumiditySensorState, | ||||
|     pump: PumpState, | ||||
|     pub sensor_a: HumiditySensorState, | ||||
|     pub sensor_b: HumiditySensorState, | ||||
|     pub pump: PumpState, | ||||
| } | ||||
|  | ||||
| fn map_range_moisture(s: f32) -> Result<f32, HumiditySensorError> { | ||||
|     if s < MOIST_SENSOR_MIN_FREQUENCY { | ||||
|         return Err(HumiditySensorError::OpenCircuit { hz: s, min: FROM.0 }); | ||||
|         return Err(HumiditySensorError::OpenCircuit { | ||||
|             hz: s, | ||||
|             min: MOIST_SENSOR_MIN_FREQUENCY, | ||||
|         }); | ||||
|     } | ||||
|     if s > MOIST_SENSOR_MAX_FREQUENCY { | ||||
|         return Err(HumiditySensorError::ShortCircuit { hz: s, max: FROM.1 }); | ||||
|         return Err(HumiditySensorError::ShortCircuit { | ||||
|             hz: s, | ||||
|             max: MOIST_SENSOR_MAX_FREQUENCY, | ||||
|         }); | ||||
|     } | ||||
|     let moisture_percent = (s - MOIST_SENSOR_MIN_FREQUENCY) * 100 / (MOIST_SENSOR_MAX_FREQUENCY - MOIST_SENSOR_MIN_FREQUENCY); | ||||
|     let moisture_percent = (s - MOIST_SENSOR_MIN_FREQUENCY) * 100 | ||||
|         / (MOIST_SENSOR_MAX_FREQUENCY - MOIST_SENSOR_MIN_FREQUENCY); | ||||
|  | ||||
|     return Ok(moisture_percent); | ||||
| } | ||||
|  | ||||
|  | ||||
| impl PlantState { | ||||
|     pub fn read_hardware_state( | ||||
|         plant_id: usize, | ||||
|         board: &mut plant_hal::PlantCtrlBoard, | ||||
|         config: &config::PlantConfig | ||||
|         config: &config::PlantConfig, | ||||
|     ) -> Self { | ||||
|         let sensor_a = if config.sensor_a { | ||||
|             match board.measure_moisture_hz(plant_id, plant_hal::Sensor::A) { | ||||
|                 Ok(raw) => { | ||||
|                     match map_range_moisture(raw) { | ||||
|                         Ok(moisture_percent) => HumiditySensorState::HumidityValue { raw_hz: raw, moisture_percent }, | ||||
|                         Err(err) => HumiditySensorState::SensorError(err), | ||||
|                     } | ||||
|                 Ok(raw) => match map_range_moisture(raw) { | ||||
|                     Ok(moisture_percent) => HumiditySensorState::HumidityValue { | ||||
|                         raw_hz: raw, | ||||
|                         moisture_percent, | ||||
|                     }, | ||||
|                     Err(err) => HumiditySensorState::SensorError(err), | ||||
|                 }, | ||||
|                 Err(err) => HumiditySensorState::BoardError(err.to_string()), | ||||
|             } | ||||
| @@ -71,11 +113,12 @@ impl PlantState { | ||||
|         }; | ||||
|         let sensor_b = if config.sensor_b { | ||||
|             match board.measure_moisture_hz(plant_id, plant_hal::Sensor::B) { | ||||
|                 Ok(raw) => { | ||||
|                     match map_range_moisture(raw) { | ||||
|                         Ok(moisture_percent) => HumiditySensorState::HumidityValue { raw_hz: raw, moisture_percent }, | ||||
|                         Err(err) => HumiditySensorState::SensorError(err), | ||||
|                     } | ||||
|                 Ok(raw) => match map_range_moisture(raw) { | ||||
|                     Ok(moisture_percent) => HumiditySensorState::HumidityValue { | ||||
|                         raw_hz: raw, | ||||
|                         moisture_percent, | ||||
|                     }, | ||||
|                     Err(err) => HumiditySensorState::SensorError(err), | ||||
|                 }, | ||||
|                 Err(err) => HumiditySensorState::BoardError(err.to_string()), | ||||
|             } | ||||
| @@ -84,45 +127,145 @@ impl PlantState { | ||||
|         }; | ||||
|         let previous_pump = board.last_pump_time(plant_id); | ||||
|         let consecutive_pump_count = board.consecutive_pump_count(plant_id); | ||||
|         Self { | ||||
|         let state = Self { | ||||
|             sensor_a, | ||||
|             sensor_b, | ||||
|             pump: PumpState { consecutive_pump_count , previous_pump} | ||||
|             pump: PumpState { | ||||
|                 consecutive_pump_count, | ||||
|                 previous_pump, | ||||
|             }, | ||||
|         }; | ||||
|         if state.is_err() { | ||||
|             board.fault(plant_id, true); | ||||
|         } | ||||
|         state | ||||
|     } | ||||
|  | ||||
|     pub fn pump_in_timeout(&self, plant_conf: &PlantConfig, current_time: &DateTime<Tz>) -> bool { | ||||
|         self.pump.previous_pump.is_some_and(|last_pump| { | ||||
|             last_pump | ||||
|                 .checked_add_signed(TimeDelta::minutes(plant_conf.pump_cooldown_min.into())) | ||||
|                 .is_some_and(|earliest_next_allowed_pump| { | ||||
|                     earliest_next_allowed_pump > *current_time | ||||
|                 }) | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub fn is_err(&self) -> bool { | ||||
|         self.sensor_a.is_err() || self.sensor_b.is_err() | ||||
|     } | ||||
|  | ||||
|     pub fn needs_to_be_watered( | ||||
|         &self, | ||||
|         plant_conf: &PlantConfig, | ||||
|         current_time: &DateTime<Tz>, | ||||
|     ) -> bool { | ||||
|         match plant_conf.mode { | ||||
|             PlantWateringMode::OFF => false, | ||||
|             PlantWateringMode::TargetMoisture => { | ||||
|                 let moisture_percent = match ( | ||||
|                     self.sensor_a.moisture_percent(), | ||||
|                     &self.sensor_b.moisture_percent(), | ||||
|                 ) { | ||||
|                     (Some(moisture_a), Some(moisture_b)) => (moisture_a + moisture_b) / 2., | ||||
|                     (Some(moisture_percent), _) => moisture_percent, | ||||
|                     (_, Some(moisture_percent)) => moisture_percent, | ||||
|                     _ => { | ||||
|                         // Case for both sensors hitting an error do not water plant in this case | ||||
|                         return false; | ||||
|                     } | ||||
|                 }; | ||||
|                 if self.pump_in_timeout(plant_conf, current_time) { | ||||
|                     false | ||||
|                 } else { | ||||
|                     if moisture_percent < plant_conf.target_moisture { | ||||
|                         true | ||||
|                     } else { | ||||
|                         false | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             PlantWateringMode::TimerOnly => { | ||||
|                 if self.pump_in_timeout(plant_conf, current_time) { | ||||
|                     false | ||||
|                 } else { | ||||
|                     true | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq, Default, Serialize)] | ||||
| fn update_plant_state( | ||||
|     plantstate: &mut [PlantInfo; PLANT_COUNT], | ||||
|     board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, | ||||
|     config: &PlantControllerConfig, | ||||
| ) { | ||||
|     for plant in 0..PLANT_COUNT { | ||||
|         let state = &plantstate[plant]; | ||||
|         let plant_config = &config.plants[plant]; | ||||
|  | ||||
|         let mode = format!("{:?}", plant_config.mode); | ||||
|  | ||||
|         let plant_dto = PlantStateMQTT { | ||||
|             a: &sensor_to_string( | ||||
|                 &state.a, | ||||
|                 &state.sensor_error_a, | ||||
|                 plant_config.mode != PlantWateringMode::OFF, | ||||
|             ), | ||||
|             a_raw: &state.a_raw.unwrap_or(0).to_string(), | ||||
|             b: &sensor_to_string(&state.b, &state.sensor_error_b, plant_config.sensor_b), | ||||
|             b_raw: &state.b_raw.unwrap_or(0).to_string(), | ||||
|             active: state.active, | ||||
|             mode: &mode, | ||||
|             last_pump: &time_to_string_utc(board.last_pump_time(plant)), | ||||
|             next_pump: &time_to_string(state.next_pump), | ||||
|             consecutive_pump_count: state.consecutive_pump_count, | ||||
|             cooldown: state.cooldown, | ||||
|             dry: state.dry, | ||||
|             not_effective: state.not_effective, | ||||
|             out_of_work_hour: state.out_of_work_hour, | ||||
|             pump_error: state.pump_error, | ||||
|         }; | ||||
|  | ||||
|         match serde_json::to_string(&plant_dto) { | ||||
|             Ok(state) => { | ||||
|                 let plant_topic = format!("/plant{}", plant + 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 lightstate {}", err); | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq, Serialize)] | ||||
| /// State of a single plant to be tracked | ||||
| pub struct PlantInfo { | ||||
|     /// state of humidity sensor on bank a | ||||
|     a: HumiditySensorState, | ||||
|     /// raw measured frequency value for sensor on bank a in hertz | ||||
|     a_raw: Option<u32>, | ||||
|     sensor_a: HumiditySensorState, | ||||
|     /// state of humidity sensor on bank b | ||||
|     b: HumiditySensorState, | ||||
|     /// raw measured frequency value for sensor on bank b in hertz | ||||
|     b_raw: Option<u32>, | ||||
|     sensor_b: HumiditySensorState, | ||||
|     /// configured plant watering mode | ||||
|     mode: config::Mode, | ||||
|     /// how often has the logic determined that plant should have been irrigated but wasn't | ||||
|     consecutive_pump_count: u32, | ||||
|     mode: config::PlantWateringMode, | ||||
|     /// plant needs to be watered | ||||
|     do_water: bool, | ||||
|     /// is plant considerd to be dry according to settings | ||||
|     dry: bool, | ||||
|     /// is pump currently running | ||||
|     active: bool, | ||||
|     /// TODO: convert this to an Option<PumpErorr> enum for every case that can happen | ||||
|     pump_error: bool, | ||||
|     /// if pump count has increased higher than configured limit | ||||
|     not_effective: bool, | ||||
|     /// plant irrigation cooldown is active | ||||
|     cooldown: bool, | ||||
|     /// we want to irrigate but tank is empty | ||||
|     no_water: bool, | ||||
|     /// pump should not be watered at this time of day | ||||
|     /// plant should not be watered at this time of day TODO: does this really belong here? Isn't this a global setting? | ||||
|     out_of_work_hour: bool, | ||||
|     /// is pump currently running | ||||
|     active: bool, | ||||
|     /// how often has the logic determined that plant should have been irrigated but wasn't | ||||
|     consecutive_pump_count: u32, | ||||
|     pump_error: Option<PumpError>, | ||||
|     /// last time when pump was active | ||||
|     last_pump: Option<DateTime<Tz>>, | ||||
|     /// next time when pump should activate | ||||
|   | ||||
							
								
								
									
										10
									
								
								rust/src/util.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								rust/src/util.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
|  | ||||
| pub trait LimitPrecision { | ||||
|     fn to_precision(self, presision: i32) -> self; | ||||
| } | ||||
|  | ||||
| impl LimitPrecision for f32 { | ||||
|     fn to_precision(self, precision: i32) -> self { | ||||
|         (self * (10_f32).powi(precision)).round() / (10_f32).powi(precision) | ||||
|     } | ||||
| } | ||||
| @@ -1,8 +1,8 @@ | ||||
| //offer ota and config mode | ||||
|  | ||||
| use crate::{ | ||||
|     determine_tank_state, get_version, log::LogMessage, map_range_moisture, plant_hal::PLANT_COUNT, | ||||
|     BOARD_ACCESS, | ||||
|     determine_tank_state, get_version, log::LogMessage, plant_hal::PLANT_COUNT, | ||||
|     plant_state::PlantState, util::LimitPrecision, BOARD_ACCESS, | ||||
| }; | ||||
| use anyhow::bail; | ||||
| use chrono::DateTime; | ||||
| @@ -35,8 +35,8 @@ struct LoadData<'a> { | ||||
|  | ||||
| #[derive(Serialize, Debug)] | ||||
| struct Moistures { | ||||
|     moisture_a: Vec<u8>, | ||||
|     moisture_b: Vec<u8>, | ||||
|     moisture_a: Vec<Option<f32>>, | ||||
|     moisture_b: Vec<Option<f32>>, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Debug)] | ||||
| @@ -81,33 +81,21 @@ fn get_live_moisture( | ||||
|     _request: &mut Request<&mut EspHttpConnection>, | ||||
| ) -> Result<Option<std::string::String>, anyhow::Error> { | ||||
|     let mut board = BOARD_ACCESS.lock().unwrap(); | ||||
|     let config = board.get_config().unwrap(); | ||||
|  | ||||
|     let mut a: Vec<u8> = Vec::new(); | ||||
|     let mut b: Vec<u8> = Vec::new(); | ||||
|     for plant in 0..8 { | ||||
|         let a_hz = board.measure_moisture_hz(plant, crate::plant_hal::Sensor::A)?; | ||||
|         let b_hz = board.measure_moisture_hz(plant, crate::plant_hal::Sensor::B)?; | ||||
|         let a_pct = map_range_moisture(a_hz as f32); | ||||
|  | ||||
|         match a_pct { | ||||
|             Ok(result) => { | ||||
|                 a.push(result); | ||||
|             } | ||||
|             Err(_) => { | ||||
|                 a.push(200); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         let b_pct = map_range_moisture(b_hz as f32); | ||||
|         match b_pct { | ||||
|             Ok(result) => { | ||||
|                 b.push(result); | ||||
|             } | ||||
|             Err(_) => { | ||||
|                 b.push(200); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     let plant_state = Vec::from_iter( | ||||
|         (0..PLANT_COUNT).map(|i| PlantState::read_hardware_state(i, &mut board, &config.plants[i])), | ||||
|     ); | ||||
|     let a = Vec::from_iter( | ||||
|         plant_state | ||||
|             .iter() | ||||
|             .map(|s| s.sensor_a.moisture_percent().map(|f| f.to_precision(2))), | ||||
|     ); | ||||
|     let b = Vec::from_iter( | ||||
|         plant_state | ||||
|             .iter() | ||||
|             .map(|s| s.sensor_b.moisture_percent().map(|f| f.to_precision(2))), | ||||
|     ); | ||||
|  | ||||
|     let data = Moistures { | ||||
|         moisture_a: a, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user