From cf31ce8d436e18b4a80e05af225b46e5285f362c Mon Sep 17 00:00:00 2001
From: ju6ge <judge@felixrichter.tech>
Date: Thu, 27 Mar 2025 22:28:41 +0100
Subject: [PATCH] WIP introduce plant_state module

---
 rust/src/main.rs        |  93 +++-------------------------
 rust/src/plant_state.rs | 130 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 138 insertions(+), 85 deletions(-)
 create mode 100644 rust/src/plant_state.rs

diff --git a/rust/src/main.rs b/rust/src/main.rs
index f1d6bbc..0e26c6d 100644
--- a/rust/src/main.rs
+++ b/rust/src/main.rs
@@ -26,21 +26,14 @@ use crate::{config::PlantControllerConfig, webserver::webserver::httpd};
 mod config;
 mod log;
 pub mod plant_hal;
+mod plant_state;
 mod tank;
 
+use plant_state::{PlantInfo, PlantStateMQTT};
 use tank::*;
 
 const TIME_ZONE: Tz = Berlin;
 
-const MOIST_SENSOR_MAX_FREQUENCY: u32 = 6500; // 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 STAY_ALIVE: Lazy<AtomicBool> = Lazy::new(|| AtomicBool::new(false));
 
@@ -80,46 +73,6 @@ struct LightState {
     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)]
 /// humidity sensor error
 enum SensorError {
@@ -128,24 +81,6 @@ enum SensorError {
     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<()> {
     // 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
@@ -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()
     });
     determine_plant_state(
@@ -636,7 +571,7 @@ fn publish_battery_state(
 fn determine_state_target_moisture_for_plant(
     board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
     plant: usize,
-    state: &mut PlantState,
+    state: &mut PlantInfo,
     config: &PlantControllerConfig,
     tank_state: &TankState,
     cur: DateTime<Tz>,
@@ -731,7 +666,7 @@ fn determine_state_target_moisture_for_plant(
 fn determine_state_timer_only_for_plant(
     board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
     plant: usize,
-    state: &mut PlantState,
+    state: &mut PlantInfo,
     config: &PlantControllerConfig,
     tank_state: &TankState,
     cur: DateTime<Tz>,
@@ -774,7 +709,7 @@ fn determine_state_timer_only_for_plant(
 fn determine_state_timer_and_deadzone_for_plant(
     board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
     plant: usize,
-    state: &mut PlantState,
+    state: &mut PlantInfo,
     config: &PlantControllerConfig,
     tank_state: &TankState,
     cur: DateTime<Tz>,
@@ -823,7 +758,7 @@ fn determine_state_timer_and_deadzone_for_plant(
 }
 
 fn determine_plant_state(
-    plantstate: &mut [PlantState; PLANT_COUNT],
+    plantstate: &mut [PlantInfo; PLANT_COUNT],
     cur: DateTime<Tz>,
     tank_state: &TankState,
     config: &PlantControllerConfig,
@@ -861,7 +796,7 @@ fn determine_plant_state(
 }
 
 fn update_plant_state(
-    plantstate: &mut [PlantState; PLANT_COUNT],
+    plantstate: &mut [PlantInfo; PLANT_COUNT],
     board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
     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 {
     let curhour = cur.hour() as u8;
     //eg 10-14
diff --git a/rust/src/plant_state.rs b/rust/src/plant_state.rs
new file mode 100644
index 0000000..69d92b6
--- /dev/null
+++ b/rust/src/plant_state.rs
@@ -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>>,
+}