Compare commits

...

2 Commits

Author SHA1 Message Date
79113530b8
WIP introduce plant_state module 2025-03-27 22:28:41 +01:00
76835b23b1
add config field to enable moisture sensor a 2025-03-27 21:48:42 +01:00
3 changed files with 140 additions and 85 deletions

View File

@ -89,6 +89,7 @@ pub struct PlantConfig {
pub pump_cooldown_min: u16,
pub pump_hour_start: u8,
pub pump_hour_end: u8,
pub sensor_a: bool,
pub sensor_b: bool,
pub max_consecutive_pump_count: u8,
}
@ -101,6 +102,7 @@ impl Default for PlantConfig {
pump_cooldown_min: 60,
pump_hour_start: 9,
pump_hour_end: 20,
sensor_a: true,
sensor_b: false,
max_consecutive_pump_count: 10,
}

View File

@ -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 = 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 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

130
rust/src/plant_state.rs Normal file
View 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>>,
}