diff --git a/rust/src/log/mod.rs b/rust/src/log/mod.rs index 9d468bc..0b3384b 100644 --- a/rust/src/log/mod.rs +++ b/rust/src/log/mod.rs @@ -137,8 +137,16 @@ pub enum LogMessage { LowVoltage, #[strum(serialize = "Error communicating with battery!! ${txt_long}")] BatteryCommunicationError, - #[strum(serialize = "Tank sensor raw ${number_a} percent ${number_b}")] - SensorTankRaw, + #[strum(serialize = "Tank water level cricial! Refill tank!")] + TankWaterLevelLow, + #[strum(serialize = "Tank sensor hardware error: ${txt_long}")] + TankSensorBoardError, + #[strum(serialize = "Tank sensor not present, raw voltage measured = ${number_a} mV")] + TankSensorMissing, + #[strum( + serialize = "Tank sensor value out of range, min = ${number_a}%, max = ${number_b}%, value = ${text_short}%" + )] + TankSensorValueRangeError, #[strum( serialize = "raw measure unscaled ${number_a} hz ${number_b}, plant ${txt_short} sensor ${txt_long}" )] diff --git a/rust/src/main.rs b/rust/src/main.rs index 8387cb7..dd08ee5 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -17,7 +17,7 @@ use esp_idf_sys::{ esp_ota_img_states_t_ESP_OTA_IMG_VALID, vTaskDelay, }; use esp_ota::{mark_app_valid, rollback_and_reboot}; -use log::log; +use log::{log, LogMessage}; use once_cell::sync::Lazy; use plant_hal::{PlantCtrlBoard, PlantHal, PLANT_COUNT}; use serde::{Deserialize, Serialize}; @@ -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 - 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,67 @@ 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 tank_state.is_enabled() { + if let Some(err) = tank_state.got_error(&config.tank) { + match err { + TankError::SensorDisabled => { /* unreachable */ } + TankError::SensorMissing(raw_value_mv) => log( + LogMessage::TankSensorMissing, + raw_value_mv as u32, + 0, + "", + "", + ), + TankError::SensorValueError { value, min, max } => log( + LogMessage::TankSensorValueRangeError, + min as u32, + max as u32, + &format!("{}", value), + "", + ), + TankError::BoardError(err) => log( + LogMessage::TankSensorBoardError, + 0, + 0, + "", + &format!("{}", &err.to_string()), + ), + } + // disabled can not trigger this because of wrapping if is_enabled + board.general_fault(true); + } else if tank_state.warn_level(&config.tank).is_ok_and(|warn| warn) { + log(LogMessage::TankWaterLevelLow, 0, 0, "", ""); + board.general_fault(true); + } + } + let mut water_frozen = false; - let mut temp: Option = None; - for _attempt in 0..5 { - let water_temperature = board.water_temperature_c(); - match water_temperature { + let mut attempt = 1; + let water_temp: Result = 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 +632,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 +683,15 @@ 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 { + 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(); @@ -803,10 +747,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 +798,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 +1005,6 @@ fn to_string(value: Result) -> String { }; } -fn map_range(from_range: (f32, f32), s: f32) -> anyhow::Result { - 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 { if s < FROM.0 { return Err(SensorError::OpenCircuit { hz: s, min: FROM.0 }); diff --git a/rust/src/plant_hal.rs b/rust/src/plant_hal.rs index c6dc2b5..7bf736e 100644 --- a/rust/src/plant_hal.rs +++ b/rust/src/plant_hal.rs @@ -471,44 +471,23 @@ impl PlantCtrlBoard<'_> { Ok(sensor_data.temperature / 10_f32) } - pub fn tank_sensor_percent(&mut self) -> Result { + /// return median tank sensor value in milli volt + pub fn tank_sensor_voltage(&mut self) -> Result { 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) { diff --git a/rust/src/tank.rs b/rust/src/tank.rs new file mode 100644 index 0000000..f0a1de8 --- /dev/null +++ b/rust/src/tank.rs @@ -0,0 +1,178 @@ +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 { + 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 { + 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 { + 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 { + 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 is_enabled(&self) -> bool { + matches!(self, TankState::TankSensorDisabled) + } + + pub fn warn_level(&self, config: &TankConfig) -> Result { + 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 { + 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, + ) -> TankInfo { + let mut tank_err: Option = 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, + /// if there is was an issue with the water level sensor + sensor_error: Option, + /// raw water sensor value + raw: Option, + /// water in tank might be frozen + water_frozen: bool, + /// water temperature + water_temp: Option, + temp_sensor_error: Option, +}