diff --git a/rust/src/config.rs b/rust/src/config.rs index e99f837..d2fdb84 100644 --- a/rust/src/config.rs +++ b/rust/src/config.rs @@ -14,8 +14,8 @@ pub struct Config { pub tank_sensor_enabled: bool, pub tank_useable_ml: u32, pub tank_warn_percent: u8, - pub tank_empty_mv: f32, - pub tank_full_mv: f32, + pub tank_empty_mv: u16, + pub tank_full_mv: u16, pub night_lamp_hour_start: u8, pub night_lamp_hour_end: u8, @@ -38,8 +38,8 @@ impl Default for Config { plants: [Plant::default(); PLANT_COUNT], max_consecutive_pump_count: 15, tank_useable_ml: 5000, - tank_empty_mv: 0.1, - tank_full_mv: 3.3, + tank_empty_mv: 0100_u16, + tank_full_mv: 3300_u16, } } } @@ -48,6 +48,7 @@ pub enum Mode { OFF, TargetMoisture, TimerOnly, + TimerAndDeadzone, } #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)] diff --git a/rust/src/main.rs b/rust/src/main.rs index d2bd2f9..ef6d10a 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -3,11 +3,11 @@ use std::{ sync::{atomic::AtomicBool, Arc, Mutex}, }; -use anyhow::Result; -use chrono::{Datelike, Duration, NaiveDateTime, Timelike}; -use chrono_tz::Europe::Berlin; +use anyhow::{Result, bail}; +use chrono::{Datelike, Duration, NaiveDateTime, Timelike, DateTime}; +use chrono_tz::{Europe::Berlin, Tz}; use esp_idf_hal::delay::Delay; -use esp_idf_sys::{esp_restart, vTaskDelay}; +use esp_idf_sys::{esp_restart, vTaskDelay, CONFIG_FREERTOS_HZ, esp_deep_sleep}; use esp_ota::rollback_and_reboot; use log::error; use once_cell::sync::Lazy; @@ -51,16 +51,21 @@ enum WaitType { #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Default)] struct PlantState { - a: u8, - b: u8, - p: u8, - after_p: u8, + a: Option, + b: Option, + p: Option, + after_p: Option, + do_water: bool, dry: bool, active: bool, pump_error: bool, not_effective: bool, cooldown: bool, no_water: bool, + sensor_error_a: bool, + sensor_error_b: bool, + sensor_error_p: bool, + out_of_work_hour: bool } fn wait_infinity(wait_type: WaitType, reboot_now: Arc) -> ! { @@ -91,7 +96,9 @@ fn wait_infinity(wait_type: WaitType, reboot_now: Arc) -> ! { vTaskDelay(delay); if wait_type == WaitType::StayAlive && !STAY_ALIVE.load(std::sync::atomic::Ordering::Relaxed) - {} + { + reboot_now.store(true, std::sync::atomic::Ordering::Relaxed); + } if reboot_now.load(std::sync::atomic::Ordering::Relaxed) { println!("Rebooting"); esp_restart(); @@ -103,8 +110,131 @@ fn wait_infinity(wait_type: WaitType, reboot_now: Arc) -> ! { pub static BOARD_ACCESS: Lazy> = Lazy::new(|| PlantHal::create().unwrap()); pub static STAY_ALIVE: Lazy = Lazy::new(|| AtomicBool::new(false)); -fn map_range(from_range: (f32, f32), to_range: (f32, f32), s: f32) -> f32 { - to_range.0 + (s - from_range.0) * (to_range.1 - to_range.0) / (from_range.1 - from_range.0) +fn map_range(from_range: (f32, f32), s: f32) -> Result { + if s < from_range.0 { + bail!("Value out of range, min {} but current is {}", from_range.0, s); + } + if s > from_range.1 { + 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 { + bail!("Value out of range, min {} but current is {}", FROM.0, s); + } + if s > FROM.1 { + bail!("Value out of range, max {} but current is {}", FROM.1, s); + } + 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, start:u8, end:u8) -> bool{ + let curhour = cur.hour() as u8; + //eg 10-14 + if(start < end){ + return curhour > start && curhour < end; + } else { + //eg 20-05 + return curhour > start || curhour < end;; + } +} + +fn determine_next_plant(plantstate: &mut [PlantState;PLANT_COUNT],cur: DateTime, enough_water: bool, tank_sensor_error: bool, config: &Config, board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>) -> Option { + 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 => { + match board.measure_moisture_hz(plant, plant_hal::Sensor::A).and_then (|moist| map_range_moisture(moist as f32)) { + Ok(a) => state.a = Some(a), + Err(err) => { + board.fault(plant, true); + println!("Could not determine Moisture A for plant {} due to {}", plant, err); + state.a = None; + state.sensor_error_a = true; + } + } + match board.measure_moisture_hz(plant, plant_hal::Sensor::B).and_then (|moist| map_range_moisture(moist as f32)) { + Ok(b) => state.b = Some(b), + Err(err) => { + board.fault(plant, true); + println!("Could not determine Moisture B for plant {} due to {}", plant, err); + state.b = None; + state.sensor_error_b = true; + } + } + //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; + if tank_sensor_error && !config.tank_allow_pumping_if_sensor_error || !enough_water { + state.no_water = true; + } + } + let duration = Duration::minutes((60 * plant_config.pump_cooldown_min).into()); + let next_pump = board.last_pump_time(plant) + duration; + if next_pump > cur { + 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.dry && !state.no_water && !state.cooldown && !state.out_of_work_hour){ + state.do_water = true; + } + }, + config::Mode::TimerOnly => { + let duration = Duration::minutes((60 * plant_config.pump_cooldown_min).into()); + let next_pump = board.last_pump_time(plant) + duration; + if next_pump > cur { + state.cooldown = true; + } else { + state.do_water = true; + } + }, + config::Mode::TimerAndDeadzone => { + let duration = Duration::minutes((60 * plant_config.pump_cooldown_min).into()); + let next_pump = board.last_pump_time(plant) + duration; + if next_pump > cur { + 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){ + state.do_water = true; + } + }, + } + //FIXME publish state here! + if state.do_water{ + if board.consecutive_pump_count(plant) > config.max_consecutive_pump_count.into() { + state.not_effective = true; + board.fault(plant, true); + } + } else { + board.store_consecutive_pump_count(plant, 0); + } + println!("Plant {} state is {:?}", plant, state); + } + for plant in 0..PLANT_COUNT { + let state = &plantstate[plant]; + println!("Checking for water plant {} with state {}", plant, state.do_water); + if state.do_water { + return Some(plant); + } + } + println!("No plant needs water"); + return None } fn main() -> Result<()> { @@ -129,8 +259,8 @@ fn main() -> Result<()> { println!("Version useing git has {}", git_hash); let mut partition_state: embedded_svc::ota::SlotState = embedded_svc::ota::SlotState::Unknown; - // match esp_idf_svc::ota::EspOta::new() { - // Ok(ota) => { + match esp_idf_svc::ota::EspOta::new() { + Ok(ota) => { // match ota.get_running_slot(){ // Ok(slot) => { // partition_state = slot.state; @@ -143,14 +273,15 @@ fn main() -> Result<()> { // println!("Error getting running slot {}", err); // }, // } - // }, - // Err(err) => { - // println!("Error obtaining ota info {}", err); - // }, - // } + }, + Err(err) => { + println!("Error obtaining ota info {}", err); + }, + } println!("Board hal init"); let mut board: std::sync::MutexGuard<'_, PlantCtrlBoard<'_>> = BOARD_ACCESS.lock().unwrap(); + board.disable_all()?; println!("Mounting filesystem"); board.mount_file_system()?; let free_space = board.file_system_size()?; @@ -257,11 +388,12 @@ fn main() -> Result<()> { board.general_fault(true); } } - println!("Running logic at utc {}", cur); - let europe_time = cur.with_timezone(&Berlin); - println!("Running logic at europe/berlin {}", europe_time); } + println!("Running logic at utc {}", cur); + let europe_time = cur.with_timezone(&Berlin); + println!("Running logic at europe/berlin {}", europe_time); + let config: Config; match board.get_config() { Ok(valid) => { @@ -298,92 +430,66 @@ fn main() -> Result<()> { } let mut enough_water = true; + let mut tank_sensor_error = false; if config.tank_sensor_enabled { - let tank_value = board.tank_sensor_mv(); - match tank_value { - Ok(tank_raw) => { - //FIXME clear - let percent = map_range( - (config.tank_empty_mv, config.tank_full_mv), - (0_f32, 100_f32), - tank_raw.into(), - ); - let left_ml = ((percent / 100_f32) * config.tank_useable_ml as f32) as u32; - println!( - "Tank sensor returned mv {} as {}% leaving {} ml useable", - tank_raw, percent as u8, left_ml - ); - if config.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_warn_percent - ); - //FIXME warn here - } - if config.tank_warn_percent <= 0 { - enough_water = false; - } - } - Err(_) => { + let mut tank_value_r = 0; + + let success = board.tank_sensor_mv().and_then(|raw| { + tank_value_r = raw; + return map_range( + (config.tank_empty_mv as f32, config.tank_full_mv as f32), + raw as f32, + ); + }).and_then(|percent| { + let left_ml = ((percent / 100_f32) * config.tank_useable_ml as f32) as u32; + println!( + "Tank sensor returned mv {} as {}% leaving {} ml useable", + tank_value_r, percent as u8, left_ml + ); + if config.tank_warn_percent > percent as u8 { board.general_fault(true); - if !config.tank_allow_pumping_if_sensor_error { - enough_water = false; - } - //set tank sensor state to fault + println!( + "Low water, current percent is {}, minimum warn level is {}", + percent as u8, config.tank_warn_percent + ); } + if config.tank_warn_percent <= 0 { + enough_water = false; + } + return Ok(()); + }); + match success { + Err(err) => { + println!("Could not determine tank value due to {}", err); + board.general_fault(true); + tank_sensor_error = true; + } + Ok(_) => {}, } } - let plantstate = [PlantState { + let mut water_frozen = false; + for _attempt in 0..5 { + let water_temperature = board.water_temperature_c(); + match water_temperature { + Ok(temp) => { + //FIXME mqtt here + println!("Water temp is {}", temp); + if(temp < 4_f32){ + water_frozen = true; + } + break; + }, + Err(err) => { + println!("Could not get water temp {}", err) + }, + } + } + + let mut plantstate = [PlantState { ..Default::default() }; PLANT_COUNT]; - for plant in 0..PLANT_COUNT { - let mut state = plantstate[plant]; - //return mapf(mMoisture_raw.getMedian(), MOIST_SENSOR_MIN_FRQ, MOIST_SENSOR_MAX_FRQ, 0, 100); - state.a = map_range( - FROM, - TO, - board.measure_moisture_hz(plant, plant_hal::Sensor::A)? as f32, - ) as u8; - state.b = map_range( - FROM, - TO, - board.measure_moisture_hz(plant, plant_hal::Sensor::B)? as f32, - ) as u8; - state.p = map_range( - FROM, - TO, - board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32, - ) as u8; - let plant_config = config.plants[plant]; - - //FIXME how to average analyze whatever? - if state.a < plant_config.target_moisture || state.b < plant_config.target_moisture { - state.dry = true; - if !enough_water { - state.no_water = true; - } - } - - let duration = Duration::minutes((60 * plant_config.pump_cooldown_min).into()); - if (board.last_pump_time(plant)? + duration) > cur { - state.cooldown = true; - } - - if state.dry { - let consecutive_pump_count = board.consecutive_pump_count(plant) + 1; - board.store_consecutive_pump_count(plant, consecutive_pump_count); - if consecutive_pump_count > config.max_consecutive_pump_count.into() { - state.not_effective = true; - board.fault(plant, true); - } - } else { - board.store_consecutive_pump_count(plant, 0); - } - - //TODO update mqtt state here! - } + let plant_to_pump = determine_next_plant(&mut plantstate, europe_time, enough_water, tank_sensor_error, &config, &mut board); if STAY_ALIVE.load(std::sync::atomic::Ordering::Relaxed) { drop(board); @@ -391,30 +497,40 @@ fn main() -> Result<()> { let _webserver = httpd(reboot_now.clone()); wait_infinity(WaitType::StayAlive, reboot_now.clone()); } - - 'eachplant: for plant in 0..PLANT_COUNT { - let mut state = plantstate[plant]; - if state.dry && !state.cooldown { - println!("Trying to pump with pump {} now", plant); + + match plant_to_pump { + Some(plant) => { + let mut state = plantstate[plant]; + let consecutive_pump_count = board.consecutive_pump_count(plant) + 1; + board.store_consecutive_pump_count(plant, consecutive_pump_count); let plant_config = config.plants[plant]; - + println!("Trying to pump for {}s with pump {} now", plant_config.pump_time_s,plant); + board.any_pump(true)?; board.store_last_pump_time(plant, cur); board.pump(plant, true)?; - board.last_pump_time(plant)?; + board.last_pump_time(plant); state.active = true; - unsafe { vTaskDelay(plant_config.pump_time_s.into()) }; - state.after_p = map_range( - FROM, - TO, - board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32, - ) as u8; - if state.after_p < state.p + 5 { + //FIXME do periodic pump test here and state update + unsafe { vTaskDelay(plant_config.pump_time_s as u32*CONFIG_FREERTOS_HZ) }; + match map_range_moisture(board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32) { + Ok(p) => state.after_p = Some(p), + Err(err) => { + board.fault(plant, true); + println!("Could not determine Moisture P after for plant {} due to {}", plant, err); + state.after_p = None; + state.sensor_error_p = true; + } + } + if state.after_p.is_none() || state.p.is_none() || state.after_p.unwrap() < state.p.unwrap() + 5 { state.pump_error = true; board.fault(plant, true); } - break 'eachplant; + }, + None => { + println!("Nothing to do"); } + , } /* @@ -437,6 +553,7 @@ fn main() -> Result<()> { } */ //deepsleep here? + unsafe { esp_deep_sleep(1000*1000*10) }; Ok(()) } diff --git a/rust/src/plant_hal.rs b/rust/src/plant_hal.rs index a40b1ad..dcac80c 100644 --- a/rust/src/plant_hal.rs +++ b/rust/src/plant_hal.rs @@ -143,7 +143,7 @@ pub trait PlantCtrlBoardInteraction { fn measure_moisture_hz(&self, plant: usize, sensor: Sensor) -> Result; fn pump(&self, plant: usize, enable: bool) -> Result<()>; - fn last_pump_time(&self, plant: usize) -> Result>; + fn last_pump_time(&self, plant: usize) -> chrono::DateTime; fn store_last_pump_time(&mut self, plant: usize, time: chrono::DateTime); fn store_consecutive_pump_count(&mut self, plant: usize, count: u32); fn consecutive_pump_count(&mut self, plant: usize) -> u32; @@ -163,6 +163,7 @@ pub trait PlantCtrlBoardInteraction { fn test(&mut self) -> Result<()>; fn is_wifi_config_file_existant(&mut self) -> bool; fn mqtt(&mut self, config: &Config) -> Result<()>; + fn disable_all(&mut self) -> Result<()>; } pub trait CreatePlantHal<'a> { @@ -230,7 +231,7 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> { if sensor_data.temperature == 85_f32 { bail!("Ds18b20 dummy temperature returned"); } - Ok(sensor_data.temperature) + Ok(sensor_data.temperature/10_f32) } fn tank_sensor_mv(&mut self) -> Result { @@ -265,11 +266,10 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> { Ok(()) } - fn last_pump_time(&self, plant: usize) -> Result> { + fn last_pump_time(&self, plant: usize) -> chrono::DateTime { let ts = unsafe { LAST_WATERING_TIMESTAMP }[plant]; - let timestamp = NaiveDateTime::from_timestamp_millis(ts) - .ok_or(anyhow!("could not convert timestamp"))?; - Ok(DateTime::::from_naive_utc_and_offset(timestamp, Utc)) + let timestamp = NaiveDateTime::from_timestamp_millis(ts).unwrap(); + DateTime::::from_naive_utc_and_offset(timestamp, Utc) } fn store_last_pump_time(&mut self, plant: usize, time: chrono::DateTime) { @@ -660,6 +660,15 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> { } bail!("Mqtt did not complete roundtrip in time"); } + + fn disable_all(&mut self) -> Result<()> { + for mut pin in self.shift_register.decompose() { + pin.set_low().unwrap(); + } + self.general_fault(false); + self.any_pump(false)?; + return Ok(()); + } } impl CreatePlantHal<'_> for PlantHal {