fix rtc storage
This commit is contained in:
874
rust/src/main.rs
874
rust/src/main.rs
@@ -3,14 +3,14 @@ use std::{
|
||||
sync::{atomic::AtomicBool, Arc, Mutex},
|
||||
};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use chrono::{DateTime, Datelike, Duration, NaiveDateTime, Timelike};
|
||||
use chrono_tz::{Europe::Berlin, Tz};
|
||||
use config::Plant;
|
||||
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, CONFIG_FREERTOS_HZ
|
||||
esp_deep_sleep, esp_restart, gpio_deep_sleep_hold_dis, gpio_deep_sleep_hold_en, vTaskDelay,
|
||||
CONFIG_FREERTOS_HZ,
|
||||
};
|
||||
use esp_ota::rollback_and_reboot;
|
||||
use log::error;
|
||||
use once_cell::sync::Lazy;
|
||||
use plant_hal::{CreatePlantHal, PlantCtrlBoard, PlantCtrlBoardInteraction, PlantHal, PLANT_COUNT};
|
||||
@@ -18,10 +18,11 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
config::{Config, WifiConfig},
|
||||
espota::rollback_and_reboot,
|
||||
webserver::webserver::{httpd, httpd_initial},
|
||||
};
|
||||
pub mod bq34z100;
|
||||
mod config;
|
||||
pub mod espota;
|
||||
pub mod plant_hal;
|
||||
|
||||
const MOIST_SENSOR_MAX_FREQUENCY: u32 = 5200; // 60kHz (500Hz margin)
|
||||
@@ -33,6 +34,9 @@ const FROM: (f32, f32) = (
|
||||
);
|
||||
const TO: (f32, f32) = (0_f32, 100_f32);
|
||||
|
||||
pub static BOARD_ACCESS: Lazy<Mutex<PlantCtrlBoard>> = Lazy::new(|| PlantHal::create().unwrap());
|
||||
pub static STAY_ALIVE: Lazy<AtomicBool> = Lazy::new(|| AtomicBool::new(false));
|
||||
|
||||
mod webserver {
|
||||
pub mod webserver;
|
||||
}
|
||||
@@ -42,6 +46,7 @@ enum OnlineMode {
|
||||
Offline,
|
||||
Wifi,
|
||||
SnTp,
|
||||
Online,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]
|
||||
@@ -74,235 +79,28 @@ struct PlantState {
|
||||
not_effective: bool,
|
||||
cooldown: bool,
|
||||
no_water: bool,
|
||||
sensor_error_a: bool,
|
||||
sensor_error_b: bool,
|
||||
sensor_error_p: bool,
|
||||
sensor_error_a: Option<SensorError>,
|
||||
sensor_error_b: Option<SensorError>,
|
||||
sensor_error_p: Option<SensorError>,
|
||||
out_of_work_hour: bool,
|
||||
}
|
||||
|
||||
fn wait_infinity(wait_type: WaitType, reboot_now: Arc<AtomicBool>) -> ! {
|
||||
let delay = match wait_type {
|
||||
WaitType::InitialConfig => 250_u32,
|
||||
WaitType::FlashError => 100_u32,
|
||||
WaitType::NormalConfig => 500_u32,
|
||||
WaitType::StayAlive => 1000_u32,
|
||||
};
|
||||
let led_count = match wait_type {
|
||||
WaitType::InitialConfig => 8,
|
||||
WaitType::FlashError => 8,
|
||||
WaitType::NormalConfig => 4,
|
||||
WaitType::StayAlive => 2,
|
||||
};
|
||||
loop {
|
||||
unsafe {
|
||||
//do not trigger watchdog
|
||||
for i in 0..8 {
|
||||
BOARD_ACCESS.lock().unwrap().fault(i, i < led_count);
|
||||
}
|
||||
BOARD_ACCESS.lock().unwrap().general_fault(true);
|
||||
vTaskDelay(delay);
|
||||
BOARD_ACCESS.lock().unwrap().general_fault(false);
|
||||
for i in 0..8 {
|
||||
BOARD_ACCESS.lock().unwrap().fault(i, false);
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]
|
||||
enum SensorError {
|
||||
Unknown,
|
||||
ShortCircuit { hz: f32, max: f32 },
|
||||
OpenCircuit { hz: f32, min: f32 },
|
||||
}
|
||||
|
||||
pub static BOARD_ACCESS: Lazy<Mutex<PlantCtrlBoard>> = Lazy::new(|| PlantHal::create().unwrap());
|
||||
pub static STAY_ALIVE: Lazy<AtomicBool> = Lazy::new(|| AtomicBool::new(false));
|
||||
|
||||
fn map_range(from_range: (f32, f32), s: f32) -> Result<f32> {
|
||||
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<u8> {
|
||||
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<Tz>, 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<Tz>,
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Default)]
|
||||
struct TankState {
|
||||
enough_water: bool,
|
||||
water_frozen: bool,
|
||||
tank_sensor_error: bool,
|
||||
config: &Config,
|
||||
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
|
||||
) -> Option<usize> {
|
||||
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 water_frozen {
|
||||
state.frozen = true;
|
||||
}
|
||||
if state.dry && !state.no_water && !state.cooldown && !state.out_of_work_hour {
|
||||
if water_frozen {
|
||||
state.frozen = true;
|
||||
} else {
|
||||
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 {
|
||||
if water_frozen {
|
||||
state.frozen = 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 {
|
||||
if water_frozen {
|
||||
state.frozen = true;
|
||||
} else {
|
||||
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;
|
||||
left_ml: u32,
|
||||
sensor_error: bool,
|
||||
raw: u16,
|
||||
}
|
||||
|
||||
fn safe_main() -> Result<()> {
|
||||
fn safe_main() -> anyhow::Result<()> {
|
||||
// It is necessary to call this function once. Otherwise some patches to the runtime
|
||||
// implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71
|
||||
esp_idf_svc::sys::link_patches();
|
||||
@@ -432,7 +230,7 @@ fn safe_main() -> Result<()> {
|
||||
};
|
||||
|
||||
println!("attempting to connect wifi");
|
||||
match board.wifi(&wifi.ssid, wifi.password.as_deref(), 10000) {
|
||||
match board.wifi(wifi.ssid, wifi.password, 5000) {
|
||||
Ok(_) => {
|
||||
online_mode = OnlineMode::Wifi;
|
||||
}
|
||||
@@ -479,6 +277,7 @@ fn safe_main() -> Result<()> {
|
||||
match board.mqtt(&config) {
|
||||
Ok(_) => {
|
||||
println!("Mqtt connection ready");
|
||||
online_mode = OnlineMode::Online;
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Could not connect mqtt due to {}", err);
|
||||
@@ -486,64 +285,27 @@ fn safe_main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
match board.battery_state() {
|
||||
Ok(_state) => {}
|
||||
Err(err) => {
|
||||
board.general_fault(true);
|
||||
println!("Could not read battery state, assuming low power {}", err);
|
||||
}
|
||||
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());
|
||||
|
||||
publish_battery_state(&mut board, &config);
|
||||
}
|
||||
|
||||
let mut enough_water = true;
|
||||
let mut tank_sensor_error = false;
|
||||
if config.tank_sensor_enabled {
|
||||
let mut tank_value_r = 0;
|
||||
|
||||
let success = board
|
||||
.tank_sensor_percent()
|
||||
.and_then(|raw| {
|
||||
tank_value_r = raw;
|
||||
return map_range(
|
||||
(
|
||||
config.tank_empty_percent as f32,
|
||||
config.tank_full_percent as f32,
|
||||
),
|
||||
raw as f32,
|
||||
);
|
||||
})
|
||||
.and_then(|percent| {
|
||||
let left_ml = (percent * 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);
|
||||
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 tank_state = determine_tank_state(&mut board, &config);
|
||||
|
||||
let mut water_frozen = false;
|
||||
for _attempt in 0..5 {
|
||||
let water_temperature = board.water_temperature_c();
|
||||
match water_temperature {
|
||||
Ok(temp) => {
|
||||
if online_mode == OnlineMode::Online {
|
||||
let _ = board.mqtt_publish(
|
||||
&config,
|
||||
"/water/temperature",
|
||||
temp.to_string().as_bytes(),
|
||||
);
|
||||
}
|
||||
//FIXME mqtt here
|
||||
println!("Water temp is {}", temp);
|
||||
if temp < 4_f32 {
|
||||
@@ -552,6 +314,9 @@ fn safe_main() -> Result<()> {
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
if online_mode == OnlineMode::Online {
|
||||
let _ = board.mqtt_publish(&config, "/water/temperature", "Error".as_bytes());
|
||||
}
|
||||
println!("Could not get water temp {}", err)
|
||||
}
|
||||
}
|
||||
@@ -561,11 +326,11 @@ fn safe_main() -> Result<()> {
|
||||
..Default::default()
|
||||
}; PLANT_COUNT];
|
||||
let plant_to_pump = determine_next_plant(
|
||||
online_mode,
|
||||
&mut plantstate,
|
||||
europe_time,
|
||||
enough_water,
|
||||
&tank_state,
|
||||
water_frozen,
|
||||
tank_sensor_error,
|
||||
&config,
|
||||
&mut board,
|
||||
);
|
||||
@@ -602,12 +367,7 @@ fn safe_main() -> Result<()> {
|
||||
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;
|
||||
state.sensor_error_p = Some(err);
|
||||
}
|
||||
}
|
||||
if state.after_p.is_none()
|
||||
@@ -616,6 +376,7 @@ fn safe_main() -> Result<()> {
|
||||
{
|
||||
state.pump_error = true;
|
||||
board.fault(plant, true);
|
||||
//mqtt sync pump error value
|
||||
}
|
||||
}
|
||||
None => {
|
||||
@@ -632,37 +393,558 @@ fn safe_main() -> Result<()> {
|
||||
config.night_lamp_hour_start,
|
||||
config.night_lamp_hour_end,
|
||||
);
|
||||
|
||||
let state_of_charge = board.state_charge_percent().unwrap_or(0);
|
||||
if state_of_charge < 30 {
|
||||
board.set_low_voltage_in_cycle();
|
||||
} else if state_of_charge > 50 {
|
||||
board.clear_low_voltage_in_cycle();
|
||||
}
|
||||
light_state.battery_low = board.low_voltage_in_cycle();
|
||||
|
||||
if !light_state.out_of_work_hour {
|
||||
if config.night_lamp_only_when_dark {
|
||||
if !light_state.is_day {
|
||||
if light_state.battery_low {
|
||||
board.light(false).unwrap();
|
||||
} else {
|
||||
light_state.active = true;
|
||||
board.light(true).unwrap();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if light_state.battery_low {
|
||||
board.light(false).unwrap();
|
||||
} else {
|
||||
light_state.active = true;
|
||||
board.light(true).unwrap();
|
||||
}
|
||||
} else {
|
||||
light_state.active = true;
|
||||
board.light(true).unwrap();
|
||||
}
|
||||
} else {
|
||||
light_state.active = false;
|
||||
board.light(false).unwrap();
|
||||
}
|
||||
|
||||
println!("Lightstate is {:?}", light_state);
|
||||
|
||||
//check if during light time
|
||||
//lightstate += out of worktime
|
||||
//check battery level
|
||||
//lightstate += battery empty
|
||||
//check solar level if config requires
|
||||
//lightstate += stillday
|
||||
//if no preventing lightstate, enable light
|
||||
//lightstate = active
|
||||
if online_mode == OnlineMode::Online {
|
||||
match serde_json::to_string(&light_state) {
|
||||
Ok(state) => {
|
||||
let _ = board.mqtt_publish(&config, "/light/active", state.as_bytes());
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error publishing lightstate {}", err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
//relatch
|
||||
unsafe{gpio_deep_sleep_hold_dis()};
|
||||
unsafe { gpio_deep_sleep_hold_dis() };
|
||||
unsafe { gpio_deep_sleep_hold_en() };
|
||||
|
||||
//determine next event
|
||||
//is light out of work trigger soon?
|
||||
//is battery low ??
|
||||
//is deep sleep
|
||||
|
||||
unsafe { esp_deep_sleep(1000 * 1000 * 20) };
|
||||
}
|
||||
|
||||
fn publish_battery_state(
|
||||
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
|
||||
config: &Config,
|
||||
) {
|
||||
match board.voltage_milli_volt() {
|
||||
Ok(v) => {
|
||||
let _ = board.mqtt_publish(
|
||||
&config,
|
||||
"/battery/voltage_milli_volt",
|
||||
v.to_string().as_bytes(),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = board.mqtt_publish(&config, "/battery/voltage_milli_volt", "-1".as_bytes());
|
||||
let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
|
||||
}
|
||||
};
|
||||
match board.average_current_milli_ampere() {
|
||||
Ok(v) => {
|
||||
let _ = board.mqtt_publish(
|
||||
&config,
|
||||
"/battery/average_current_milli_ampere",
|
||||
v.to_string().as_bytes(),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = board.mqtt_publish(
|
||||
&config,
|
||||
"/battery/average_current_milli_ampere",
|
||||
"-1".as_bytes(),
|
||||
);
|
||||
let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
|
||||
}
|
||||
};
|
||||
match board.cycle_count() {
|
||||
Ok(v) => {
|
||||
let _ = board.mqtt_publish(&config, "/battery/cycle_count", v.to_string().as_bytes());
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = board.mqtt_publish(&config, "/battery/cycle_count", "-1".as_bytes());
|
||||
let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
|
||||
}
|
||||
};
|
||||
match board.design_milli_ampere_hour() {
|
||||
Ok(v) => {
|
||||
let _ = board.mqtt_publish(
|
||||
&config,
|
||||
"/battery/design_milli_ampere_hour",
|
||||
v.to_string().as_bytes(),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = board.mqtt_publish(
|
||||
&config,
|
||||
"/battery/design_milli_ampere_hour",
|
||||
"-1".as_bytes(),
|
||||
);
|
||||
let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
|
||||
}
|
||||
};
|
||||
match board.max_milli_ampere_hour() {
|
||||
Ok(v) => {
|
||||
let _ = board.mqtt_publish(
|
||||
&config,
|
||||
"/battery/max_milli_ampere_hour",
|
||||
v.to_string().as_bytes(),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = board.mqtt_publish(&config, "/battery/max_milli_ampere_hour", "-1".as_bytes());
|
||||
let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
|
||||
}
|
||||
};
|
||||
match board.remaining_milli_ampere_hour() {
|
||||
Ok(v) => {
|
||||
let _ = board.mqtt_publish(
|
||||
&config,
|
||||
"/battery/remaining_milli_ampere_hour",
|
||||
v.to_string().as_bytes(),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = board.mqtt_publish(
|
||||
&config,
|
||||
"/battery/remaining_milli_ampere_hour",
|
||||
"-1".as_bytes(),
|
||||
);
|
||||
let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
|
||||
}
|
||||
};
|
||||
match board.state_charge_percent() {
|
||||
Ok(v) => {
|
||||
let _ = board.mqtt_publish(
|
||||
&config,
|
||||
"/battery/state_charge_percent",
|
||||
v.to_string().as_bytes(),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = board.mqtt_publish(&config, "/battery/state_charge_percent", "-1".as_bytes());
|
||||
let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
|
||||
}
|
||||
};
|
||||
match board.state_health_percent() {
|
||||
Ok(v) => {
|
||||
let _ = board.mqtt_publish(
|
||||
&config,
|
||||
"/battery/state_health_percent",
|
||||
v.to_string().as_bytes(),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = board.mqtt_publish(&config, "/battery/state_health_percent", "-1".as_bytes());
|
||||
let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn determine_tank_state(
|
||||
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
|
||||
config: &Config,
|
||||
) -> TankState {
|
||||
if config.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_empty_percent as f32,
|
||||
config.tank_full_percent as f32,
|
||||
),
|
||||
raw as f32,
|
||||
);
|
||||
})
|
||||
.and_then(|percent| {
|
||||
rv.left_ml = (percent * config.tank_useable_ml as f32) as u32;
|
||||
println!(
|
||||
"Tank sensor returned mv {} as {}% leaving {} ml useable",
|
||||
rv.raw, percent as u8, rv.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
|
||||
);
|
||||
}
|
||||
if config.tank_empty_percent > percent as u8 {
|
||||
println!(
|
||||
"Empty water, current percent is {}, minimum empty level is {}",
|
||||
percent as u8, config.tank_empty_percent
|
||||
);
|
||||
rv.enough_water = false;
|
||||
}
|
||||
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 {
|
||||
enough_water: true,
|
||||
left_ml: 1337,
|
||||
sensor_error: false,
|
||||
raw: 0,
|
||||
};
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
if s > FROM.1 {
|
||||
return Err(SensorError::ShortCircuit { hz: s, max: FROM.1 });
|
||||
}
|
||||
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<Tz>, 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 option_to_string(value: Option<u8>) -> String {
|
||||
match value {
|
||||
Some(v) => v.to_string(),
|
||||
None => "Error".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn determine_state_target_moisture_for_plant(
|
||||
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
|
||||
plant: usize,
|
||||
state: &mut PlantState,
|
||||
config: &Config,
|
||||
tank_state: &TankState,
|
||||
water_frozen: bool,
|
||||
cur: DateTime<Tz>,
|
||||
) {
|
||||
let plant_config = &config.plants[plant];
|
||||
match board.measure_moisture_hz(plant, plant_hal::Sensor::A) {
|
||||
Ok(a) => {
|
||||
let mapped = map_range_moisture(a as f32);
|
||||
match mapped {
|
||||
Ok(result) => state.a = Some(result),
|
||||
Err(err) => {
|
||||
state.sensor_error_a = Some(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
if a_low || b_low {
|
||||
state.dry = true;
|
||||
if tank_state.sensor_error && !config.tank_allow_pumping_if_sensor_error
|
||||
|| !tank_state.enough_water
|
||||
{
|
||||
state.no_water = true;
|
||||
}
|
||||
}
|
||||
let duration = Duration::minutes((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 water_frozen {
|
||||
state.frozen = true;
|
||||
}
|
||||
if state.dry && !state.no_water && !state.cooldown && !state.out_of_work_hour {
|
||||
if water_frozen {
|
||||
state.frozen = true;
|
||||
} else {
|
||||
state.do_water = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn determine_next_plant(
|
||||
online_mode: OnlineMode,
|
||||
plantstate: &mut [PlantState; PLANT_COUNT],
|
||||
cur: DateTime<Tz>,
|
||||
tank_state: &TankState,
|
||||
water_frozen: bool,
|
||||
config: &Config,
|
||||
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
|
||||
) -> Option<usize> {
|
||||
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 => {
|
||||
determine_state_target_moisture_for_plant(
|
||||
board,
|
||||
plant,
|
||||
state,
|
||||
config,
|
||||
tank_state,
|
||||
water_frozen,
|
||||
cur,
|
||||
);
|
||||
}
|
||||
config::Mode::TimerOnly => {
|
||||
let duration = Duration::minutes((plant_config.pump_cooldown_min).into());
|
||||
let next_pump = board.last_pump_time(plant) + duration;
|
||||
if next_pump > cur {
|
||||
state.cooldown = true;
|
||||
} else {
|
||||
if water_frozen {
|
||||
state.frozen = 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 {
|
||||
if water_frozen {
|
||||
state.frozen = true;
|
||||
} else {
|
||||
state.do_water = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if state.sensor_error_a.is_some()
|
||||
|| state.sensor_error_b.is_some()
|
||||
|| state.sensor_error_p.is_some()
|
||||
{
|
||||
board.fault(plant, true);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
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 wait_infinity(wait_type: WaitType, reboot_now: Arc<AtomicBool>) -> ! {
|
||||
let delay = match wait_type {
|
||||
WaitType::InitialConfig => 250_u32,
|
||||
WaitType::FlashError => 100_u32,
|
||||
WaitType::NormalConfig => 500_u32,
|
||||
WaitType::StayAlive => 1000_u32,
|
||||
};
|
||||
let led_count = match wait_type {
|
||||
WaitType::InitialConfig => 8,
|
||||
WaitType::FlashError => 8,
|
||||
WaitType::NormalConfig => 4,
|
||||
WaitType::StayAlive => 2,
|
||||
};
|
||||
loop {
|
||||
unsafe {
|
||||
//do not trigger watchdog
|
||||
for i in 0..8 {
|
||||
BOARD_ACCESS.lock().unwrap().fault(i, i < led_count);
|
||||
}
|
||||
BOARD_ACCESS.lock().unwrap().general_fault(true);
|
||||
vTaskDelay(delay);
|
||||
BOARD_ACCESS.lock().unwrap().general_fault(false);
|
||||
for i in 0..8 {
|
||||
BOARD_ACCESS.lock().unwrap().fault(i, false);
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let result = safe_main();
|
||||
result.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user