cleanups
This commit is contained in:
		
							
								
								
									
										328
									
								
								Software/MainBoard/rust/src/plant_state.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										328
									
								
								Software/MainBoard/rust/src/plant_state.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,328 @@
 | 
			
		||||
use crate::hal::Moistures;
 | 
			
		||||
use crate::{
 | 
			
		||||
    config::PlantConfig,
 | 
			
		||||
    hal::HAL,
 | 
			
		||||
    in_time_range,
 | 
			
		||||
};
 | 
			
		||||
use chrono::{DateTime, TimeDelta, Utc};
 | 
			
		||||
use chrono_tz::Tz;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
const MOIST_SENSOR_MAX_FREQUENCY: f32 = 7500.; // 60kHz (500Hz margin)
 | 
			
		||||
const MOIST_SENSOR_MIN_FREQUENCY: f32 = 150.; // this is really, really dry, think like cactus levels
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, PartialEq, Serialize)]
 | 
			
		||||
pub enum MoistureSensorError {
 | 
			
		||||
    ShortCircuit { hz: f32, max: f32 },
 | 
			
		||||
    OpenLoop { hz: f32, min: f32 },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, PartialEq, Serialize)]
 | 
			
		||||
pub enum MoistureSensorState {
 | 
			
		||||
    Disabled,
 | 
			
		||||
    MoistureValue { raw_hz: f32, moisture_percent: f32 },
 | 
			
		||||
    SensorError(MoistureSensorError),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl MoistureSensorState {
 | 
			
		||||
    pub fn is_err(&self) -> Option<&MoistureSensorError> {
 | 
			
		||||
        match self {
 | 
			
		||||
            MoistureSensorState::SensorError(moisture_sensor_error) => Some(moisture_sensor_error),
 | 
			
		||||
            _ => None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn moisture_percent(&self) -> Option<f32> {
 | 
			
		||||
        if let MoistureSensorState::MoistureValue {
 | 
			
		||||
            raw_hz: _,
 | 
			
		||||
            moisture_percent,
 | 
			
		||||
        } = self
 | 
			
		||||
        {
 | 
			
		||||
            Some(*moisture_percent)
 | 
			
		||||
        } else {
 | 
			
		||||
            None
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl MoistureSensorState {}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, PartialEq, Serialize)]
 | 
			
		||||
pub enum PumpError {
 | 
			
		||||
    PumpNotWorking {
 | 
			
		||||
        failed_attempts: usize,
 | 
			
		||||
        max_allowed_failures: usize,
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize)]
 | 
			
		||||
pub struct PumpState {
 | 
			
		||||
    consecutive_pump_count: u32,
 | 
			
		||||
    previous_pump: Option<DateTime<Utc>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PumpState {
 | 
			
		||||
    fn is_err(&self, plant_config: &PlantConfig) -> Option<PumpError> {
 | 
			
		||||
        if self.consecutive_pump_count > plant_config.max_consecutive_pump_count as u32 {
 | 
			
		||||
            Some(PumpError::PumpNotWorking {
 | 
			
		||||
                failed_attempts: self.consecutive_pump_count as usize,
 | 
			
		||||
                max_allowed_failures: plant_config.max_consecutive_pump_count as usize,
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
            None
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)]
 | 
			
		||||
pub enum PlantWateringMode {
 | 
			
		||||
    OFF,
 | 
			
		||||
    TargetMoisture,
 | 
			
		||||
    MinMoisture,
 | 
			
		||||
    TimerOnly,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct PlantState {
 | 
			
		||||
    pub sensor_a: MoistureSensorState,
 | 
			
		||||
    pub sensor_b: MoistureSensorState,
 | 
			
		||||
    pub pump: PumpState,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn map_range_moisture(
 | 
			
		||||
    s: f32,
 | 
			
		||||
    min_frequency: Option<f32>,
 | 
			
		||||
    max_frequency: Option<f32>,
 | 
			
		||||
) -> Result<f32, MoistureSensorError> {
 | 
			
		||||
    // Use overrides if provided, otherwise fallback to defaults
 | 
			
		||||
    let min_freq = min_frequency.unwrap_or(MOIST_SENSOR_MIN_FREQUENCY);
 | 
			
		||||
    let max_freq = max_frequency.unwrap_or(MOIST_SENSOR_MAX_FREQUENCY);
 | 
			
		||||
 | 
			
		||||
    if s < min_freq {
 | 
			
		||||
        return Err(MoistureSensorError::OpenLoop {
 | 
			
		||||
            hz: s,
 | 
			
		||||
            min: min_freq,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    if s > max_freq {
 | 
			
		||||
        return Err(MoistureSensorError::ShortCircuit {
 | 
			
		||||
            hz: s,
 | 
			
		||||
            max: max_freq,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    let moisture_percent = (s - min_freq) * 100.0 / (max_freq - min_freq);
 | 
			
		||||
 | 
			
		||||
    Ok(moisture_percent)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PlantState {
 | 
			
		||||
    pub async fn read_hardware_state(moistures: Moistures, plant_id: usize, board: &mut HAL<'_>) -> Self {
 | 
			
		||||
        let sensor_a = if board.board_hal.get_config().plants[plant_id].sensor_a {
 | 
			
		||||
            let raw = moistures.sensor_a_hz[plant_id];
 | 
			
		||||
            match map_range_moisture(
 | 
			
		||||
                raw,
 | 
			
		||||
                    board.board_hal.get_config().plants[plant_id].moisture_sensor_min_frequency,
 | 
			
		||||
                    board.board_hal.get_config().plants[plant_id].moisture_sensor_max_frequency,
 | 
			
		||||
                ) {
 | 
			
		||||
                    Ok(moisture_percent) => MoistureSensorState::MoistureValue {
 | 
			
		||||
                        raw_hz: raw,
 | 
			
		||||
                        moisture_percent,
 | 
			
		||||
                    },
 | 
			
		||||
                    Err(err) => MoistureSensorState::SensorError(err),
 | 
			
		||||
                }
 | 
			
		||||
        } else {
 | 
			
		||||
            MoistureSensorState::Disabled
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let sensor_b = if board.board_hal.get_config().plants[plant_id].sensor_b {
 | 
			
		||||
            let raw = moistures.sensor_b_hz[plant_id];
 | 
			
		||||
            match map_range_moisture(
 | 
			
		||||
                raw,
 | 
			
		||||
                board.board_hal.get_config().plants[plant_id].moisture_sensor_min_frequency,
 | 
			
		||||
                board.board_hal.get_config().plants[plant_id].moisture_sensor_max_frequency,
 | 
			
		||||
            ) {
 | 
			
		||||
                Ok(moisture_percent) => MoistureSensorState::MoistureValue {
 | 
			
		||||
                    raw_hz: raw,
 | 
			
		||||
                    moisture_percent,
 | 
			
		||||
                },
 | 
			
		||||
                Err(err) => MoistureSensorState::SensorError(err),
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            MoistureSensorState::Disabled
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let previous_pump = board.board_hal.get_esp().last_pump_time(plant_id);
 | 
			
		||||
        let consecutive_pump_count = board.board_hal.get_esp().consecutive_pump_count(plant_id);
 | 
			
		||||
        let state = Self {
 | 
			
		||||
            sensor_a,
 | 
			
		||||
            sensor_b,
 | 
			
		||||
            pump: PumpState {
 | 
			
		||||
                consecutive_pump_count,
 | 
			
		||||
                previous_pump,
 | 
			
		||||
            },
 | 
			
		||||
        };
 | 
			
		||||
        if state.is_err() {
 | 
			
		||||
            let _ = board.board_hal.fault(plant_id, true);
 | 
			
		||||
        }
 | 
			
		||||
        state
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn pump_in_timeout(&self, plant_conf: &PlantConfig, current_time: &DateTime<Tz>) -> bool {
 | 
			
		||||
        if matches!(plant_conf.mode, PlantWateringMode::OFF) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        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().is_some() || self.sensor_b.is_err().is_some()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn plant_moisture(
 | 
			
		||||
        &self,
 | 
			
		||||
    ) -> (
 | 
			
		||||
        Option<f32>,
 | 
			
		||||
        (Option<&MoistureSensorError>, Option<&MoistureSensorError>),
 | 
			
		||||
    ) {
 | 
			
		||||
        match (
 | 
			
		||||
            self.sensor_a.moisture_percent(),
 | 
			
		||||
            self.sensor_b.moisture_percent(),
 | 
			
		||||
        ) {
 | 
			
		||||
            (Some(moisture_a), Some(moisture_b)) => {
 | 
			
		||||
                (Some((moisture_a + moisture_b) / 2.), (None, None))
 | 
			
		||||
            }
 | 
			
		||||
            (Some(moisture_percent), _) => (Some(moisture_percent), (None, self.sensor_b.is_err())),
 | 
			
		||||
            (_, Some(moisture_percent)) => (Some(moisture_percent), (self.sensor_a.is_err(), None)),
 | 
			
		||||
            _ => (None, (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, _) = self.plant_moisture();
 | 
			
		||||
                if let Some(moisture_percent) = moisture_percent {
 | 
			
		||||
                    if self.pump_in_timeout(plant_conf, current_time) {
 | 
			
		||||
                        false
 | 
			
		||||
                    } else if moisture_percent < plant_conf.target_moisture {
 | 
			
		||||
                        in_time_range(
 | 
			
		||||
                            current_time,
 | 
			
		||||
                            plant_conf.pump_hour_start,
 | 
			
		||||
                            plant_conf.pump_hour_end,
 | 
			
		||||
                        )
 | 
			
		||||
                    } else {
 | 
			
		||||
                        false
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    // in case no moisture can be determined, do not water the plant
 | 
			
		||||
                    false
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            PlantWateringMode::MinMoisture => {
 | 
			
		||||
                let (moisture_percent, _) = self.plant_moisture();
 | 
			
		||||
                if let Some(_moisture_percent) = moisture_percent {
 | 
			
		||||
                    if self.pump_in_timeout(plant_conf, current_time) {
 | 
			
		||||
                        false
 | 
			
		||||
                    } else if !in_time_range(
 | 
			
		||||
                        current_time,
 | 
			
		||||
                        plant_conf.pump_hour_start,
 | 
			
		||||
                        plant_conf.pump_hour_end,
 | 
			
		||||
                    ) {
 | 
			
		||||
                        false
 | 
			
		||||
                    } else if true {
 | 
			
		||||
                        //if not cooldown min and below max
 | 
			
		||||
                        true
 | 
			
		||||
                    } else if true {
 | 
			
		||||
                        //if below min disable cooldown min
 | 
			
		||||
                        true
 | 
			
		||||
                    } else {
 | 
			
		||||
                        false
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    false
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            PlantWateringMode::TimerOnly => !self.pump_in_timeout(plant_conf, current_time),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn to_mqtt_info(
 | 
			
		||||
        &self,
 | 
			
		||||
        plant_conf: &PlantConfig,
 | 
			
		||||
        current_time: &DateTime<Tz>,
 | 
			
		||||
    ) -> PlantInfo<'_> {
 | 
			
		||||
        PlantInfo {
 | 
			
		||||
            sensor_a: &self.sensor_a,
 | 
			
		||||
            sensor_b: &self.sensor_b,
 | 
			
		||||
            mode: plant_conf.mode,
 | 
			
		||||
            do_water: self.needs_to_be_watered(plant_conf, current_time),
 | 
			
		||||
            dry: if let Some(moisture_percent) = self.plant_moisture().0 {
 | 
			
		||||
                moisture_percent < plant_conf.target_moisture
 | 
			
		||||
            } else {
 | 
			
		||||
                false
 | 
			
		||||
            },
 | 
			
		||||
            cooldown: self.pump_in_timeout(plant_conf, current_time),
 | 
			
		||||
            out_of_work_hour: in_time_range(
 | 
			
		||||
                current_time,
 | 
			
		||||
                plant_conf.pump_hour_start,
 | 
			
		||||
                plant_conf.pump_hour_end,
 | 
			
		||||
            ),
 | 
			
		||||
            consecutive_pump_count: self.pump.consecutive_pump_count,
 | 
			
		||||
            pump_error: self.pump.is_err(plant_conf),
 | 
			
		||||
            last_pump: self
 | 
			
		||||
                .pump
 | 
			
		||||
                .previous_pump
 | 
			
		||||
                .map(|t| t.with_timezone(¤t_time.timezone())),
 | 
			
		||||
            next_pump: if matches!(
 | 
			
		||||
                plant_conf.mode,
 | 
			
		||||
                PlantWateringMode::TimerOnly
 | 
			
		||||
                    | PlantWateringMode::TargetMoisture
 | 
			
		||||
                    | PlantWateringMode::MinMoisture
 | 
			
		||||
            ) {
 | 
			
		||||
                self.pump.previous_pump.and_then(|last_pump| {
 | 
			
		||||
                    last_pump
 | 
			
		||||
                        .checked_add_signed(TimeDelta::minutes(plant_conf.pump_cooldown_min.into()))
 | 
			
		||||
                        .map(|t| t.with_timezone(¤t_time.timezone()))
 | 
			
		||||
                })
 | 
			
		||||
            } else {
 | 
			
		||||
                None
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, PartialEq, Serialize)]
 | 
			
		||||
/// State of a single plant to be tracked
 | 
			
		||||
pub struct PlantInfo<'a> {
 | 
			
		||||
    /// state of humidity sensor on bank a
 | 
			
		||||
    sensor_a: &'a MoistureSensorState,
 | 
			
		||||
    /// state of humidity sensor on bank b
 | 
			
		||||
    sensor_b: &'a MoistureSensorState,
 | 
			
		||||
    /// configured plant watering mode
 | 
			
		||||
    mode: PlantWateringMode,
 | 
			
		||||
    /// the plant needs to be watered
 | 
			
		||||
    do_water: bool,
 | 
			
		||||
    /// plant is considered to be dry according to settings
 | 
			
		||||
    dry: bool,
 | 
			
		||||
    /// plant irrigation cooldown is active
 | 
			
		||||
    cooldown: bool,
 | 
			
		||||
    /// 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,
 | 
			
		||||
    /// how often has the pump been watered without reaching target moisture
 | 
			
		||||
    consecutive_pump_count: u32,
 | 
			
		||||
    pump_error: Option<PumpError>,
 | 
			
		||||
    /// last time when the 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