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