refactor/plant-state-handling #11
| @@ -77,7 +77,7 @@ serde = { version = "1.0.192",  features = ["derive"] } | |||||||
| serde_json = "1.0.108" | serde_json = "1.0.108" | ||||||
|  |  | ||||||
| #timezone | #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" ]} | chrono-tz = {version="0.8.0", default-features = false , features = [ "filter-by-regex" ]} | ||||||
| eeprom24x = "0.7.2" | eeprom24x = "0.7.2" | ||||||
| url = "2.5.3" | url = "2.5.3" | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ use std::str::FromStr; | |||||||
|  |  | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
|  |  | ||||||
|  | use crate::plant_state::PlantWateringMode; | ||||||
| use crate::PLANT_COUNT; | use crate::PLANT_COUNT; | ||||||
|  |  | ||||||
| #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] | ||||||
| @@ -83,8 +84,8 @@ pub struct PlantControllerConfig { | |||||||
| #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] | ||||||
| #[serde(default)] | #[serde(default)] | ||||||
| pub struct PlantConfig { | pub struct PlantConfig { | ||||||
|     pub mode: Mode, |     pub mode: PlantWateringMode, | ||||||
|     pub target_moisture: u8, |     pub target_moisture: f32, | ||||||
|     pub pump_time_s: u16, |     pub pump_time_s: u16, | ||||||
|     pub pump_cooldown_min: u16, |     pub pump_cooldown_min: u16, | ||||||
|     pub pump_hour_start: u8, |     pub pump_hour_start: u8, | ||||||
| @@ -96,8 +97,8 @@ pub struct PlantConfig { | |||||||
| impl Default for PlantConfig { | impl Default for PlantConfig { | ||||||
|     fn default() -> Self { |     fn default() -> Self { | ||||||
|         Self { |         Self { | ||||||
|             mode: Mode::OFF, |             mode: PlantWateringMode::OFF, | ||||||
|             target_moisture: 40, |             target_moisture: 40., | ||||||
|             pump_time_s: 30, |             pump_time_s: 30, | ||||||
|             pump_cooldown_min: 60, |             pump_cooldown_min: 60, | ||||||
|             pump_hour_start: 9, |             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::{DateTime, Datelike, TimeDelta, Timelike, Utc}; | ||||||
| use chrono_tz::{Europe::Berlin, Tz}; | use chrono_tz::{Europe::Berlin, Tz}; | ||||||
|  |  | ||||||
| use config::Mode; | use config::PlantWateringMode; | ||||||
| use esp_idf_hal::delay::Delay; | use esp_idf_hal::delay::Delay; | ||||||
| use esp_idf_sys::{ | use esp_idf_sys::{ | ||||||
|     esp_ota_get_app_partition_count, esp_ota_get_running_partition, esp_ota_get_state_partition, |     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; | pub mod plant_hal; | ||||||
| mod plant_state; | mod plant_state; | ||||||
| mod tank; | mod tank; | ||||||
|  | pub mod util; | ||||||
|  |  | ||||||
| use plant_state::{PlantInfo, PlantStateMQTT}; | use plant_state::{PlantInfo, PlantState}; | ||||||
| use tank::*; | use tank::*; | ||||||
|  |  | ||||||
| const TIME_ZONE: Tz = Berlin; | 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 { |     let mut plantstate: [PlantState; PLANT_COUNT] = | ||||||
|         ..Default::default() |         core::array::from_fn(|i| PlantState::read_hardware_state(i, &mut board, &config.plants[i])); | ||||||
|     }); |  | ||||||
|     determine_plant_state( |  | ||||||
|         &mut plantstate, |  | ||||||
|         timezone_time, |  | ||||||
|         &tank_state, |  | ||||||
|         &config, |  | ||||||
|         &mut board, |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     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 { |     if pump_required { | ||||||
|         log(log::LogMessage::EnableMain, dry_run as u32, 0, "", ""); |         log(log::LogMessage::EnableMain, dry_run as u32, 0, "", ""); | ||||||
|         if !dry_run { |         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>) -> ! { | fn wait_infinity(wait_type: WaitType, reboot_now: Arc<AtomicBool>) -> ! { | ||||||
|     let delay = wait_type.blink_pattern(); |     let delay = wait_type.blink_pattern(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,68 +1,110 @@ | |||||||
| use chrono::{DateTime, Utc}; | use chrono::{DateTime, TimeDelta, Utc}; | ||||||
| use chrono_tz::Tz; | 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_MAX_FREQUENCY: u32 = 5500; // 60kHz (500Hz margin) | ||||||
| const MOIST_SENSOR_MIN_FREQUENCY: u32 = 150; // this is really really dry, think like cactus levels | const MOIST_SENSOR_MIN_FREQUENCY: u32 = 150; // this is really really dry, think like cactus levels | ||||||
|  |  | ||||||
| pub enum HumiditySensorError { | pub enum HumiditySensorError { | ||||||
|     ShortCircuit { hz: f32, max: f32 }, |     ShortCircuit { hz: f32, max: f32 }, | ||||||
|     OpenLoop{hz: f32, min: f32} |     OpenLoop { hz: f32, min: f32 }, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, PartialEq)] | ||||||
| pub enum HumiditySensorState { | pub enum HumiditySensorState { | ||||||
|     Disabled, |     Disabled, | ||||||
|     HumidityValue { raw_hz: u32, moisture_percent: f32 }, |     HumidityValue { raw_hz: u32, moisture_percent: f32 }, | ||||||
|     SensorError(HumiditySensorError), |     SensorError(HumiditySensorError), | ||||||
|     BoardError(String) |     BoardError(String), | ||||||
| } | } | ||||||
|  |  | ||||||
| impl HumiditySensorState { | impl HumiditySensorState { | ||||||
|  |     pub fn is_err(&self) -> bool { | ||||||
|  |         matches!(self, Self::SensorError(_)) || matches!(self, Self::BoardError(_)) | ||||||
|     } |     } | ||||||
|  |  | ||||||
| pub enum PumpError {} |     pub fn moisture_percent(&self) -> Option<f32> { | ||||||
|  |         if let HumiditySensorState::HumidityValue { | ||||||
|  |             raw_hz, | ||||||
|  |             moisture_percent, | ||||||
|  |         } = self | ||||||
|  |         { | ||||||
|  |             Some(moisture_percent) | ||||||
|  |         } else { | ||||||
|  |             None | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl HumiditySensorState {} | ||||||
|  |  | ||||||
|  | #[derive(Debug, PartialEq)] | ||||||
|  | pub enum PumpError { | ||||||
|  |     PumpNotWorking { | ||||||
|  |         failed_attempts: usize, | ||||||
|  |         max_allowed_failures: usize, | ||||||
|  |     }, | ||||||
|  | } | ||||||
|  |  | ||||||
| pub struct PumpState { | pub struct PumpState { | ||||||
|     consecutive_pump_count: u32, |     consecutive_pump_count: u32, | ||||||
|     previous_pump: Option<DateTime<Utc>> |     previous_pump: Option<DateTime<Utc>>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] | ||||||
|  | pub enum PlantWateringMode { | ||||||
|  |     OFF, | ||||||
|  |     TargetMoisture, | ||||||
|  |     TimerOnly, | ||||||
| } | } | ||||||
|  |  | ||||||
| pub enum PlantError {} | pub enum PlantError {} | ||||||
|  |  | ||||||
| pub struct PlantState { | pub struct PlantState { | ||||||
|     sensor_a: HumiditySensorState, |     pub sensor_a: HumiditySensorState, | ||||||
|     sensor_b: HumiditySensorState, |     pub sensor_b: HumiditySensorState, | ||||||
|     pump: PumpState, |     pub pump: PumpState, | ||||||
| } | } | ||||||
|  |  | ||||||
| fn map_range_moisture(s: f32) -> Result<f32, HumiditySensorError> { | fn map_range_moisture(s: f32) -> Result<f32, HumiditySensorError> { | ||||||
|     if s < MOIST_SENSOR_MIN_FREQUENCY { |     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 { |     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); |     return Ok(moisture_percent); | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| impl PlantState { | impl PlantState { | ||||||
|     pub fn read_hardware_state( |     pub fn read_hardware_state( | ||||||
|         plant_id: usize, |         plant_id: usize, | ||||||
|         board: &mut plant_hal::PlantCtrlBoard, |         board: &mut plant_hal::PlantCtrlBoard, | ||||||
|         config: &config::PlantConfig |         config: &config::PlantConfig, | ||||||
|     ) -> Self { |     ) -> Self { | ||||||
|         let sensor_a = if config.sensor_a { |         let sensor_a = if config.sensor_a { | ||||||
|             match board.measure_moisture_hz(plant_id, plant_hal::Sensor::A) { |             match board.measure_moisture_hz(plant_id, plant_hal::Sensor::A) { | ||||||
|                 Ok(raw) => { |                 Ok(raw) => match map_range_moisture(raw) { | ||||||
|                     match map_range_moisture(raw) { |                     Ok(moisture_percent) => HumiditySensorState::HumidityValue { | ||||||
|                         Ok(moisture_percent) => HumiditySensorState::HumidityValue { raw_hz: raw, moisture_percent }, |                         raw_hz: raw, | ||||||
|  |                         moisture_percent, | ||||||
|  |                     }, | ||||||
|                     Err(err) => HumiditySensorState::SensorError(err), |                     Err(err) => HumiditySensorState::SensorError(err), | ||||||
|                     } |  | ||||||
|                 }, |                 }, | ||||||
|                 Err(err) => HumiditySensorState::BoardError(err.to_string()), |                 Err(err) => HumiditySensorState::BoardError(err.to_string()), | ||||||
|             } |             } | ||||||
| @@ -71,11 +113,12 @@ impl PlantState { | |||||||
|         }; |         }; | ||||||
|         let sensor_b = if config.sensor_b { |         let sensor_b = if config.sensor_b { | ||||||
|             match board.measure_moisture_hz(plant_id, plant_hal::Sensor::B) { |             match board.measure_moisture_hz(plant_id, plant_hal::Sensor::B) { | ||||||
|                 Ok(raw) => { |                 Ok(raw) => match map_range_moisture(raw) { | ||||||
|                     match map_range_moisture(raw) { |                     Ok(moisture_percent) => HumiditySensorState::HumidityValue { | ||||||
|                         Ok(moisture_percent) => HumiditySensorState::HumidityValue { raw_hz: raw, moisture_percent }, |                         raw_hz: raw, | ||||||
|  |                         moisture_percent, | ||||||
|  |                     }, | ||||||
|                     Err(err) => HumiditySensorState::SensorError(err), |                     Err(err) => HumiditySensorState::SensorError(err), | ||||||
|                     } |  | ||||||
|                 }, |                 }, | ||||||
|                 Err(err) => HumiditySensorState::BoardError(err.to_string()), |                 Err(err) => HumiditySensorState::BoardError(err.to_string()), | ||||||
|             } |             } | ||||||
| @@ -84,45 +127,145 @@ impl PlantState { | |||||||
|         }; |         }; | ||||||
|         let previous_pump = board.last_pump_time(plant_id); |         let previous_pump = board.last_pump_time(plant_id); | ||||||
|         let consecutive_pump_count = board.consecutive_pump_count(plant_id); |         let consecutive_pump_count = board.consecutive_pump_count(plant_id); | ||||||
|         Self { |         let state = Self { | ||||||
|             sensor_a, |             sensor_a, | ||||||
|             sensor_b, |             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 | /// State of a single plant to be tracked | ||||||
| pub struct PlantInfo { | pub struct PlantInfo { | ||||||
|     /// state of humidity sensor on bank a |     /// state of humidity sensor on bank a | ||||||
|     a: HumiditySensorState, |     sensor_a: HumiditySensorState, | ||||||
|     /// raw measured frequency value for sensor on bank a in hertz |  | ||||||
|     a_raw: Option<u32>, |  | ||||||
|     /// state of humidity sensor on bank b |     /// state of humidity sensor on bank b | ||||||
|     b: HumiditySensorState, |     sensor_b: HumiditySensorState, | ||||||
|     /// raw measured frequency value for sensor on bank b in hertz |  | ||||||
|     b_raw: Option<u32>, |  | ||||||
|     /// configured plant watering mode |     /// configured plant watering mode | ||||||
|     mode: config::Mode, |     mode: config::PlantWateringMode, | ||||||
|     /// how often has the logic determined that plant should have been irrigated but wasn't |  | ||||||
|     consecutive_pump_count: u32, |  | ||||||
|     /// plant needs to be watered |     /// plant needs to be watered | ||||||
|     do_water: bool, |     do_water: bool, | ||||||
|     /// is plant considerd to be dry according to settings |     /// is plant considerd to be dry according to settings | ||||||
|     dry: bool, |     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 |     /// plant irrigation cooldown is active | ||||||
|     cooldown: bool, |     cooldown: bool, | ||||||
|     /// we want to irrigate but tank is empty |     /// we want to irrigate but tank is empty | ||||||
|     no_water: bool, |     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, |     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 time when pump was active | ||||||
|     last_pump: Option<DateTime<Tz>>, |     last_pump: Option<DateTime<Tz>>, | ||||||
|     /// next time when pump should activate |     /// 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 | //offer ota and config mode | ||||||
|  |  | ||||||
| use crate::{ | use crate::{ | ||||||
|     determine_tank_state, get_version, log::LogMessage, map_range_moisture, plant_hal::PLANT_COUNT, |     determine_tank_state, get_version, log::LogMessage, plant_hal::PLANT_COUNT, | ||||||
|     BOARD_ACCESS, |     plant_state::PlantState, util::LimitPrecision, BOARD_ACCESS, | ||||||
| }; | }; | ||||||
| use anyhow::bail; | use anyhow::bail; | ||||||
| use chrono::DateTime; | use chrono::DateTime; | ||||||
| @@ -35,8 +35,8 @@ struct LoadData<'a> { | |||||||
|  |  | ||||||
| #[derive(Serialize, Debug)] | #[derive(Serialize, Debug)] | ||||||
| struct Moistures { | struct Moistures { | ||||||
|     moisture_a: Vec<u8>, |     moisture_a: Vec<Option<f32>>, | ||||||
|     moisture_b: Vec<u8>, |     moisture_b: Vec<Option<f32>>, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Deserialize, Debug)] | #[derive(Deserialize, Debug)] | ||||||
| @@ -81,33 +81,21 @@ fn get_live_moisture( | |||||||
|     _request: &mut Request<&mut EspHttpConnection>, |     _request: &mut Request<&mut EspHttpConnection>, | ||||||
| ) -> Result<Option<std::string::String>, anyhow::Error> { | ) -> Result<Option<std::string::String>, anyhow::Error> { | ||||||
|     let mut board = BOARD_ACCESS.lock().unwrap(); |     let mut board = BOARD_ACCESS.lock().unwrap(); | ||||||
|  |     let config = board.get_config().unwrap(); | ||||||
|  |  | ||||||
|     let mut a: Vec<u8> = Vec::new(); |     let plant_state = Vec::from_iter( | ||||||
|     let mut b: Vec<u8> = Vec::new(); |         (0..PLANT_COUNT).map(|i| PlantState::read_hardware_state(i, &mut board, &config.plants[i])), | ||||||
|     for plant in 0..8 { |     ); | ||||||
|         let a_hz = board.measure_moisture_hz(plant, crate::plant_hal::Sensor::A)?; |     let a = Vec::from_iter( | ||||||
|         let b_hz = board.measure_moisture_hz(plant, crate::plant_hal::Sensor::B)?; |         plant_state | ||||||
|         let a_pct = map_range_moisture(a_hz as f32); |             .iter() | ||||||
|  |             .map(|s| s.sensor_a.moisture_percent().map(|f| f.to_precision(2))), | ||||||
|         match a_pct { |     ); | ||||||
|             Ok(result) => { |     let b = Vec::from_iter( | ||||||
|                 a.push(result); |         plant_state | ||||||
|             } |             .iter() | ||||||
|             Err(_) => { |             .map(|s| s.sensor_b.moisture_percent().map(|f| f.to_precision(2))), | ||||||
|                 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 data = Moistures { |     let data = Moistures { | ||||||
|         moisture_a: a, |         moisture_a: a, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user