allow to selectivly enable redundant sensors

This commit is contained in:
Empire 2024-02-17 17:25:50 +01:00
parent 680d1c3aaf
commit 9d1a807805
5 changed files with 260 additions and 101 deletions

View File

@ -65,7 +65,7 @@ embedded-svc = { version = "0.27.0", features = ["experimental"] }
esp-idf-hal = "0.43.0" esp-idf-hal = "0.43.0"
esp-idf-sys = { version = "0.34.0", features = ["binstart", "native"] } esp-idf-sys = { version = "0.34.0", features = ["binstart", "native"] }
esp_idf_build = "0.1.3" 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" ]} chrono-tz = {version="0.8.0", default-features = false , features = [ "filter-by-regex" ]}
embedded-hal = "1.0.0" embedded-hal = "1.0.0"
one-wire-bus = "0.1.1" one-wire-bus = "0.1.1"

View File

@ -59,6 +59,8 @@ pub struct Plant {
pub pump_cooldown_min: u16, pub pump_cooldown_min: u16,
pub pump_hour_start: u8, pub pump_hour_start: u8,
pub pump_hour_end: u8, pub pump_hour_end: u8,
pub sensor_b: bool,
pub sensor_p: bool
} }
impl Default for Plant { impl Default for Plant {
fn default() -> Self { fn default() -> Self {
@ -69,6 +71,8 @@ impl Default for Plant {
pump_hour_start: 8, pump_hour_start: 8,
pump_hour_end: 20, pump_hour_end: 20,
mode: Mode::OFF, mode: Mode::OFF,
sensor_b : false,
sensor_p : false
} }
} }
} }

View File

@ -6,6 +6,7 @@ use std::{
use chrono::{DateTime, Datelike, Duration, NaiveDateTime, Timelike}; use chrono::{DateTime, Datelike, Duration, NaiveDateTime, Timelike};
use chrono_tz::{Europe::Berlin, Tz}; use chrono_tz::{Europe::Berlin, Tz};
use config::Plant; use config::Plant;
use embedded_svc::mqtt;
use esp_idf_hal::delay::Delay; use esp_idf_hal::delay::Delay;
use esp_idf_sys::{ use esp_idf_sys::{
esp_deep_sleep, esp_restart, gpio_deep_sleep_hold_dis, gpio_deep_sleep_hold_en, vTaskDelay, 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, is_day: bool,
} }
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Default)] #[derive(Copy, Clone, Debug, PartialEq, Default)]
struct PlantState { struct PlantState {
a: Option<u8>, a: Option<u8>,
b: Option<u8>, b: Option<u8>,
@ -83,6 +84,7 @@ struct PlantState {
sensor_error_b: Option<SensorError>, sensor_error_b: Option<SensorError>,
sensor_error_p: Option<SensorError>, sensor_error_p: Option<SensorError>,
out_of_work_hour: bool, out_of_work_hour: bool,
next_pump: Option<DateTime<Tz>>
} }
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)] #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]
@ -230,7 +232,7 @@ fn safe_main() -> anyhow::Result<()> {
}; };
println!("attempting to connect wifi"); println!("attempting to connect wifi");
match board.wifi(wifi.ssid, wifi.password, 5000) { match board.wifi(wifi.ssid, wifi.password, 10000) {
Ok(_) => { Ok(_) => {
online_mode = OnlineMode::Wifi; online_mode = OnlineMode::Wifi;
} }
@ -288,7 +290,10 @@ fn safe_main() -> anyhow::Result<()> {
if online_mode == OnlineMode::Online { if online_mode == OnlineMode::Online {
let _ = board.mqtt_publish(&config, "/firmware/githash", git_hash.as_bytes()); 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, "/state", "online".as_bytes());
let _ = board.mqtt_publish(&config, "/last_online", europe_time.to_rfc3339().as_bytes());
publish_battery_state(&mut board, &config); 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; let consecutive_pump_count = board.consecutive_pump_count(plant) + 1;
board.store_consecutive_pump_count(plant, consecutive_pump_count); board.store_consecutive_pump_count(plant, consecutive_pump_count);
let plant_config = config.plants[plant]; 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!( println!(
"Trying to pump for {}s with pump {} now", "Trying to pump for {}s with pump {} now",
plant_config.pump_time_s, plant plant_config.pump_time_s, plant
@ -358,32 +377,61 @@ fn safe_main() -> anyhow::Result<()> {
board.pump(plant, true)?; board.pump(plant, true)?;
board.last_pump_time(plant); board.last_pump_time(plant);
state.active = true; state.active = true;
//FIXME do periodic pump test here and state update for t in 0..plant_config.pump_time_s {
unsafe { vTaskDelay(plant_config.pump_time_s as u32 * CONFIG_FREERTOS_HZ) }; //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)?; board.pump(plant, false)?;
match map_range_moisture( if plant_config.sensor_p {
board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32 match map_range_moisture(
) { board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32
Ok(p) => state.after_p = Some(p), ) {
Err(err) => { 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); 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 => { None => {
println!("Nothing to do"); println!("Nothing to do");
} }
} }
if online_mode == OnlineMode::Online {
update_plant_state(&mut plantstate, &mut board, &config);
}
let mut light_state = LightState { let mut light_state = LightState {
..Default::default() ..Default::default()
}; };
@ -700,20 +748,23 @@ fn determine_state_target_moisture_for_plant(
state.sensor_error_a = Some(SensorError::Unknown); state.sensor_error_a = Some(SensorError::Unknown);
} }
} }
match board.measure_moisture_hz(plant, plant_hal::Sensor::B) { if plant_config.sensor_b {
Ok(b) => { match board.measure_moisture_hz(plant, plant_hal::Sensor::B) {
let mapped = map_range_moisture(b as f32); Ok(b) => {
match mapped { let mapped = map_range_moisture(b as f32);
Ok(result) => state.b = Some(result), match mapped {
Err(err) => { Ok(result) => state.b = Some(result),
state.sensor_error_b = Some(err); Err(err) => {
state.sensor_error_b = Some(err);
}
} }
} }
} Err(_) => {
Err(_) => { state.sensor_error_b = Some(SensorError::Unknown);
state.sensor_error_b = Some(SensorError::Unknown); }
} }
} }
//FIXME how to average analyze whatever? //FIXME how to average analyze whatever?
let a_low = state.a.is_some() && state.a.unwrap() < plant_config.target_moisture; 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; 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 duration = Duration::minutes((plant_config.pump_cooldown_min).into());
let next_pump = board.last_pump_time(plant) + duration; let next_pump = board.last_pump_time(plant) + duration;
if next_pump > cur { if next_pump > cur {
let europe_time = next_pump.with_timezone(&Berlin);
state.next_pump = Some(europe_time);
state.cooldown = true; state.cooldown = true;
} }
if !in_time_range( if !in_time_range(
@ -779,6 +832,8 @@ fn determine_next_plant(
let duration = Duration::minutes((plant_config.pump_cooldown_min).into()); let duration = Duration::minutes((plant_config.pump_cooldown_min).into());
let next_pump = board.last_pump_time(plant) + duration; let next_pump = board.last_pump_time(plant) + duration;
if next_pump > cur { if next_pump > cur {
let europe_time = next_pump.with_timezone(&Berlin);
state.next_pump = Some(europe_time);
state.cooldown = true; state.cooldown = true;
} else { } else {
if water_frozen { if water_frozen {
@ -792,6 +847,8 @@ fn determine_next_plant(
let duration = Duration::minutes((60 * plant_config.pump_cooldown_min).into()); let duration = Duration::minutes((60 * plant_config.pump_cooldown_min).into());
let next_pump = board.last_pump_time(plant) + duration; let next_pump = board.last_pump_time(plant) + duration;
if next_pump > cur { if next_pump > cur {
let europe_time = next_pump.with_timezone(&Berlin);
state.next_pump = Some(europe_time);
state.cooldown = true; state.cooldown = true;
} }
if !in_time_range( if !in_time_range(
@ -828,69 +885,7 @@ fn determine_next_plant(
println!("Plant {} state is {:?}", plant, state); println!("Plant {} state is {:?}", plant, state);
} }
if online_mode == OnlineMode::Online { if online_mode == OnlineMode::Online {
for plant in 0..PLANT_COUNT { update_plant_state(plantstate, board, config);
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(),
);
}
} }
for plant in 0..PLANT_COUNT { for plant in 0..PLANT_COUNT {
let state = &plantstate[plant]; let state = &plantstate[plant];
@ -906,6 +901,133 @@ fn determine_next_plant(
return None; 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<AtomicBool>) -> ! { fn wait_infinity(wait_type: WaitType, reboot_now: Arc<AtomicBool>) -> ! {
let delay = match wait_type { let delay = match wait_type {
WaitType::InitialConfig => 250_u32, WaitType::InitialConfig => 250_u32,

View File

@ -589,21 +589,18 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> {
} }
for i in 0..8 { for i in 0..8 {
self.pump(i, true)?; self.pump(i, true)?;
unsafe { vTaskDelay(500) }; unsafe { vTaskDelay(100) };
self.pump(i, false)?; self.pump(i, false)?;
unsafe { vTaskDelay(500) }; unsafe { vTaskDelay(100) };
} }
for i in 0..8 { for i in 0..8 {
self.measure_moisture_hz(i, Sensor::A)?; self.measure_moisture_hz(i, Sensor::A)?;
unsafe { vTaskDelay(500) };
} }
for i in 0..8 { for i in 0..8 {
self.measure_moisture_hz(i, Sensor::B)?; self.measure_moisture_hz(i, Sensor::B)?;
unsafe { vTaskDelay(500) };
} }
for i in 0..8 { for i in 0..8 {
self.measure_moisture_hz(i, Sensor::PUMP)?; self.measure_moisture_hz(i, Sensor::PUMP)?;
unsafe { vTaskDelay(500) };
} }
Ok(()) Ok(())

View File

@ -18,7 +18,9 @@ interface PlantConfig {
pump_time_s: number, pump_time_s: number,
pump_cooldown_min: number, pump_cooldown_min: number,
pump_hour_start: 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) holder.appendChild(text)
text.innerHTML += "Pump Hour End" 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); sync(current);
} }
@ -208,6 +236,11 @@ let fromWrapper = (() => {
plant_pump_hour_start.value = current.plants[i].pump_hour_start.toString(); 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; 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(); 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_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_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_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] = { current.plants[i] = {
mode: plant_mode.value, mode: plant_mode.value,
@ -243,8 +278,9 @@ let fromWrapper = (() => {
pump_time_s: +plant_pump_time_s.value, pump_time_s: +plant_pump_time_s.value,
pump_cooldown_min: +plant_pump_cooldown_min.value, pump_cooldown_min: +plant_pump_cooldown_min.value,
pump_hour_start: +plant_pump_hour_start.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); sync(current);