Compare commits

..

2 Commits

Author SHA1 Message Date
d2b55fdafb
save compiling state during refactor 2025-04-18 14:01:51 +02:00
4b1d87b66f
WIP refactor plant_state 2025-04-18 01:05:12 +02:00
8 changed files with 305 additions and 445 deletions

View File

@ -77,7 +77,7 @@ serde = { version = "1.0.192", features = ["derive"] }
serde_json = "1.0.108" serde_json = "1.0.108"
#timezone #timezone
chrono = { version = "0.4.23", default-features = false , features = ["iana-time-zone" , "alloc"] } chrono = { version = "0.4.23", default-features = false , features = ["iana-time-zone" , "alloc", "serde"] }
chrono-tz = {version="0.8.0", default-features = false , features = [ "filter-by-regex" ]} chrono-tz = {version="0.8.0", default-features = false , features = [ "filter-by-regex" ]}
eeprom24x = "0.7.2" eeprom24x = "0.7.2"
url = "2.5.3" url = "2.5.3"

View File

@ -3,39 +3,39 @@ use std::process::Command;
use vergen::EmitBuilder; use vergen::EmitBuilder;
fn main() { fn main() {
println!("cargo:rerun-if-changed=./src/src_webpack"); println!("cargo:rerun-if-changed=./src/src_webpack");
Command::new("rm") // Command::new("rm")
.arg("./src/webserver/bundle.js") // .arg("./src/webserver/bundle.js")
.output() // .output()
.unwrap(); // .unwrap();
//
match Command::new("cmd").spawn() { // match Command::new("cmd").spawn() {
Ok(_) => { // Ok(_) => {
println!("Assuming build on windows"); // println!("Assuming build on windows");
let output = Command::new("cmd") // let output = Command::new("cmd")
.arg("/K") // .arg("/K")
.arg("npx") // .arg("npx")
.arg("webpack") // .arg("webpack")
.current_dir("./src_webpack") // .current_dir("./src_webpack")
.output() // .output()
.unwrap(); // .unwrap();
println!("status: {}", output.status); // println!("status: {}", output.status);
println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); // println!("stdout: {}", String::from_utf8_lossy(&output.stdout));
println!("stderr: {}", String::from_utf8_lossy(&output.stderr)); // println!("stderr: {}", String::from_utf8_lossy(&output.stderr));
assert!(output.status.success()); // assert!(output.status.success());
} // }
Err(_) => { // Err(_) => {
println!("Assuming build on linux"); // println!("Assuming build on linux");
let output = Command::new("npx") // let output = Command::new("npx")
.arg("webpack") // .arg("webpack")
.current_dir("./src_webpack") // .current_dir("./src_webpack")
.output() // .output()
.unwrap(); // .unwrap();
println!("status: {}", output.status); // println!("status: {}", output.status);
println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); // println!("stdout: {}", String::from_utf8_lossy(&output.stdout));
println!("stderr: {}", String::from_utf8_lossy(&output.stderr)); // println!("stderr: {}", String::from_utf8_lossy(&output.stderr));
assert!(output.status.success()); // assert!(output.status.success());
} // }
} // }
embuild::espidf::sysenv::output(); embuild::espidf::sysenv::output();
let _ = EmitBuilder::builder().all_git().all_build().emit(); let _ = EmitBuilder::builder().all_git().all_build().emit();

View File

@ -2,6 +2,7 @@ use std::str::FromStr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::plant_state::PlantWateringMode;
use crate::PLANT_COUNT; use crate::PLANT_COUNT;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
@ -83,8 +84,8 @@ pub struct PlantControllerConfig {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(default)] #[serde(default)]
pub struct PlantConfig { pub struct PlantConfig {
pub mode: Mode, pub mode: PlantWateringMode,
pub target_moisture: u8, pub target_moisture: f32,
pub pump_time_s: u16, pub pump_time_s: u16,
pub pump_cooldown_min: u16, pub pump_cooldown_min: u16,
pub pump_hour_start: u8, pub pump_hour_start: u8,
@ -96,8 +97,8 @@ pub struct PlantConfig {
impl Default for PlantConfig { impl Default for PlantConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
mode: Mode::OFF, mode: PlantWateringMode::OFF,
target_moisture: 40, target_moisture: 40.,
pump_time_s: 30, pump_time_s: 30,
pump_cooldown_min: 60, pump_cooldown_min: 60,
pump_hour_start: 9, pump_hour_start: 9,
@ -108,11 +109,3 @@ impl Default for PlantConfig {
} }
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum Mode {
OFF,
TargetMoisture,
TimerOnly,
TimerAndDeadzone,
}

View File

@ -7,7 +7,6 @@ use anyhow::{bail, Result};
use chrono::{DateTime, Datelike, TimeDelta, Timelike, Utc}; use chrono::{DateTime, Datelike, TimeDelta, Timelike, Utc};
use chrono_tz::{Europe::Berlin, Tz}; use chrono_tz::{Europe::Berlin, Tz};
use config::Mode;
use esp_idf_hal::delay::Delay; use esp_idf_hal::delay::Delay;
use esp_idf_sys::{ use esp_idf_sys::{
esp_ota_get_app_partition_count, esp_ota_get_running_partition, esp_ota_get_state_partition, esp_ota_get_app_partition_count, esp_ota_get_running_partition, esp_ota_get_state_partition,
@ -28,8 +27,9 @@ mod log;
pub mod plant_hal; pub mod plant_hal;
mod plant_state; mod plant_state;
mod tank; mod tank;
pub mod util;
use plant_state::{PlantInfo, PlantStateMQTT}; use plant_state::{PlantInfo, PlantState};
use tank::*; use tank::*;
const TIME_ZONE: Tz = Berlin; const TIME_ZONE: Tz = Berlin;
@ -406,61 +406,58 @@ fn safe_main() -> anyhow::Result<()> {
} }
}; };
let mut plantstate: [PlantInfo; PLANT_COUNT] = core::array::from_fn(|_| PlantInfo { let mut plantstate: [PlantState; PLANT_COUNT] =
..Default::default() core::array::from_fn(|i| PlantState::read_hardware_state(i, &mut board, &config.plants[i]));
});
determine_plant_state(
&mut plantstate,
timezone_time,
&tank_state,
&config,
&mut board,
);
let pump_required = plantstate.iter().any(|it| it.do_water) && !water_frozen; let pump_required = plantstate
.iter()
.zip(&config.plants)
.any(|(it, conf)| it.needs_to_be_watered(&conf, &timezone_time))
&& !water_frozen;
if pump_required { if pump_required {
log(log::LogMessage::EnableMain, dry_run as u32, 0, "", ""); log(log::LogMessage::EnableMain, dry_run as u32, 0, "", "");
if !dry_run { if !dry_run {
board.any_pump(true)?; board.any_pump(true)?; // what does this do? Does it need to be reset?
} }
for plant in 0..PLANT_COUNT { for (plant_id, (state, plant_config)) in plantstate.iter().zip(&config.plants).enumerate() {
let state = &mut plantstate[plant]; if state.needs_to_be_watered(&plant_config, &timezone_time) {
if state.do_water { let pump_count = board.consecutive_pump_count(plant_id) + 1;
let plant_config = &config.plants[plant]; board.store_consecutive_pump_count(plant_id, pump_count);
state.consecutive_pump_count = board.consecutive_pump_count(plant) + 1; //TODO(judge) where to put this?
board.store_consecutive_pump_count(plant, state.consecutive_pump_count); //if state.consecutive_pump_count > plant_config.max_consecutive_pump_count as u32 {
if state.consecutive_pump_count > plant_config.max_consecutive_pump_count as u32 { // log(
log( // log::LogMessage::ConsecutivePumpCountLimit,
log::LogMessage::ConsecutivePumpCountLimit, // state.consecutive_pump_count as u32,
state.consecutive_pump_count as u32, // plant_config.max_consecutive_pump_count as u32,
plant_config.max_consecutive_pump_count as u32, // &plant.to_string(),
&plant.to_string(), // "",
"", // );
); // state.not_effective = true;
state.not_effective = true; // board.fault(plant, true);
board.fault(plant, true); //}
}
log( log(
log::LogMessage::PumpPlant, log::LogMessage::PumpPlant,
(plant + 1) as u32, (plant_id + 1) as u32,
plant_config.pump_time_s as u32, plant_config.pump_time_s as u32,
&dry_run.to_string(), &dry_run.to_string(),
"", "",
); );
board.store_last_pump_time(plant, cur); board.store_last_pump_time(plant_id, cur);
board.last_pump_time(plant); board.last_pump_time(plant_id);
state.active = true; //state.active = true;
if !dry_run { if !dry_run {
board.pump(plant, true)?; board.pump(plant_id, true)?;
for _ in 0..plant_config.pump_time_s { Delay::new_default().delay_ms(1000*plant_config.pump_time_s as u32);
Delay::new_default().delay_ms(1000); board.pump(plant_id, false)?;
}
board.pump(plant, false)?;
} }
} else if !state.pump_in_timeout(&plant_config, &timezone_time){
// plant does not need to be watered and is not in timeout
// -> reset consecutive pump count
board.store_consecutive_pump_count(plant_id, 0);
} }
} }
} }
update_plant_state(&mut plantstate, &mut board, &config); //update_plant_state(&mut plantstate, &mut board, &config);
let is_day = board.is_day(); let is_day = board.is_day();
let state_of_charge = board.state_charge_percent().unwrap_or(0); let state_of_charge = board.state_charge_percent().unwrap_or(0);
@ -568,279 +565,6 @@ fn publish_battery_state(
}; };
} }
fn determine_state_target_moisture_for_plant(
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
plant: usize,
state: &mut PlantInfo,
config: &PlantControllerConfig,
tank_state: &TankState,
cur: DateTime<Tz>,
) {
let plant_config = &config.plants[plant];
if plant_config.mode == Mode::OFF {
return;
}
match board.measure_moisture_hz(plant, plant_hal::Sensor::A) {
Ok(a) => {
state.a_raw = Some(a);
let mapped = map_range_moisture(a as f32);
match mapped {
Ok(result) => state.a = Some(result),
Err(err) => {
state.sensor_error_a = Some(err);
}
}
}
Err(_) => {
state.sensor_error_a = Some(SensorError::Unknown);
}
}
if plant_config.sensor_b {
match board.measure_moisture_hz(plant, plant_hal::Sensor::B) {
Ok(b) => {
state.b_raw = Some(b);
let mapped = map_range_moisture(b as f32);
match mapped {
Ok(result) => state.b = Some(result),
Err(err) => {
state.sensor_error_b = Some(err);
}
}
}
Err(_) => {
state.sensor_error_b = Some(SensorError::Unknown);
}
}
}
//FIXME how to average analyze whatever?
let a_low = state.a.is_some() && state.a.unwrap() < plant_config.target_moisture;
let b_low = state.b.is_some() && state.b.unwrap() < plant_config.target_moisture;
if a_low || b_low {
state.dry = true;
match tank_state.enough_water(&config.tank) {
Err(_tank_err) => {
if !config.tank.tank_allow_pumping_if_sensor_error {
state.no_water = true;
}
}
// when no tank error, if plant should be watered depends on if enough water is in tank
// no_water behaves inversly to enough_water
Ok(enough_water) => state.no_water = !enough_water,
}
}
let duration = TimeDelta::try_minutes(plant_config.pump_cooldown_min as i64).unwrap();
let last_pump = board.last_pump_time(plant);
match last_pump {
Some(last_pump) => {
let next_pump = last_pump + duration;
if next_pump > cur {
let local_time = next_pump.with_timezone(&TIME_ZONE);
state.next_pump = Some(local_time);
state.cooldown = true;
}
}
None => {
println!(
"Could not restore last pump for plant {}, restoring",
plant + 1
);
board.store_last_pump_time(plant, DateTime::from_timestamp_millis(0).unwrap());
state.pump_error = true;
}
}
if !in_time_range(
&cur,
plant_config.pump_hour_start,
plant_config.pump_hour_end,
) {
state.out_of_work_hour = true;
}
if state.dry && !state.no_water && !state.cooldown && !state.out_of_work_hour {
state.do_water = true;
}
}
fn determine_state_timer_only_for_plant(
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
plant: usize,
state: &mut PlantInfo,
config: &PlantControllerConfig,
tank_state: &TankState,
cur: DateTime<Tz>,
) {
let plant_config = &config.plants[plant];
let duration = TimeDelta::try_minutes(plant_config.pump_cooldown_min as i64).unwrap();
let last_pump = board.last_pump_time(plant);
match last_pump {
Some(last_pump) => {
let next_pump = last_pump + duration;
if next_pump > cur {
let europe_time = next_pump.with_timezone(&TIME_ZONE);
state.next_pump = Some(europe_time);
state.cooldown = true;
} else {
match tank_state.enough_water(&config.tank) {
Err(_tank_err) => {
if !config.tank.tank_allow_pumping_if_sensor_error {
state.do_water = true;
}
}
Ok(enough_water) => {
state.no_water = !enough_water;
}
}
}
}
None => {
println!(
"Could not restore last pump for plant {}, restoring",
plant + 1
);
board.store_last_pump_time(plant, DateTime::from_timestamp_millis(0).unwrap());
state.pump_error = true;
}
}
}
fn determine_state_timer_and_deadzone_for_plant(
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
plant: usize,
state: &mut PlantInfo,
config: &PlantControllerConfig,
tank_state: &TankState,
cur: DateTime<Tz>,
) {
let plant_config = &config.plants[plant];
let duration = TimeDelta::try_minutes(plant_config.pump_cooldown_min as i64).unwrap();
let last_pump = board.last_pump_time(plant);
match last_pump {
Some(last_pump) => {
let next_pump = last_pump + duration;
if next_pump > cur {
let europe_time = next_pump.with_timezone(&TIME_ZONE);
state.next_pump = Some(europe_time);
state.cooldown = true;
}
if !in_time_range(
&cur,
plant_config.pump_hour_start,
plant_config.pump_hour_end,
) {
state.out_of_work_hour = true;
}
if !state.cooldown && !state.out_of_work_hour {
match tank_state.enough_water(&config.tank) {
Err(_tank_err) => {
if !config.tank.tank_allow_pumping_if_sensor_error {
state.do_water = true;
}
}
Ok(enough_water) => {
state.no_water = !enough_water;
}
}
}
}
None => {
println!(
"Could not restore last pump for plant {}, restoring",
plant + 1
);
board.store_last_pump_time(plant, DateTime::from_timestamp_millis(0).unwrap());
state.pump_error = true;
}
}
}
fn determine_plant_state(
plantstate: &mut [PlantInfo; PLANT_COUNT],
cur: DateTime<Tz>,
tank_state: &TankState,
config: &PlantControllerConfig,
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
) {
for plant in 0..PLANT_COUNT {
let state = &mut plantstate[plant];
let plant_config = &config.plants[plant];
match plant_config.mode {
config::Mode::OFF => {}
config::Mode::TargetMoisture => {
determine_state_target_moisture_for_plant(
board, plant, state, config, tank_state, cur,
);
}
config::Mode::TimerOnly => {
determine_state_timer_only_for_plant(board, plant, state, config, tank_state, cur);
}
config::Mode::TimerAndDeadzone => {
determine_state_timer_and_deadzone_for_plant(
board, plant, state, config, tank_state, cur,
);
}
}
if state.sensor_error_a.is_some() || state.sensor_error_b.is_some() {
board.fault(plant, true);
}
if !state.dry {
state.consecutive_pump_count = 0;
board.store_consecutive_pump_count(plant, 0);
}
println!("Plant {} state is {:?}", plant, state);
}
}
fn update_plant_state(
plantstate: &mut [PlantInfo; PLANT_COUNT],
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
config: &PlantControllerConfig,
) {
for plant in 0..PLANT_COUNT {
let state = &plantstate[plant];
let plant_config = &config.plants[plant];
let mode = format!("{:?}", plant_config.mode);
let plant_dto = PlantStateMQTT {
a: &sensor_to_string(
&state.a,
&state.sensor_error_a,
plant_config.mode != Mode::OFF,
),
a_raw: &state.a_raw.unwrap_or(0).to_string(),
b: &sensor_to_string(&state.b, &state.sensor_error_b, plant_config.sensor_b),
b_raw: &state.b_raw.unwrap_or(0).to_string(),
active: state.active,
mode: &mode,
last_pump: &time_to_string_utc(board.last_pump_time(plant)),
next_pump: &time_to_string(state.next_pump),
consecutive_pump_count: state.consecutive_pump_count,
cooldown: state.cooldown,
dry: state.dry,
not_effective: state.not_effective,
out_of_work_hour: state.out_of_work_hour,
pump_error: state.pump_error,
};
match serde_json::to_string(&plant_dto) {
Ok(state) => {
let plant_topic = format!("/plant{}", plant + 1);
let _ = board.mqtt_publish(&config, &plant_topic, state.as_bytes());
//reduce speed as else messages will be dropped
Delay::new_default().delay_ms(200);
}
Err(err) => {
println!("Error publishing lightstate {}", err);
}
};
}
}
fn wait_infinity(wait_type: WaitType, reboot_now: Arc<AtomicBool>) -> ! { fn wait_infinity(wait_type: WaitType, reboot_now: Arc<AtomicBool>) -> ! {
let delay = wait_type.blink_pattern(); let delay = wait_type.blink_pattern();

View File

@ -603,7 +603,7 @@ impl PlantCtrlBoard<'_> {
self.time() self.time()
} }
pub fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<u32> { pub fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<f32> {
let sensor_channel = match sensor { let sensor_channel = match sensor {
Sensor::A => match plant { Sensor::A => match plant {
0 => SENSOR_A_1, 0 => SENSOR_A_1,
@ -629,7 +629,7 @@ impl PlantCtrlBoard<'_> {
}, },
}; };
let mut results = [0_u32; REPEAT_MOIST_MEASURE]; let mut results = [0_f32; REPEAT_MOIST_MEASURE];
for repeat in 0..REPEAT_MOIST_MEASURE { for repeat in 0..REPEAT_MOIST_MEASURE {
self.signal_counter.counter_pause()?; self.signal_counter.counter_pause()?;
self.signal_counter.counter_clear()?; self.signal_counter.counter_clear()?;
@ -644,7 +644,7 @@ impl PlantCtrlBoard<'_> {
.unwrap(); .unwrap();
let delay = Delay::new_default(); let delay = Delay::new_default();
let measurement = 100; let measurement = 100; // TODO what is this scaling factor? what is its purpose?
let factor = 1000 as f32 / measurement as f32; let factor = 1000 as f32 / measurement as f32;
//give some time to stabilize //give some time to stabilize
@ -658,7 +658,7 @@ impl PlantCtrlBoard<'_> {
.unwrap(); .unwrap();
delay.delay_ms(10); delay.delay_ms(10);
let unscaled = self.signal_counter.get_counter_value()? as i32; let unscaled = self.signal_counter.get_counter_value()? as i32;
let hz = (unscaled as f32 * factor) as u32; let hz = unscaled as f32 * factor;
log( log(
LogMessage::RawMeasure, LogMessage::RawMeasure,
unscaled as u32, unscaled as u32,
@ -668,7 +668,7 @@ impl PlantCtrlBoard<'_> {
); );
results[repeat] = hz; results[repeat] = hz;
} }
results.sort(); results.sort_by(|a,b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord
let mid = results.len() / 2; let mid = results.len() / 2;

View File

@ -1,68 +1,112 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, TimeDelta, Utc};
use chrono_tz::Tz; use chrono_tz::Tz;
use serde::Serialize; use measurements::humidity;
use serde::{Deserialize, Serialize};
use crate::{config, plant_hal}; use crate::{
config::{self, PlantConfig},
plant_hal::{self, PLANT_COUNT},
};
const MOIST_SENSOR_MAX_FREQUENCY: u32 = 5500; // 60kHz (500Hz margin) const MOIST_SENSOR_MAX_FREQUENCY: f32 = 5500.; // 60kHz (500Hz margin)
const MOIST_SENSOR_MIN_FREQUENCY: u32 = 150; // this is really really dry, think like cactus levels const MOIST_SENSOR_MIN_FREQUENCY: f32 = 150.; // this is really really dry, think like cactus levels
pub enum HumiditySensorError{ #[derive(Debug, PartialEq, Serialize)]
ShortCircuit{hz: f32, max: f32}, pub enum HumiditySensorError {
OpenLoop{hz: f32, min: f32} ShortCircuit { hz: f32, max: f32 },
OpenLoop { hz: f32, min: f32 },
} }
#[derive(Debug, PartialEq, Serialize)]
pub enum HumiditySensorState { pub enum HumiditySensorState {
Disabled, Disabled,
HumidityValue{raw_hz: u32, moisture_percent: f32}, HumidityValue { raw_hz: f32, moisture_percent: f32 },
SensorError(HumiditySensorError), SensorError(HumiditySensorError),
BoardError(String) BoardError(String),
} }
impl HumiditySensorState { impl HumiditySensorState {
pub fn is_err(&self) -> bool {
matches!(self, Self::SensorError(_)) || matches!(self, Self::BoardError(_))
}
pub fn moisture_percent(&self) -> Option<f32> {
if let HumiditySensorState::HumidityValue {
raw_hz,
moisture_percent,
} = self
{
Some(*moisture_percent)
} else {
None
}
}
} }
pub enum PumpError {} impl HumiditySensorState {}
#[derive(Debug, PartialEq, Serialize)]
pub enum PumpError {
PumpNotWorking {
failed_attempts: usize,
max_allowed_failures: usize,
},
}
#[derive(Debug, Serialize)]
pub struct PumpState { pub struct PumpState {
consecutive_pump_count: u32, consecutive_pump_count: u32,
previous_pump: Option<DateTime<Utc>> previous_pump: Option<DateTime<Utc>>,
} }
pub enum PlantError{} #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum PlantWateringMode {
OFF,
TargetMoisture,
TimerOnly,
}
pub enum PlantError {}
pub struct PlantState { pub struct PlantState {
sensor_a: HumiditySensorState, pub sensor_a: HumiditySensorState,
sensor_b: HumiditySensorState, pub sensor_b: HumiditySensorState,
pump: PumpState, pub pump: PumpState,
} }
fn map_range_moisture(s: f32) -> Result<f32, HumiditySensorError> { fn map_range_moisture(s: f32) -> Result<f32, HumiditySensorError> {
if s < MOIST_SENSOR_MIN_FREQUENCY { if s < MOIST_SENSOR_MIN_FREQUENCY {
return Err(HumiditySensorError::OpenCircuit { hz: s, min: FROM.0 }); return Err(HumiditySensorError::OpenLoop {
hz: s,
min: MOIST_SENSOR_MIN_FREQUENCY,
});
} }
if s > MOIST_SENSOR_MAX_FREQUENCY { if s > MOIST_SENSOR_MAX_FREQUENCY {
return Err(HumiditySensorError::ShortCircuit { hz: s, max: FROM.1 }); return Err(HumiditySensorError::ShortCircuit {
hz: s,
max: MOIST_SENSOR_MAX_FREQUENCY,
});
} }
let moisture_percent = (s - MOIST_SENSOR_MIN_FREQUENCY) * 100 / (MOIST_SENSOR_MAX_FREQUENCY - MOIST_SENSOR_MIN_FREQUENCY); let moisture_percent = (s - MOIST_SENSOR_MIN_FREQUENCY) * 100.0
/ (MOIST_SENSOR_MAX_FREQUENCY - MOIST_SENSOR_MIN_FREQUENCY);
return Ok(moisture_percent); return Ok(moisture_percent);
} }
impl PlantState { impl PlantState {
pub fn read_hardware_state( pub fn read_hardware_state(
plant_id: usize, plant_id: usize,
board: &mut plant_hal::PlantCtrlBoard, board: &mut plant_hal::PlantCtrlBoard,
config: &config::PlantConfig config: &config::PlantConfig,
) -> Self { ) -> Self {
let sensor_a = if config.sensor_a { let sensor_a = if config.sensor_a {
match board.measure_moisture_hz(plant_id, plant_hal::Sensor::A) { match board.measure_moisture_hz(plant_id, plant_hal::Sensor::A) {
Ok(raw) => { Ok(raw) => match map_range_moisture(raw) {
match map_range_moisture(raw) { Ok(moisture_percent) => HumiditySensorState::HumidityValue {
Ok(moisture_percent) => HumiditySensorState::HumidityValue { raw_hz: raw, moisture_percent }, raw_hz: raw,
Err(err) => HumiditySensorState::SensorError(err), moisture_percent,
} },
Err(err) => HumiditySensorState::SensorError(err),
}, },
Err(err) => HumiditySensorState::BoardError(err.to_string()), Err(err) => HumiditySensorState::BoardError(err.to_string()),
} }
@ -71,11 +115,12 @@ impl PlantState {
}; };
let sensor_b = if config.sensor_b { let sensor_b = if config.sensor_b {
match board.measure_moisture_hz(plant_id, plant_hal::Sensor::B) { match board.measure_moisture_hz(plant_id, plant_hal::Sensor::B) {
Ok(raw) => { Ok(raw) => match map_range_moisture(raw) {
match map_range_moisture(raw) { Ok(moisture_percent) => HumiditySensorState::HumidityValue {
Ok(moisture_percent) => HumiditySensorState::HumidityValue { raw_hz: raw, moisture_percent }, raw_hz: raw,
Err(err) => HumiditySensorState::SensorError(err), moisture_percent,
} },
Err(err) => HumiditySensorState::SensorError(err),
}, },
Err(err) => HumiditySensorState::BoardError(err.to_string()), Err(err) => HumiditySensorState::BoardError(err.to_string()),
} }
@ -84,45 +129,145 @@ impl PlantState {
}; };
let previous_pump = board.last_pump_time(plant_id); let previous_pump = board.last_pump_time(plant_id);
let consecutive_pump_count = board.consecutive_pump_count(plant_id); let consecutive_pump_count = board.consecutive_pump_count(plant_id);
Self { let state = Self {
sensor_a, sensor_a,
sensor_b, sensor_b,
pump: PumpState { consecutive_pump_count , previous_pump} pump: PumpState {
consecutive_pump_count,
previous_pump,
},
};
if state.is_err() {
board.fault(plant_id, true);
}
state
}
pub fn pump_in_timeout(&self, plant_conf: &PlantConfig, current_time: &DateTime<Tz>) -> bool {
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() || 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 = match (
self.sensor_a.moisture_percent(),
self.sensor_b.moisture_percent(),
) {
(Some(moisture_a), Some(moisture_b)) => (moisture_a + moisture_b) / 2.,
(Some(moisture_percent), _) => moisture_percent,
(_, Some(moisture_percent)) => moisture_percent,
_ => {
// Case for both sensors hitting an error do not water plant in this case
return false;
}
};
if self.pump_in_timeout(plant_conf, current_time) {
false
} else {
if moisture_percent < plant_conf.target_moisture {
true
} else {
false
}
}
}
PlantWateringMode::TimerOnly => {
if self.pump_in_timeout(plant_conf, current_time) {
false
} else {
true
}
}
} }
} }
} }
#[derive(Debug, PartialEq, Default, Serialize)] //fn update_plant_state(
// plantstate: &mut [PlantInfo; PLANT_COUNT],
// board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
// config: &PlantControllerConfig,
//) {
// for plant in 0..PLANT_COUNT {
// let state = &plantstate[plant];
// let plant_config = &config.plants[plant];
//
// let mode = format!("{:?}", plant_config.mode);
//
// let plant_dto = PlantStateMQTT {
// a: &sensor_to_string(
// &state.a,
// &state.sensor_error_a,
// plant_config.mode != PlantWateringMode::OFF,
// ),
// a_raw: &state.a_raw.unwrap_or(0).to_string(),
// b: &sensor_to_string(&state.b, &state.sensor_error_b, plant_config.sensor_b),
// b_raw: &state.b_raw.unwrap_or(0).to_string(),
// active: state.active,
// mode: &mode,
// last_pump: &time_to_string_utc(board.last_pump_time(plant)),
// next_pump: &time_to_string(state.next_pump),
// consecutive_pump_count: state.consecutive_pump_count,
// cooldown: state.cooldown,
// dry: state.dry,
// not_effective: state.not_effective,
// out_of_work_hour: state.out_of_work_hour,
// pump_error: state.pump_error,
// };
//
// match serde_json::to_string(&plant_dto) {
// Ok(state) => {
// let plant_topic = format!("/plant{}", plant + 1);
// let _ = board.mqtt_publish(&config, &plant_topic, state.as_bytes());
// //reduce speed as else messages will be dropped
// Delay::new_default().delay_ms(200);
// }
// Err(err) => {
// println!("Error publishing lightstate {}", err);
// }
// };
// }
//}
#[derive(Debug, PartialEq, Serialize)]
/// State of a single plant to be tracked /// State of a single plant to be tracked
pub struct PlantInfo { pub struct PlantInfo {
/// state of humidity sensor on bank a /// state of humidity sensor on bank a
a: HumiditySensorState, sensor_a: HumiditySensorState,
/// raw measured frequency value for sensor on bank a in hertz
a_raw: Option<u32>,
/// state of humidity sensor on bank b /// state of humidity sensor on bank b
b: HumiditySensorState, sensor_b: HumiditySensorState,
/// raw measured frequency value for sensor on bank b in hertz
b_raw: Option<u32>,
/// configured plant watering mode /// configured plant watering mode
mode: config::Mode, mode: PlantWateringMode,
/// how often has the logic determined that plant should have been irrigated but wasn't
consecutive_pump_count: u32,
/// plant needs to be watered /// plant needs to be watered
do_water: bool, do_water: bool,
/// is plant considerd to be dry according to settings /// is plant considerd to be dry according to settings
dry: bool, 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 /// plant irrigation cooldown is active
cooldown: bool, cooldown: bool,
/// we want to irrigate but tank is empty /// we want to irrigate but tank is empty
no_water: bool, no_water: bool,
/// pump should not be watered at this time of day /// 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, out_of_work_hour: bool,
/// is pump currently running
active: bool,
/// how often has the logic determined that plant should have been irrigated but wasn't
consecutive_pump_count: u32,
pump_error: Option<PumpError>,
/// last time when pump was active /// last time when pump was active
last_pump: Option<DateTime<Tz>>, last_pump: Option<DateTime<Tz>>,
/// next time when pump should activate /// next time when pump should activate

10
rust/src/util.rs Normal file
View File

@ -0,0 +1,10 @@
pub trait LimitPrecision {
fn to_precision(self, presision: i32) -> Self;
}
impl LimitPrecision for f32 {
fn to_precision(self, precision: i32) -> Self {
(self * (10_f32).powi(precision)).round() / (10_f32).powi(precision)
}
}

View File

@ -1,8 +1,8 @@
//offer ota and config mode //offer ota and config mode
use crate::{ use crate::{
determine_tank_state, get_version, log::LogMessage, map_range_moisture, plant_hal::PLANT_COUNT, determine_tank_state, get_version, log::LogMessage, plant_hal::PLANT_COUNT,
BOARD_ACCESS, plant_state::PlantState, util::LimitPrecision, BOARD_ACCESS,
}; };
use anyhow::bail; use anyhow::bail;
use chrono::DateTime; use chrono::DateTime;
@ -35,8 +35,8 @@ struct LoadData<'a> {
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
struct Moistures { struct Moistures {
moisture_a: Vec<u8>, moisture_a: Vec<Option<f32>>,
moisture_b: Vec<u8>, moisture_b: Vec<Option<f32>>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -81,33 +81,21 @@ fn get_live_moisture(
_request: &mut Request<&mut EspHttpConnection>, _request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> { ) -> Result<Option<std::string::String>, anyhow::Error> {
let mut board = BOARD_ACCESS.lock().unwrap(); let mut board = BOARD_ACCESS.lock().unwrap();
let config = board.get_config().unwrap();
let mut a: Vec<u8> = Vec::new(); let plant_state = Vec::from_iter(
let mut b: Vec<u8> = Vec::new(); (0..PLANT_COUNT).map(|i| PlantState::read_hardware_state(i, &mut board, &config.plants[i])),
for plant in 0..8 { );
let a_hz = board.measure_moisture_hz(plant, crate::plant_hal::Sensor::A)?; let a = Vec::from_iter(
let b_hz = board.measure_moisture_hz(plant, crate::plant_hal::Sensor::B)?; plant_state
let a_pct = map_range_moisture(a_hz as f32); .iter()
.map(|s| s.sensor_a.moisture_percent().map(|f| f.to_precision(2))),
match a_pct { );
Ok(result) => { let b = Vec::from_iter(
a.push(result); plant_state
} .iter()
Err(_) => { .map(|s| s.sensor_b.moisture_percent().map(|f| f.to_precision(2))),
a.push(200); );
}
}
let b_pct = map_range_moisture(b_hz as f32);
match b_pct {
Ok(result) => {
b.push(result);
}
Err(_) => {
b.push(200);
}
}
}
let data = Moistures { let data = Moistures {
moisture_a: a, moisture_a: a,