Compare commits
7 Commits
ead011ec0b
...
fbf1a84e7d
Author | SHA1 | Date | |
---|---|---|---|
fbf1a84e7d | |||
4a334ef2f2 | |||
b993f2b037 | |||
48b0777d94 | |||
e878a774ff | |||
fb180630e4 | |||
420f4f4ae5 |
@ -1,6 +1,6 @@
|
||||
use serde::Serialize;
|
||||
use std::{collections::HashMap, sync::Mutex};
|
||||
use strum::{EnumIter, IntoEnumIterator};
|
||||
use strum::EnumIter;
|
||||
use strum_macros::IntoStaticStr;
|
||||
|
||||
use esp_idf_svc::systime::EspSystemTime;
|
||||
|
224
rust/src/main.rs
224
rust/src/main.rs
@ -26,6 +26,9 @@ use crate::{config::PlantControllerConfig, webserver::webserver::httpd};
|
||||
mod config;
|
||||
mod log;
|
||||
pub mod plant_hal;
|
||||
mod tank;
|
||||
|
||||
use tank::*;
|
||||
|
||||
const TIME_ZONE: Tz = Berlin;
|
||||
|
||||
@ -125,34 +128,6 @@ enum SensorError {
|
||||
OpenCircuit { hz: f32, min: f32 },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Default)]
|
||||
/// State data for water tank
|
||||
///
|
||||
/// TODO unify with TankStateMQTT
|
||||
struct TankState {
|
||||
/// is there enough water in the tank
|
||||
enough_water: bool,
|
||||
/// warning that water needs to be refilled soon
|
||||
warn_level: bool,
|
||||
/// estimation how many ml are still in tank
|
||||
left_ml: u32,
|
||||
/// if there is was an issue with the water level sensor
|
||||
/// TODO merge with left_ml as Result<u32, error_type>
|
||||
sensor_error: bool,
|
||||
/// raw water sensor value
|
||||
raw: u16,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct TankStateMQTT {
|
||||
enough_water: bool,
|
||||
warn_level: bool,
|
||||
left_ml: u32,
|
||||
sensor_error: bool,
|
||||
raw: u16,
|
||||
water_frozen: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PlantStateMQTT<'a> {
|
||||
a: &'a str,
|
||||
@ -426,41 +401,63 @@ fn safe_main() -> anyhow::Result<()> {
|
||||
let dry_run = false;
|
||||
|
||||
let tank_state = determine_tank_state(&mut board, &config);
|
||||
let mut tank_state_mqtt = TankStateMQTT {
|
||||
enough_water: tank_state.enough_water,
|
||||
left_ml: tank_state.left_ml,
|
||||
warn_level: tank_state.warn_level,
|
||||
sensor_error: tank_state.sensor_error,
|
||||
raw: tank_state.raw,
|
||||
water_frozen: "".to_owned(),
|
||||
};
|
||||
if let Some(_err) = tank_state.got_error(&config.tank) {
|
||||
//TODO log error state to serial
|
||||
//log(
|
||||
//LogMessage::SensorTankRaw,
|
||||
//raw_value_mv as u32,
|
||||
//percent as u32,
|
||||
//"",
|
||||
//"",
|
||||
//);
|
||||
} else if tank_state.warn_level(&config.tank).is_ok_and(|warn| warn) {
|
||||
//TODO(judge) enable logging again, might require returning level as well
|
||||
//println!(
|
||||
//"Low water, current percent is {}, minimum warn level is {}",
|
||||
//percent as u8, config.tank.tank_warn_percent
|
||||
//);
|
||||
board.general_fault(true);
|
||||
} else if tank_state.enough_water(&config.tank).is_ok_and(|enough| enough) {
|
||||
//TODO(judge) enable logging again, might require returning level as well
|
||||
//println!(
|
||||
//"Enough water, current percent is {}, minimum empty level is {}",
|
||||
//percent as u8, config.tank.tank_empty_percent
|
||||
//);
|
||||
} else {
|
||||
if let Ok(_left_ml) = tank_state.left_ml(&config.tank) {
|
||||
//TODO(judge) enable logging again, might require returning level as well
|
||||
//println!(
|
||||
//"Tank sensor returned mv {} as {}% leaving {} ml useable",
|
||||
//rv.raw, percent as u8, rv.left_ml
|
||||
//);
|
||||
}
|
||||
}
|
||||
let mut water_frozen = false;
|
||||
|
||||
let mut temp: Option<f32> = None;
|
||||
for _attempt in 0..5 {
|
||||
let water_temperature = board.water_temperature_c();
|
||||
match water_temperature {
|
||||
let mut attempt = 1;
|
||||
let water_temp: Result<f32, anyhow::Error> = loop {
|
||||
let temp = board.water_temperature_c();
|
||||
match &temp {
|
||||
Ok(res) => {
|
||||
temp = Some(res);
|
||||
break;
|
||||
println!("Water temp is {}", res);
|
||||
break temp
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Could not get water temp {} attempt {}", err, _attempt)
|
||||
println!("Could not get water temp {} attempt {}", err, attempt)
|
||||
}
|
||||
}
|
||||
}
|
||||
match temp {
|
||||
Some(res) => {
|
||||
println!("Water temp is {}", res);
|
||||
if res < 4_f32 {
|
||||
water_frozen = true;
|
||||
}
|
||||
tank_state_mqtt.water_frozen = water_frozen.to_string();
|
||||
if attempt == 5 {
|
||||
break temp
|
||||
}
|
||||
attempt += 1;
|
||||
};
|
||||
if let Ok(res) = water_temp {
|
||||
if res < WATER_FROZEN_THRESH {
|
||||
water_frozen = true;
|
||||
}
|
||||
None => tank_state_mqtt.water_frozen = "tank sensor error".to_owned(),
|
||||
}
|
||||
|
||||
match serde_json::to_string(&tank_state_mqtt) {
|
||||
match serde_json::to_string(&tank_state.as_mqtt_info(&config.tank, water_temp)) {
|
||||
Ok(state) => {
|
||||
let _ = board.mqtt_publish(&config, "/water", state.as_bytes());
|
||||
}
|
||||
@ -631,68 +628,6 @@ fn publish_battery_state(
|
||||
};
|
||||
}
|
||||
|
||||
fn determine_tank_state(
|
||||
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
|
||||
config: &PlantControllerConfig,
|
||||
) -> TankState {
|
||||
if config.tank.tank_sensor_enabled {
|
||||
let mut rv: TankState = TankState {
|
||||
..Default::default()
|
||||
};
|
||||
let success = board
|
||||
.tank_sensor_percent()
|
||||
.and_then(|raw| {
|
||||
rv.raw = raw;
|
||||
return map_range(
|
||||
(
|
||||
config.tank.tank_empty_percent as f32,
|
||||
config.tank.tank_full_percent as f32,
|
||||
),
|
||||
raw as f32,
|
||||
);
|
||||
})
|
||||
.and_then(|percent| {
|
||||
rv.left_ml = ((percent * config.tank.tank_useable_ml as f32) / 100_f32) as u32;
|
||||
println!(
|
||||
"Tank sensor returned mv {} as {}% leaving {} ml useable",
|
||||
rv.raw, percent as u8, rv.left_ml
|
||||
);
|
||||
if config.tank.tank_warn_percent > percent as u8 {
|
||||
board.general_fault(true);
|
||||
println!(
|
||||
"Low water, current percent is {}, minimum warn level is {}",
|
||||
percent as u8, config.tank.tank_warn_percent
|
||||
);
|
||||
rv.warn_level = true;
|
||||
}
|
||||
if config.tank.tank_empty_percent < percent as u8 {
|
||||
println!(
|
||||
"Enough water, current percent is {}, minimum empty level is {}",
|
||||
percent as u8, config.tank.tank_empty_percent
|
||||
);
|
||||
rv.enough_water = true;
|
||||
}
|
||||
return Ok(());
|
||||
});
|
||||
match success {
|
||||
Err(err) => {
|
||||
println!("Could not determine tank value due to {}", err);
|
||||
board.general_fault(true);
|
||||
rv.sensor_error = true;
|
||||
}
|
||||
Ok(_) => {}
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
return TankState {
|
||||
warn_level: false,
|
||||
enough_water: true,
|
||||
left_ml: 1337,
|
||||
sensor_error: false,
|
||||
raw: 0,
|
||||
};
|
||||
}
|
||||
|
||||
fn determine_state_target_moisture_for_plant(
|
||||
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
|
||||
plant: usize,
|
||||
@ -744,10 +679,17 @@ fn determine_state_target_moisture_for_plant(
|
||||
|
||||
if a_low || b_low {
|
||||
state.dry = true;
|
||||
if tank_state.sensor_error && !config.tank.tank_allow_pumping_if_sensor_error {
|
||||
//ignore is ok
|
||||
} else if !tank_state.enough_water {
|
||||
state.no_water = true;
|
||||
match tank_state.enough_water(&config.tank) {
|
||||
Err(_tank_err) => {
|
||||
if !config.tank.tank_allow_pumping_if_sensor_error {
|
||||
// ignore is ok
|
||||
// wtf does this meen, shouldn't something happen if the configuration specifies
|
||||
// that no water should flow if there was an error?
|
||||
}
|
||||
},
|
||||
Ok(enough_water) => {
|
||||
state.no_water = !enough_water
|
||||
},
|
||||
}
|
||||
}
|
||||
let duration = TimeDelta::try_minutes(plant_config.pump_cooldown_min as i64).unwrap();
|
||||
@ -803,10 +745,15 @@ fn determine_state_timer_only_for_plant(
|
||||
state.next_pump = Some(europe_time);
|
||||
state.cooldown = true;
|
||||
} else {
|
||||
if tank_state.sensor_error && !config.tank.tank_allow_pumping_if_sensor_error {
|
||||
state.do_water = true;
|
||||
} else if !tank_state.enough_water {
|
||||
state.no_water = true;
|
||||
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;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -849,10 +796,15 @@ fn determine_state_timer_and_deadzone_for_plant(
|
||||
state.out_of_work_hour = true;
|
||||
}
|
||||
if !state.cooldown && !state.out_of_work_hour {
|
||||
if tank_state.sensor_error && !config.tank.tank_allow_pumping_if_sensor_error {
|
||||
state.do_water = true;
|
||||
} else if !tank_state.enough_water {
|
||||
state.no_water = true;
|
||||
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;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1051,24 +1003,6 @@ fn to_string<T: Display>(value: Result<T>) -> String {
|
||||
};
|
||||
}
|
||||
|
||||
fn map_range(from_range: (f32, f32), s: f32) -> anyhow::Result<f32> {
|
||||
if s < from_range.0 {
|
||||
anyhow::bail!(
|
||||
"Value out of range, min {} but current is {}",
|
||||
from_range.0,
|
||||
s
|
||||
);
|
||||
}
|
||||
if s > from_range.1 {
|
||||
anyhow::bail!(
|
||||
"Value out of range, max {} but current is {}",
|
||||
from_range.1,
|
||||
s
|
||||
);
|
||||
}
|
||||
return Ok(TO.0 + (s - from_range.0) * (TO.1 - TO.0) / (from_range.1 - from_range.0));
|
||||
}
|
||||
|
||||
fn map_range_moisture(s: f32) -> Result<u8, SensorError> {
|
||||
if s < FROM.0 {
|
||||
return Err(SensorError::OpenCircuit { hz: s, min: FROM.0 });
|
||||
|
@ -471,44 +471,23 @@ impl PlantCtrlBoard<'_> {
|
||||
Ok(sensor_data.temperature / 10_f32)
|
||||
}
|
||||
|
||||
pub fn tank_sensor_percent(&mut self) -> Result<u16> {
|
||||
/// return median tank sensor value in milli volt
|
||||
pub fn tank_sensor_voltage(&mut self) -> Result<f32> {
|
||||
let delay = Delay::new_default();
|
||||
self.tank_power.set_high()?;
|
||||
//let stabilize
|
||||
delay.delay_ms(100);
|
||||
unsafe {
|
||||
vTaskDelay(100);
|
||||
}
|
||||
|
||||
let mut store = [0_u16; TANK_MULTI_SAMPLE];
|
||||
for multisample in 0..TANK_MULTI_SAMPLE {
|
||||
let value = self.tank_channel.read()?;
|
||||
store[multisample] = value;
|
||||
}
|
||||
self.tank_power.set_low()?;
|
||||
|
||||
store.sort();
|
||||
let median = store[6] as f32 / 1000_f32;
|
||||
let config_open_voltage_mv = 3.0;
|
||||
if config_open_voltage_mv < median {
|
||||
self.tank_power.set_low()?;
|
||||
bail!(
|
||||
"Tank sensor missing, open loop voltage {} on tank sensor input {}",
|
||||
config_open_voltage_mv,
|
||||
median
|
||||
);
|
||||
}
|
||||
|
||||
let r2 = median * 50.0 / (3.3 - median);
|
||||
let mut percent = r2 / 190_f32 * 100_f32;
|
||||
percent = percent.clamp(0.0, 100.0);
|
||||
log(
|
||||
LogMessage::SensorTankRaw,
|
||||
median as u32,
|
||||
percent as u32,
|
||||
"",
|
||||
"",
|
||||
);
|
||||
|
||||
return Ok(percent as u16);
|
||||
let median_mv = store[6] as f32 / 1000_f32;
|
||||
Ok(median_mv)
|
||||
}
|
||||
|
||||
pub fn set_low_voltage_in_cycle(&mut self) {
|
||||
|
159
rust/src/tank.rs
Normal file
159
rust/src/tank.rs
Normal file
@ -0,0 +1,159 @@
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{config::{PlantControllerConfig, TankConfig}, plant_hal::PlantCtrlBoard};
|
||||
|
||||
const OPEN_TANK_VOLTAGE: f32 = 3.0;
|
||||
pub const WATER_FROZEN_THRESH: f32 = 4.0;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub enum TankError {
|
||||
SensorDisabled,
|
||||
SensorMissing(f32),
|
||||
SensorValueError { value: f32, min: f32, max: f32 },
|
||||
BoardError(String)
|
||||
}
|
||||
|
||||
pub enum TankState {
|
||||
TankSensorPresent(f32),
|
||||
TankSensorError(TankError),
|
||||
TankSensorDisabled,
|
||||
}
|
||||
|
||||
fn raw_volatge_to_divider_percent(raw_value_mv: f32) -> Result<f32, TankError> {
|
||||
if raw_value_mv > OPEN_TANK_VOLTAGE {
|
||||
return Err(TankError::SensorMissing(raw_value_mv));
|
||||
}
|
||||
|
||||
let r2 = raw_value_mv * 50.0 / (3.3 - raw_value_mv);
|
||||
let mut percent = r2 / 190_f32 * 100_f32;
|
||||
percent = percent.clamp(0.0, 100.0);
|
||||
Ok(percent)
|
||||
}
|
||||
|
||||
fn raw_voltage_to_tank_fill_percent(
|
||||
raw_value_mv: f32,
|
||||
config: &TankConfig,
|
||||
) -> Result<f32, TankError> {
|
||||
let divider_percent = raw_volatge_to_divider_percent(raw_value_mv)?;
|
||||
if divider_percent < config.tank_empty_percent.into() || divider_percent > config.tank_full_percent.into() {
|
||||
return Err(TankError::SensorValueError {
|
||||
value: divider_percent,
|
||||
min: config.tank_empty_percent.into(),
|
||||
max: config.tank_full_percent.into(),
|
||||
});
|
||||
}
|
||||
Ok((divider_percent - f32::from(config.tank_empty_percent)) * 100.
|
||||
/ f32::from(config.tank_full_percent - config.tank_empty_percent))
|
||||
}
|
||||
|
||||
|
||||
impl TankState {
|
||||
pub fn left_ml(&self, config: &TankConfig) -> Result<f32, TankError> {
|
||||
match self {
|
||||
TankState::TankSensorDisabled => Err(TankError::SensorDisabled),
|
||||
TankState::TankSensorError(err) => Err(err.clone()),
|
||||
TankState::TankSensorPresent(raw_value_mv) => {
|
||||
let tank_fill_percent = raw_voltage_to_tank_fill_percent(*raw_value_mv, config)?;
|
||||
Ok(config.tank_useable_ml as f32 * tank_fill_percent / 100.)
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn enough_water(&self, config: &TankConfig) -> Result<bool, TankError> {
|
||||
match self {
|
||||
TankState::TankSensorDisabled => Err(TankError::SensorDisabled),
|
||||
TankState::TankSensorError(err) => Err(err.clone()),
|
||||
TankState::TankSensorPresent(raw_value_mv) => {
|
||||
let tank_fill_percent = raw_voltage_to_tank_fill_percent(*raw_value_mv, config)?;
|
||||
if tank_fill_percent > config.tank_empty_percent.into() {
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn warn_level(&self, config: &TankConfig) -> Result<bool, TankError> {
|
||||
match self {
|
||||
TankState::TankSensorDisabled => Err(TankError::SensorDisabled),
|
||||
TankState::TankSensorError(err) => Err(err.clone()),
|
||||
TankState::TankSensorPresent(raw_value_mv) => {
|
||||
let tank_fill_percent = raw_voltage_to_tank_fill_percent(*raw_value_mv, config)?;
|
||||
if tank_fill_percent < config.tank_warn_percent.into() {
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn got_error(&self, config: &TankConfig) -> Option<TankError> {
|
||||
match self {
|
||||
TankState::TankSensorPresent(raw_value_mv) => raw_voltage_to_tank_fill_percent(*raw_value_mv, config).err(),
|
||||
TankState::TankSensorError(err) => Some(err.clone()),
|
||||
TankState::TankSensorDisabled => Some(TankError::SensorDisabled),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_mqtt_info(&self, config: &TankConfig, water_temp: Result<f32, anyhow::Error>) -> TankInfo {
|
||||
let mut tank_err: Option<TankError> = None;
|
||||
let left_ml = match self.left_ml(config) {
|
||||
Err(err) => { tank_err = Some(err); None },
|
||||
Ok(left_ml) => Some(left_ml),
|
||||
};
|
||||
let enough_water = self.enough_water(config).unwrap_or(false); //NOTE: is this correct if there is an error assume not enough water?
|
||||
let warn_level = self.warn_level(config).unwrap_or(false); //NOTE: should no warn level be triggered if there is an error?
|
||||
let raw = match self {
|
||||
TankState::TankSensorDisabled
|
||||
| TankState::TankSensorError(_) => None,
|
||||
TankState::TankSensorPresent(raw_value_mv) => Some(*raw_value_mv),
|
||||
};
|
||||
|
||||
TankInfo {
|
||||
enough_water,
|
||||
warn_level,
|
||||
left_ml,
|
||||
sensor_error: tank_err,
|
||||
raw,
|
||||
water_frozen: water_temp.as_ref().is_ok_and(|temp| *temp < WATER_FROZEN_THRESH),
|
||||
water_temp: water_temp.as_ref().copied().ok(),
|
||||
temp_sensor_error: water_temp.err().map(|err| err.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn determine_tank_state(
|
||||
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
|
||||
config: &PlantControllerConfig,
|
||||
) -> TankState {
|
||||
if config.tank.tank_sensor_enabled {
|
||||
match board.tank_sensor_voltage() {
|
||||
Ok(raw_sensor_value_mv) => TankState::TankSensorPresent(raw_sensor_value_mv),
|
||||
Err(err) => TankState::TankSensorError(TankError::BoardError(err.to_string()))
|
||||
}
|
||||
|
||||
} else {
|
||||
TankState::TankSensorDisabled
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
/// Information structure send to mqtt for monitoring purposes
|
||||
pub struct TankInfo {
|
||||
/// is there enough water in the tank
|
||||
enough_water: bool,
|
||||
/// warning that water needs to be refilled soon
|
||||
warn_level: bool,
|
||||
/// estimation how many ml are still in tank
|
||||
left_ml: Option<f32>,
|
||||
/// if there is was an issue with the water level sensor
|
||||
sensor_error: Option<TankError>,
|
||||
/// raw water sensor value
|
||||
raw: Option<f32>,
|
||||
/// water in tank might be frozen
|
||||
water_frozen: bool,
|
||||
/// water temperature
|
||||
water_temp: Option<f32>,
|
||||
temp_sensor_error: Option<String>,
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user