WIP introduce plant_state module
This commit is contained in:
		| @@ -26,21 +26,14 @@ 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 tank; | mod tank; | ||||||
|  |  | ||||||
|  | use plant_state::{PlantInfo, PlantStateMQTT}; | ||||||
| use tank::*; | use tank::*; | ||||||
|  |  | ||||||
| const TIME_ZONE: Tz = Berlin; | const TIME_ZONE: Tz = Berlin; | ||||||
|  |  | ||||||
| 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 FROM: (f32, f32) = ( |  | ||||||
|     MOIST_SENSOR_MIN_FREQUENCY as f32, |  | ||||||
|     MOIST_SENSOR_MAX_FREQUENCY as f32, |  | ||||||
| ); |  | ||||||
| const TO: (f32, f32) = (0_f32, 100_f32); |  | ||||||
|  |  | ||||||
| pub static BOARD_ACCESS: Lazy<Mutex<PlantCtrlBoard>> = Lazy::new(|| PlantHal::create().unwrap()); | pub static BOARD_ACCESS: Lazy<Mutex<PlantCtrlBoard>> = Lazy::new(|| PlantHal::create().unwrap()); | ||||||
| pub static STAY_ALIVE: Lazy<AtomicBool> = Lazy::new(|| AtomicBool::new(false)); | pub static STAY_ALIVE: Lazy<AtomicBool> = Lazy::new(|| AtomicBool::new(false)); | ||||||
|  |  | ||||||
| @@ -80,46 +73,6 @@ struct LightState { | |||||||
|     is_day: bool, |     is_day: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, PartialEq, Default)] |  | ||||||
| /// State of a single plant to be tracked |  | ||||||
| /// |  | ||||||
| /// TODO can some state be replaced with functions |  | ||||||
| /// TODO unify with PlantStateMQTT |  | ||||||
| struct PlantState { |  | ||||||
|     /// state of humidity sensor on bank a |  | ||||||
|     a: Option<u8>, |  | ||||||
|     /// raw measured frequency value for sensor on bank a in hertz |  | ||||||
|     a_raw: Option<u32>, |  | ||||||
|     /// state of humidity sensor on bank b |  | ||||||
|     b: Option<u8>, |  | ||||||
|     /// raw measured frequency value for sensor on bank b in hertz |  | ||||||
|     b_raw: Option<u32>, |  | ||||||
|     /// how often has the logic determined that plant should have been irrigated but wasn't |  | ||||||
|     consecutive_pump_count: u32, |  | ||||||
|     /// 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, |  | ||||||
|     ///TODO: combine with field a using Result |  | ||||||
|     sensor_error_a: Option<SensorError>, |  | ||||||
|     ///TODO: combine with field b using Result |  | ||||||
|     sensor_error_b: Option<SensorError>, |  | ||||||
|     /// pump should not be watered at this time of day |  | ||||||
|     out_of_work_hour: bool, |  | ||||||
|     /// next time when pump should activate |  | ||||||
|     next_pump: Option<DateTime<Tz>>, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Serialize, Deserialize, Debug, PartialEq)] | #[derive(Serialize, Deserialize, Debug, PartialEq)] | ||||||
| /// humidity sensor error | /// humidity sensor error | ||||||
| enum SensorError { | enum SensorError { | ||||||
| @@ -128,24 +81,6 @@ enum SensorError { | |||||||
|     OpenCircuit { hz: f32, min: f32 }, |     OpenCircuit { hz: f32, min: f32 }, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Serialize)] |  | ||||||
| struct PlantStateMQTT<'a> { |  | ||||||
|     a: &'a str, |  | ||||||
|     a_raw: &'a str, |  | ||||||
|     b: &'a str, |  | ||||||
|     b_raw: &'a str, |  | ||||||
|     mode: &'a str, |  | ||||||
|     consecutive_pump_count: u32, |  | ||||||
|     dry: bool, |  | ||||||
|     active: bool, |  | ||||||
|     pump_error: bool, |  | ||||||
|     not_effective: bool, |  | ||||||
|     cooldown: bool, |  | ||||||
|     out_of_work_hour: bool, |  | ||||||
|     last_pump: &'a str, |  | ||||||
|     next_pump: &'a str, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fn safe_main() -> anyhow::Result<()> { | 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 | ||||||
| @@ -471,7 +406,7 @@ fn safe_main() -> anyhow::Result<()> { | |||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     let mut plantstate: [PlantState; PLANT_COUNT] = core::array::from_fn(|_| PlantState { |     let mut plantstate: [PlantInfo; PLANT_COUNT] = core::array::from_fn(|_| PlantInfo { | ||||||
|         ..Default::default() |         ..Default::default() | ||||||
|     }); |     }); | ||||||
|     determine_plant_state( |     determine_plant_state( | ||||||
| @@ -636,7 +571,7 @@ fn publish_battery_state( | |||||||
| fn determine_state_target_moisture_for_plant( | fn determine_state_target_moisture_for_plant( | ||||||
|     board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, |     board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, | ||||||
|     plant: usize, |     plant: usize, | ||||||
|     state: &mut PlantState, |     state: &mut PlantInfo, | ||||||
|     config: &PlantControllerConfig, |     config: &PlantControllerConfig, | ||||||
|     tank_state: &TankState, |     tank_state: &TankState, | ||||||
|     cur: DateTime<Tz>, |     cur: DateTime<Tz>, | ||||||
| @@ -731,7 +666,7 @@ fn determine_state_target_moisture_for_plant( | |||||||
| fn determine_state_timer_only_for_plant( | fn determine_state_timer_only_for_plant( | ||||||
|     board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, |     board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, | ||||||
|     plant: usize, |     plant: usize, | ||||||
|     state: &mut PlantState, |     state: &mut PlantInfo, | ||||||
|     config: &PlantControllerConfig, |     config: &PlantControllerConfig, | ||||||
|     tank_state: &TankState, |     tank_state: &TankState, | ||||||
|     cur: DateTime<Tz>, |     cur: DateTime<Tz>, | ||||||
| @@ -774,7 +709,7 @@ fn determine_state_timer_only_for_plant( | |||||||
| fn determine_state_timer_and_deadzone_for_plant( | fn determine_state_timer_and_deadzone_for_plant( | ||||||
|     board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, |     board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, | ||||||
|     plant: usize, |     plant: usize, | ||||||
|     state: &mut PlantState, |     state: &mut PlantInfo, | ||||||
|     config: &PlantControllerConfig, |     config: &PlantControllerConfig, | ||||||
|     tank_state: &TankState, |     tank_state: &TankState, | ||||||
|     cur: DateTime<Tz>, |     cur: DateTime<Tz>, | ||||||
| @@ -823,7 +758,7 @@ fn determine_state_timer_and_deadzone_for_plant( | |||||||
| } | } | ||||||
|  |  | ||||||
| fn determine_plant_state( | fn determine_plant_state( | ||||||
|     plantstate: &mut [PlantState; PLANT_COUNT], |     plantstate: &mut [PlantInfo; PLANT_COUNT], | ||||||
|     cur: DateTime<Tz>, |     cur: DateTime<Tz>, | ||||||
|     tank_state: &TankState, |     tank_state: &TankState, | ||||||
|     config: &PlantControllerConfig, |     config: &PlantControllerConfig, | ||||||
| @@ -861,7 +796,7 @@ fn determine_plant_state( | |||||||
| } | } | ||||||
|  |  | ||||||
| fn update_plant_state( | fn update_plant_state( | ||||||
|     plantstate: &mut [PlantState; PLANT_COUNT], |     plantstate: &mut [PlantInfo; PLANT_COUNT], | ||||||
|     board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, |     board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, | ||||||
|     config: &PlantControllerConfig, |     config: &PlantControllerConfig, | ||||||
| ) { | ) { | ||||||
| @@ -1006,18 +941,6 @@ fn to_string<T: Display>(value: Result<T>) -> String { | |||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
| fn map_range_moisture(s: f32) -> Result<u8, SensorError> { |  | ||||||
|     if s < FROM.0 { |  | ||||||
|         return Err(SensorError::OpenCircuit { hz: s, min: FROM.0 }); |  | ||||||
|     } |  | ||||||
|     if s > FROM.1 { |  | ||||||
|         return Err(SensorError::ShortCircuit { hz: s, max: FROM.1 }); |  | ||||||
|     } |  | ||||||
|     let tmp = TO.0 + (s - FROM.0) * (TO.1 - TO.0) / (FROM.1 - FROM.0); |  | ||||||
|  |  | ||||||
|     return Ok(tmp as u8); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fn in_time_range(cur: &DateTime<Tz>, start: u8, end: u8) -> bool { | fn in_time_range(cur: &DateTime<Tz>, start: u8, end: u8) -> bool { | ||||||
|     let curhour = cur.hour() as u8; |     let curhour = cur.hour() as u8; | ||||||
|     //eg 10-14 |     //eg 10-14 | ||||||
|   | |||||||
							
								
								
									
										130
									
								
								rust/src/plant_state.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								rust/src/plant_state.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,130 @@ | |||||||
|  | use chrono::{DateTime, Utc}; | ||||||
|  | use chrono_tz::Tz; | ||||||
|  | use serde::Serialize; | ||||||
|  |  | ||||||
|  | use crate::{config, plant_hal}; | ||||||
|  |  | ||||||
|  | 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 HumiditySensorState { | ||||||
|  |     Disabled, | ||||||
|  |     HumidityValue{raw_hz: u32, moisture_percent: f32}, | ||||||
|  |     SensorError(HumiditySensorError), | ||||||
|  |     BoardError(String) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl HumiditySensorState { | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub enum PumpError {} | ||||||
|  |  | ||||||
|  | pub struct PumpState { | ||||||
|  |     consecutive_pump_count: u32, | ||||||
|  |     previous_pump: Option<DateTime<Utc>> | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub enum PlantError{} | ||||||
|  |  | ||||||
|  | pub struct PlantState { | ||||||
|  |     sensor_a: HumiditySensorState, | ||||||
|  |     sensor_b: HumiditySensorState, | ||||||
|  |     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 }); | ||||||
|  |     } | ||||||
|  |     if s > MOIST_SENSOR_MAX_FREQUENCY { | ||||||
|  |         return Err(HumiditySensorError::ShortCircuit { hz: s, max: FROM.1 }); | ||||||
|  |     } | ||||||
|  |     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 | ||||||
|  |     ) -> 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), | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 Err(err) => HumiditySensorState::BoardError(err.to_string()), | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             HumiditySensorState::Disabled | ||||||
|  |         }; | ||||||
|  |         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), | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 Err(err) => HumiditySensorState::BoardError(err.to_string()), | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             HumiditySensorState::Disabled | ||||||
|  |         }; | ||||||
|  |         let previous_pump = board.last_pump_time(plant_id); | ||||||
|  |         let consecutive_pump_count = board.consecutive_pump_count(plant_id); | ||||||
|  |         Self { | ||||||
|  |             sensor_a, | ||||||
|  |             sensor_b, | ||||||
|  |             pump: PumpState { consecutive_pump_count , previous_pump} | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, PartialEq, Default, 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>, | ||||||
|  |     /// state of humidity sensor on bank b | ||||||
|  |     b: HumiditySensorState, | ||||||
|  |     /// raw measured frequency value for sensor on bank b in hertz | ||||||
|  |     b_raw: Option<u32>, | ||||||
|  |     /// 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, | ||||||
|  |     /// 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 | ||||||
|  |     out_of_work_hour: bool, | ||||||
|  |     /// last time when pump was active | ||||||
|  |     last_pump: Option<DateTime<Tz>>, | ||||||
|  |     /// next time when pump should activate | ||||||
|  |     next_pump: Option<DateTime<Tz>>, | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user