diff --git a/rust/Cargo.toml b/rust/Cargo.toml index fb328d0..fe1fa84 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -65,7 +65,7 @@ embedded-svc = { version = "0.27.0", features = ["experimental"] } esp-idf-hal = "0.43.0" esp-idf-sys = { version = "0.34.0", features = ["binstart", "native"] } esp_idf_build = "0.1.3" -chrono = { version = "0.4.23", default-features = false , features = ["iana-time-zone"] } +chrono = { version = "0.4.23", default-features = false , features = ["iana-time-zone" , "alloc"] } chrono-tz = {version="0.8.0", default-features = false , features = [ "filter-by-regex" ]} embedded-hal = "1.0.0" one-wire-bus = "0.1.1" diff --git a/rust/src/config.rs b/rust/src/config.rs index 0ebc29b..7797c98 100644 --- a/rust/src/config.rs +++ b/rust/src/config.rs @@ -59,6 +59,8 @@ pub struct Plant { pub pump_cooldown_min: u16, pub pump_hour_start: u8, pub pump_hour_end: u8, + pub sensor_b: bool, + pub sensor_p: bool } impl Default for Plant { fn default() -> Self { @@ -69,6 +71,8 @@ impl Default for Plant { pump_hour_start: 8, pump_hour_end: 20, mode: Mode::OFF, + sensor_b : false, + sensor_p : false } } } diff --git a/rust/src/main.rs b/rust/src/main.rs index 0c8290a..6a71722 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -6,6 +6,7 @@ use std::{ use chrono::{DateTime, Datelike, Duration, NaiveDateTime, Timelike}; use chrono_tz::{Europe::Berlin, Tz}; use config::Plant; +use embedded_svc::mqtt; use esp_idf_hal::delay::Delay; use esp_idf_sys::{ esp_deep_sleep, esp_restart, gpio_deep_sleep_hold_dis, gpio_deep_sleep_hold_en, vTaskDelay, @@ -65,7 +66,7 @@ struct LightState { is_day: bool, } -#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Default)] +#[derive(Copy, Clone, Debug, PartialEq, Default)] struct PlantState { a: Option, b: Option, @@ -83,6 +84,7 @@ struct PlantState { sensor_error_b: Option, sensor_error_p: Option, out_of_work_hour: bool, + next_pump: Option> } #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)] @@ -230,7 +232,7 @@ fn safe_main() -> anyhow::Result<()> { }; println!("attempting to connect wifi"); - match board.wifi(wifi.ssid, wifi.password, 5000) { + match board.wifi(wifi.ssid, wifi.password, 10000) { Ok(_) => { online_mode = OnlineMode::Wifi; } @@ -288,7 +290,10 @@ fn safe_main() -> anyhow::Result<()> { if online_mode == OnlineMode::Online { let _ = board.mqtt_publish(&config, "/firmware/githash", git_hash.as_bytes()); let _ = board.mqtt_publish(&config, "/state", "online".as_bytes()); + let _ = board.mqtt_publish(&config, "/last_online", europe_time.to_rfc3339().as_bytes()); + + publish_battery_state(&mut board, &config); } @@ -348,6 +353,20 @@ fn safe_main() -> anyhow::Result<()> { 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]; + + + if plant_config.sensor_p { + match map_range_moisture( + board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32 + ) { + Ok(p) => state.p = Some(p), + Err(err) => { + board.fault(plant, true); + state.sensor_error_p = Some(err); + } + } + } + println!( "Trying to pump for {}s with pump {} now", plant_config.pump_time_s, plant @@ -358,32 +377,61 @@ fn safe_main() -> anyhow::Result<()> { board.pump(plant, true)?; board.last_pump_time(plant); state.active = true; - //FIXME do periodic pump test here and state update - unsafe { vTaskDelay(plant_config.pump_time_s as u32 * CONFIG_FREERTOS_HZ) }; + for t in 0..plant_config.pump_time_s { + //FIXME do periodic pump test here and state update + unsafe { vTaskDelay(CONFIG_FREERTOS_HZ) }; + if plant_config.sensor_p { + let moist = map_range_moisture( + board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32); + if online_mode == OnlineMode::Online { + let _ = board.mqtt_publish( + &config, + format!("/plant{}/Sensor P after", plant+1).as_str(), + option_to_string(moist.ok()).as_bytes(), + ); + + } + } else { + if online_mode == OnlineMode::Online { + let _ = board.mqtt_publish( + &config, + format!("/plant{}/Sensor P after", plant+1).as_str(), + "disabled".as_bytes(), + ); + } + } + + } + board.pump(plant, false)?; - match map_range_moisture( - board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32 - ) { - Ok(p) => state.after_p = Some(p), - Err(err) => { + if plant_config.sensor_p { + 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); + state.sensor_error_p = Some(err); + } + } + 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); - state.sensor_error_p = Some(err); + //mqtt sync pump error value } } - 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); - //mqtt sync pump error value - } + } None => { println!("Nothing to do"); } } - + if online_mode == OnlineMode::Online { + update_plant_state(&mut plantstate, &mut board, &config); + } let mut light_state = LightState { ..Default::default() }; @@ -700,20 +748,23 @@ fn determine_state_target_moisture_for_plant( state.sensor_error_a = Some(SensorError::Unknown); } } - match board.measure_moisture_hz(plant, plant_hal::Sensor::B) { - Ok(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); + if plant_config.sensor_b { + match board.measure_moisture_hz(plant, plant_hal::Sensor::B) { + Ok(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); + 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; @@ -729,6 +780,8 @@ fn determine_state_target_moisture_for_plant( let duration = Duration::minutes((plant_config.pump_cooldown_min).into()); let next_pump = board.last_pump_time(plant) + duration; if next_pump > cur { + let europe_time = next_pump.with_timezone(&Berlin); + state.next_pump = Some(europe_time); state.cooldown = true; } if !in_time_range( @@ -779,6 +832,8 @@ fn determine_next_plant( let duration = Duration::minutes((plant_config.pump_cooldown_min).into()); let next_pump = board.last_pump_time(plant) + duration; if next_pump > cur { + let europe_time = next_pump.with_timezone(&Berlin); + state.next_pump = Some(europe_time); state.cooldown = true; } else { if water_frozen { @@ -792,6 +847,8 @@ fn determine_next_plant( let duration = Duration::minutes((60 * plant_config.pump_cooldown_min).into()); let next_pump = board.last_pump_time(plant) + duration; if next_pump > cur { + let europe_time = next_pump.with_timezone(&Berlin); + state.next_pump = Some(europe_time); state.cooldown = true; } if !in_time_range( @@ -828,69 +885,7 @@ fn determine_next_plant( println!("Plant {} state is {:?}", plant, state); } if online_mode == OnlineMode::Online { - for plant in 0..PLANT_COUNT { - let state = &plantstate[plant]; - let _ = board.mqtt_publish( - &config, - format!("/plant{}/Sensor A", plant).as_str(), - option_to_string(state.a).as_bytes(), - ); - let _ = board.mqtt_publish( - &config, - format!("/plant{}/Sensor B", plant).as_str(), - option_to_string(state.b).as_bytes(), - ); - let _ = board.mqtt_publish( - &config, - format!("/plant{}/Sensor P before", plant).as_str(), - option_to_string(state.p).as_bytes(), - ); - let _ = board.mqtt_publish( - &config, - format!("/plant{}/Sensor P after", plant).as_str(), - option_to_string(state.after_p).as_bytes(), - ); - let _ = board.mqtt_publish( - &config, - format!("/plant{}/Should water", plant).as_str(), - state.do_water.to_string().as_bytes(), - ); - let _ = board.mqtt_publish( - &config, - format!("/plant{}/Is frozen", plant).as_str(), - state.frozen.to_string().as_bytes(), - ); - let _ = board.mqtt_publish( - &config, - format!("/plant{}/Is dry", plant).as_str(), - state.dry.to_string().as_bytes(), - ); - let _ = board.mqtt_publish( - &config, - format!("/plant{}/Pump Error", plant).as_str(), - state.pump_error.to_string().as_bytes(), - ); - let _ = board.mqtt_publish( - &config, - format!("/plant{}/Pump Ineffective", plant).as_str(), - state.not_effective.to_string().as_bytes(), - ); - let _ = board.mqtt_publish( - &config, - format!("/plant{}/Is in Cooldown", plant).as_str(), - state.cooldown.to_string().as_bytes(), - ); - let _ = board.mqtt_publish( - &config, - format!("/plant{}/No Water", plant).as_str(), - state.no_water.to_string().as_bytes(), - ); - let _ = board.mqtt_publish( - &config, - format!("/plant{}/Out of Work Hour", plant).as_str(), - state.out_of_work_hour.to_string().as_bytes(), - ); - } + update_plant_state(plantstate, board, config); } for plant in 0..PLANT_COUNT { let state = &plantstate[plant]; @@ -906,6 +901,133 @@ fn determine_next_plant( return None; } +fn update_plant_state(plantstate: &mut [PlantState; PLANT_COUNT], board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, config: &Config){ + for plant in 0..PLANT_COUNT { + let state = &plantstate[plant]; + let plant_config = config.plants[plant]; + + let last_time = board.last_pump_time(plant); + let europe_time = last_time.with_timezone(&Berlin); + if europe_time.year() > 2023 { + let time = europe_time.to_rfc3339(); + let _ = board.mqtt_publish( + &config, + format!("/plant{}/last pump", plant+1).as_str(), + time.as_bytes(), + ); + } else { + let _ = board.mqtt_publish( + &config, + format!("/plant{}/last pump", plant+1).as_str(), + "N/A".as_bytes(), + ); + } + + match state.next_pump { + Some(next) => { + let time = next.to_rfc3339(); + let _ = board.mqtt_publish( + &config, + format!("/plant{}/next pump", plant+1).as_str(), + time.as_bytes(), + ); + }, + None => { + let _ = board.mqtt_publish( + &config, + format!("/plant{}/next pump", plant+1).as_str(), + "N/A".as_bytes(), + ); + }, + } + + + let _ = board.mqtt_publish( + &config, + format!("/plant{}/Sensor A", plant+1).as_str(), + option_to_string(state.a).as_bytes(), + ); + if plant_config.sensor_b { + let _ = board.mqtt_publish( + &config, + format!("/plant{}/Sensor B", plant+1).as_str(), + option_to_string(state.b).as_bytes(), + ); + } else { + let _ = board.mqtt_publish( + &config, + format!("/plant{}/Sensor B", plant+1).as_str(), + "disabled".as_bytes(), + ); + } + + if plant_config.sensor_p { + let _ = board.mqtt_publish( + &config, + format!("/plant{}/Sensor P before", plant+1).as_str(), + option_to_string(state.p).as_bytes(), + ); + let _ = board.mqtt_publish( + &config, + format!("/plant{}/Sensor P after", plant+1).as_str(), + option_to_string(state.after_p).as_bytes(), + ); + } else { + let _ = board.mqtt_publish( + &config, + format!("/plant{}/Sensor P before", plant+1).as_str(), + "disabled".as_bytes(), + ); + let _ = board.mqtt_publish( + &config, + format!("/plant{}/Sensor P after", plant+1).as_str(), + "disabled".as_bytes(), + ); + } + + let _ = board.mqtt_publish( + &config, + format!("/plant{}/Should water", plant+1).as_str(), + state.do_water.to_string().as_bytes(), + ); + let _ = board.mqtt_publish( + &config, + format!("/plant{}/Is frozen", plant+1).as_str(), + state.frozen.to_string().as_bytes(), + ); + let _ = board.mqtt_publish( + &config, + format!("/plant{}/Is dry", plant+1).as_str(), + state.dry.to_string().as_bytes(), + ); + let _ = board.mqtt_publish( + &config, + format!("/plant{}/Pump Error", plant+1).as_str(), + state.pump_error.to_string().as_bytes(), + ); + let _ = board.mqtt_publish( + &config, + format!("/plant{}/Pump Ineffective", plant+1).as_str(), + state.not_effective.to_string().as_bytes(), + ); + let _ = board.mqtt_publish( + &config, + format!("/plant{}/Is in Cooldown", plant+1).as_str(), + state.cooldown.to_string().as_bytes(), + ); + let _ = board.mqtt_publish( + &config, + format!("/plant{}/No Water", plant+1).as_str(), + state.no_water.to_string().as_bytes(), + ); + let _ = board.mqtt_publish( + &config, + format!("/plant{}/Out of Work Hour", plant+1).as_str(), + state.out_of_work_hour.to_string().as_bytes(), + ); + } +} + fn wait_infinity(wait_type: WaitType, reboot_now: Arc) -> ! { let delay = match wait_type { WaitType::InitialConfig => 250_u32, diff --git a/rust/src/plant_hal.rs b/rust/src/plant_hal.rs index db76475..47695ea 100644 --- a/rust/src/plant_hal.rs +++ b/rust/src/plant_hal.rs @@ -589,21 +589,18 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> { } for i in 0..8 { self.pump(i, true)?; - unsafe { vTaskDelay(500) }; + unsafe { vTaskDelay(100) }; self.pump(i, false)?; - unsafe { vTaskDelay(500) }; + unsafe { vTaskDelay(100) }; } for i in 0..8 { self.measure_moisture_hz(i, Sensor::A)?; - unsafe { vTaskDelay(500) }; } for i in 0..8 { self.measure_moisture_hz(i, Sensor::B)?; - unsafe { vTaskDelay(500) }; } for i in 0..8 { self.measure_moisture_hz(i, Sensor::PUMP)?; - unsafe { vTaskDelay(500) }; } Ok(()) diff --git a/rust/src_webpack/src/form.ts b/rust/src_webpack/src/form.ts index 6c0890f..2064658 100644 --- a/rust/src_webpack/src/form.ts +++ b/rust/src_webpack/src/form.ts @@ -18,7 +18,9 @@ interface PlantConfig { pump_time_s: number, pump_cooldown_min: number, pump_hour_start: number, - pump_hour_end: number + pump_hour_end: number, + sensor_b: boolean, + sensor_p: boolean }[] } @@ -173,6 +175,32 @@ let fromWrapper = (() => { holder.appendChild(text) text.innerHTML += "Pump Hour End" } + { + let holder = document.createElement("div"); + plant.appendChild(holder); + let input = document.createElement("input"); + input.id = "plant_" + i + "_sensor_b"; + input.type = "checkbox"; + input.onchange = updateJson; + holder.appendChild(input) + + let text = document.createElement("span"); + holder.appendChild(text) + text.innerHTML += "Sensor B installed" + } + { + let holder = document.createElement("div"); + plant.appendChild(holder); + let input = document.createElement("input"); + input.id = "plant_" + i + "_sensor_p"; + input.type = "checkbox"; + input.onchange = updateJson; + holder.appendChild(input) + + let text = document.createElement("span"); + holder.appendChild(text) + text.innerHTML += "Sensor P installed" + } } sync(current); } @@ -208,6 +236,11 @@ let fromWrapper = (() => { plant_pump_hour_start.value = current.plants[i].pump_hour_start.toString(); let plant_pump_hour_end = document.getElementById("plant_" + i + "_pump_hour_end") as HTMLInputElement; plant_pump_hour_end.value = current.plants[i].pump_hour_end.toString(); + + let plant_sensor_b = document.getElementById("plant_" + i + "_sensor_b") as HTMLInputElement; + plant_sensor_b.checked = current.plants[i].sensor_b; + let plant_sensor_p = document.getElementById("plant_" + i + "_sensor_p") as HTMLInputElement; + plant_sensor_p.checked = current.plants[i].sensor_p; } } @@ -236,6 +269,8 @@ let fromWrapper = (() => { let plant_pump_cooldown_min = document.getElementById("plant_" + i + "_pump_cooldown_min") as HTMLInputElement; let plant_pump_hour_start = document.getElementById("plant_" + i + "_pump_hour_start") as HTMLInputElement; let plant_pump_hour_end = document.getElementById("plant_" + i + "_pump_hour_end") as HTMLInputElement; + let plant_sensor_b = document.getElementById("plant_" + i + "_sensor_b") as HTMLInputElement; + let plant_sensor_p = document.getElementById("plant_" + i + "_sensor_p") as HTMLInputElement; current.plants[i] = { mode: plant_mode.value, @@ -243,8 +278,9 @@ let fromWrapper = (() => { pump_time_s: +plant_pump_time_s.value, pump_cooldown_min: +plant_pump_cooldown_min.value, pump_hour_start: +plant_pump_hour_start.value, - pump_hour_end: +plant_pump_hour_end.value - + pump_hour_end: +plant_pump_hour_end.value, + sensor_b: plant_sensor_b.checked, + sensor_p: plant_sensor_p.checked } } sync(current);