move to central config, make TZ compile time const, confgureable later!
This commit is contained in:
336
rust/src/main.rs
336
rust/src/main.rs
@@ -18,18 +18,20 @@ use esp_idf_sys::{
|
||||
};
|
||||
use log::error;
|
||||
use once_cell::sync::Lazy;
|
||||
use plant_hal::{CreatePlantHal, PlantCtrlBoard, PlantCtrlBoardInteraction, PlantHal, PLANT_COUNT};
|
||||
use plant_hal::{PlantCtrlBoard, PlantHal, PLANT_COUNT};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
config::{Config, WifiConfig},
|
||||
config::Config,
|
||||
espota::{mark_app_valid, rollback_and_reboot},
|
||||
webserver::webserver::{httpd, httpd_initial},
|
||||
webserver::webserver::httpd,
|
||||
};
|
||||
mod config;
|
||||
pub mod espota;
|
||||
pub mod plant_hal;
|
||||
|
||||
const TIME_ZONE: Tz = Berlin;
|
||||
|
||||
const MOIST_SENSOR_MAX_FREQUENCY: u32 = 50000; // 60kHz (500Hz margin)
|
||||
const MOIST_SENSOR_MIN_FREQUENCY: u32 = 500; // 0.5kHz (500Hz margin)
|
||||
|
||||
@@ -46,21 +48,10 @@ mod webserver {
|
||||
pub mod webserver;
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
enum OnlineMode {
|
||||
Offline,
|
||||
Wifi,
|
||||
SnTp,
|
||||
Online,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
enum WaitType {
|
||||
InitialConfig,
|
||||
FlashError,
|
||||
NormalConfig,
|
||||
StayAlive,
|
||||
StayAliveBtn
|
||||
MissingConfig,
|
||||
Config,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
|
||||
@@ -208,20 +199,32 @@ fn safe_main() -> anyhow::Result<()> {
|
||||
free_space.total_size, free_space.used_size, free_space.free_size
|
||||
);
|
||||
|
||||
let time = board.time();
|
||||
let mut cur = match time {
|
||||
Ok(cur) => cur,
|
||||
let mut cur = match board.get_rtc_time() {
|
||||
Ok(time) => time,
|
||||
Err(err) => {
|
||||
log::error!("time error {}", err);
|
||||
DateTime::from_timestamp_millis(0).unwrap()
|
||||
println!("rtc module error: {}", err);
|
||||
board.general_fault(true);
|
||||
let time = board.time();
|
||||
match time {
|
||||
Ok(cur) => cur,
|
||||
Err(err) => {
|
||||
log::error!("time error {}", err);
|
||||
DateTime::from_timestamp_millis(0).unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//check if we know the time current > 2020
|
||||
if cur.year() < 2020 {
|
||||
println!("Running time estimation super fallback");
|
||||
if board.is_day() {
|
||||
//assume TZ safe times ;)
|
||||
|
||||
println!("Is day -> 15:00");
|
||||
cur = *cur.with_hour(15).get_or_insert(cur);
|
||||
} else {
|
||||
println!("Is night -> 3:00");
|
||||
cur = *cur.with_hour(3).get_or_insert(cur);
|
||||
}
|
||||
}
|
||||
@@ -229,9 +232,9 @@ fn safe_main() -> anyhow::Result<()> {
|
||||
println!("cur is {}", cur);
|
||||
|
||||
let mut to_config = false;
|
||||
if board.is_config_reset() {
|
||||
if board.is_mode_override() {
|
||||
board.general_fault(true);
|
||||
println!("Reset config is pressed, waiting 5s");
|
||||
println!("config mode override is pressed, waiting 5s");
|
||||
for _i in 0..5 {
|
||||
board.general_fault(true);
|
||||
Delay::new_default().delay_ms(100);
|
||||
@@ -239,133 +242,88 @@ fn safe_main() -> anyhow::Result<()> {
|
||||
Delay::new_default().delay_ms(100);
|
||||
}
|
||||
|
||||
if board.is_config_reset() {
|
||||
if board.is_mode_override() {
|
||||
to_config = true;
|
||||
println!("Reset config is still pressed, proceed to config mode");
|
||||
for _i in 0..25 {
|
||||
board.general_fault(true);
|
||||
Delay::new_default().delay_ms(25);
|
||||
board.general_fault(false);
|
||||
Delay::new_default().delay_ms(25);
|
||||
}
|
||||
if board.is_config_reset() {
|
||||
println!("Reset config is still pressed, proceed to delete configs");
|
||||
match board.remove_configs() {
|
||||
Ok(case) => {
|
||||
println!("Succeeded in deleting config {}", case);
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Could not remove config files, system borked {}", err);
|
||||
//terminate main app and freeze
|
||||
|
||||
wait_infinity(WaitType::FlashError, Arc::new(AtomicBool::new(false)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
}
|
||||
} else {
|
||||
board.general_fault(false);
|
||||
}
|
||||
}
|
||||
|
||||
let mut online_mode = OnlineMode::Offline;
|
||||
let wifi_conf = board.get_wifi();
|
||||
let wifi: WifiConfig;
|
||||
match wifi_conf {
|
||||
Ok(conf) => {
|
||||
wifi = conf;
|
||||
}
|
||||
Err(err) => {
|
||||
if board.is_wifi_config_file_existant() {
|
||||
if ota_state == esp_ota_img_states_t_ESP_OTA_IMG_PENDING_VERIFY {
|
||||
println!("Config seem to be unparsable after upgrade, reverting");
|
||||
rollback_and_reboot()?;
|
||||
}
|
||||
}
|
||||
println!("Missing wifi config, entering initial config mode {}", err);
|
||||
board.wifi_ap().unwrap();
|
||||
//config upload will trigger reboot!
|
||||
drop(board);
|
||||
let reboot_now = Arc::new(AtomicBool::new(false));
|
||||
let _webserver = httpd_initial(reboot_now.clone());
|
||||
wait_infinity(WaitType::InitialConfig, reboot_now.clone());
|
||||
}
|
||||
};
|
||||
|
||||
println!("attempting to connect wifi");
|
||||
let mut ip_address: Option<String> = None;
|
||||
match board.wifi(wifi.ssid, wifi.password, 10000) {
|
||||
Ok(ip_info) => {
|
||||
ip_address = Some(ip_info.ip.to_string());
|
||||
online_mode = OnlineMode::Wifi;
|
||||
}
|
||||
Err(_) => {
|
||||
println!("Offline mode");
|
||||
board.general_fault(true);
|
||||
}
|
||||
}
|
||||
|
||||
if online_mode == OnlineMode::Wifi {
|
||||
match board.sntp(1000 * 5) {
|
||||
Ok(new_time) => {
|
||||
cur = new_time;
|
||||
online_mode = OnlineMode::SnTp;
|
||||
}
|
||||
Err(err) => {
|
||||
println!("sntp error: {}", err);
|
||||
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);
|
||||
|
||||
let config: Config;
|
||||
match board.get_config() {
|
||||
match board.get_config() {
|
||||
Ok(valid) => {
|
||||
config = valid;
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Missing normal config, entering config mode {}", err);
|
||||
//config upload will trigger reboot!
|
||||
let _ = board.wifi_ap();
|
||||
drop(board);
|
||||
let reboot_now = Arc::new(AtomicBool::new(false));
|
||||
let _webserver = httpd(reboot_now.clone());
|
||||
wait_infinity(WaitType::NormalConfig, reboot_now.clone());
|
||||
wait_infinity(WaitType::MissingConfig, reboot_now.clone());
|
||||
}
|
||||
}
|
||||
|
||||
//do mqtt before config check, as mqtt might configure
|
||||
if online_mode == OnlineMode::SnTp {
|
||||
match board.mqtt(&config) {
|
||||
Ok(_) => {
|
||||
println!("Mqtt connection ready");
|
||||
online_mode = OnlineMode::Online;
|
||||
let mut wifi = false;
|
||||
let mut mqtt = false;
|
||||
let mut sntp = false;
|
||||
println!("attempting to connect wifi");
|
||||
let mut ip_address: Option<String> = None;
|
||||
if config.ssid.is_some() {
|
||||
match board.wifi(config.ssid.clone().unwrap(), config.password.clone(), 10000) {
|
||||
Ok(ip_info) => {
|
||||
ip_address = Some(ip_info.ip.to_string());
|
||||
wifi = true;
|
||||
|
||||
match board.sntp(1000 * 5) {
|
||||
Ok(new_time) => {
|
||||
println!("Using time from sntp");
|
||||
let _ = board.set_rtc_time(&new_time);
|
||||
cur = new_time;
|
||||
sntp = true;
|
||||
}
|
||||
Err(err) => {
|
||||
println!("sntp error: {}", err);
|
||||
board.general_fault(true);
|
||||
}
|
||||
}
|
||||
match board.mqtt(&config) {
|
||||
Ok(_) => {
|
||||
println!("Mqtt connection ready");
|
||||
mqtt = true;
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Could not connect mqtt due to {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Could not connect mqtt due to {}", err);
|
||||
Err(_) => {
|
||||
println!("Offline mode");
|
||||
board.general_fault(true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("No wifi configured");
|
||||
}
|
||||
|
||||
if online_mode == OnlineMode::Online {
|
||||
match ip_address {
|
||||
Some(add_some) => {
|
||||
let _ = board.mqtt_publish(&config, "/firmware/address", add_some.as_bytes());
|
||||
}
|
||||
None => {
|
||||
let _ = board.mqtt_publish(&config, "/firmware/address", "N/A?".as_bytes());
|
||||
}
|
||||
}
|
||||
let timezone_time = cur.with_timezone(&TIME_ZONE);
|
||||
println!(
|
||||
"Running logic at utc {} and {} {}",
|
||||
cur,
|
||||
TIME_ZONE.name(),
|
||||
timezone_time
|
||||
);
|
||||
|
||||
if mqtt {
|
||||
let ip_string = ip_address.unwrap_or("N/A".to_owned());
|
||||
let _ = board.mqtt_publish(&config, "/firmware/address", ip_string.as_bytes());
|
||||
let _ = board.mqtt_publish(&config, "/firmware/githash", git_hash.as_bytes());
|
||||
let _ = board.mqtt_publish(&config, "/firmware/buildtime", build_timestamp.as_bytes());
|
||||
let _ = board.mqtt_publish(
|
||||
&config,
|
||||
"/firmware/last_online",
|
||||
europe_time.to_rfc3339().as_bytes(),
|
||||
timezone_time.to_rfc3339().as_bytes(),
|
||||
);
|
||||
let _ = board.mqtt_publish(&config, "/firmware/ota_state", ota_state_string.as_bytes());
|
||||
let _ = board.mqtt_publish(
|
||||
@@ -378,6 +336,18 @@ fn safe_main() -> anyhow::Result<()> {
|
||||
publish_battery_state(&mut board, &config);
|
||||
}
|
||||
|
||||
println!("startup state wifi {} sntp {} mqtt {}", wifi, sntp, mqtt);
|
||||
|
||||
if to_config {
|
||||
//check if client or ap mode and init wifi
|
||||
println!("executing config mode override");
|
||||
//config upload will trigger reboot!
|
||||
drop(board);
|
||||
let reboot_now = Arc::new(AtomicBool::new(false));
|
||||
let _webserver = httpd(reboot_now.clone());
|
||||
wait_infinity(WaitType::Config, reboot_now.clone());
|
||||
}
|
||||
|
||||
let tank_state = determine_tank_state(&mut board, &config);
|
||||
let mut tank_state_mqtt = TankStateMQTT {
|
||||
enough_water: tank_state.enough_water,
|
||||
@@ -413,30 +383,27 @@ fn safe_main() -> anyhow::Result<()> {
|
||||
None => tank_state_mqtt.water_frozen = "tank sensor error".to_owned(),
|
||||
}
|
||||
|
||||
if online_mode == OnlineMode::Online {
|
||||
match serde_json::to_string(&tank_state_mqtt) {
|
||||
Ok(state) => {
|
||||
let _ = board.mqtt_publish(&config, "/water", state.as_bytes());
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error publishing tankstate {}", err);
|
||||
}
|
||||
};
|
||||
}
|
||||
match serde_json::to_string(&tank_state_mqtt) {
|
||||
Ok(state) => {
|
||||
let _ = board.mqtt_publish(&config, "/water", state.as_bytes());
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error publishing tankstate {}", err);
|
||||
}
|
||||
};
|
||||
|
||||
let mut plantstate: [PlantState; PLANT_COUNT] = core::array::from_fn(|_| PlantState {
|
||||
..Default::default()
|
||||
});
|
||||
let plant_to_pump = determine_next_plant(
|
||||
&mut plantstate,
|
||||
europe_time,
|
||||
timezone_time,
|
||||
&tank_state,
|
||||
water_frozen,
|
||||
&config,
|
||||
&mut board,
|
||||
);
|
||||
|
||||
|
||||
let stay_alive_mqtt = STAY_ALIVE.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let stay_alive = stay_alive_mqtt;
|
||||
println!("Check stay alive, current state is {}", stay_alive);
|
||||
@@ -452,7 +419,7 @@ fn safe_main() -> anyhow::Result<()> {
|
||||
board.fault(plant, true);
|
||||
}
|
||||
|
||||
let plant_config = config.plants[plant];
|
||||
let plant_config = &config.plants[plant];
|
||||
|
||||
println!(
|
||||
"Trying to pump for {}s with pump {} now",
|
||||
@@ -477,16 +444,15 @@ fn safe_main() -> anyhow::Result<()> {
|
||||
println!("Nothing to do");
|
||||
}
|
||||
}
|
||||
if online_mode == OnlineMode::Online {
|
||||
update_plant_state(&mut plantstate, &mut board, &config);
|
||||
}
|
||||
update_plant_state(&mut plantstate, &mut board, &config);
|
||||
|
||||
let mut light_state = LightState {
|
||||
..Default::default()
|
||||
};
|
||||
let is_day = board.is_day();
|
||||
light_state.is_day = is_day;
|
||||
light_state.out_of_work_hour = !in_time_range(
|
||||
&europe_time,
|
||||
&timezone_time,
|
||||
config.night_lamp_hour_start,
|
||||
config.night_lamp_hour_end,
|
||||
);
|
||||
@@ -524,43 +490,32 @@ fn safe_main() -> anyhow::Result<()> {
|
||||
|
||||
println!("Lightstate is {:?}", light_state);
|
||||
|
||||
if online_mode == OnlineMode::Online {
|
||||
match serde_json::to_string(&light_state) {
|
||||
Ok(state) => {
|
||||
let _ = board.mqtt_publish(&config, "/light", state.as_bytes());
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error publishing lightstate {}", err);
|
||||
}
|
||||
};
|
||||
}
|
||||
match serde_json::to_string(&light_state) {
|
||||
Ok(state) => {
|
||||
let _ = board.mqtt_publish(&config, "/light", state.as_bytes());
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error publishing lightstate {}", err);
|
||||
}
|
||||
};
|
||||
|
||||
let deep_sleep_duration_minutes: u32 = if state_of_charge < 10 {
|
||||
if online_mode == OnlineMode::Online {
|
||||
let _ = board.mqtt_publish(&config, "/deepsleep", "low Volt 12h".as_bytes());
|
||||
}
|
||||
let _ = board.mqtt_publish(&config, "/deepsleep", "low Volt 12h".as_bytes());
|
||||
12 * 60
|
||||
} else if is_day {
|
||||
if did_pump {
|
||||
if online_mode == OnlineMode::Online {
|
||||
let _ = board.mqtt_publish(&config, "/deepsleep", "after pump".as_bytes());
|
||||
}
|
||||
let _ = board.mqtt_publish(&config, "/deepsleep", "after pump".as_bytes());
|
||||
0
|
||||
} else {
|
||||
if online_mode == OnlineMode::Online {
|
||||
let _ = board.mqtt_publish(&config, "/deepsleep", "normal 20m".as_bytes());
|
||||
}
|
||||
let _ = board.mqtt_publish(&config, "/deepsleep", "normal 20m".as_bytes());
|
||||
|
||||
20
|
||||
}
|
||||
} else {
|
||||
if online_mode == OnlineMode::Online {
|
||||
let _ = board.mqtt_publish(&config, "/deepsleep", "night 1h".as_bytes());
|
||||
}
|
||||
let _ = board.mqtt_publish(&config, "/deepsleep", "night 1h".as_bytes());
|
||||
60
|
||||
};
|
||||
if online_mode == OnlineMode::Online {
|
||||
let _ = board.mqtt_publish(&config, "/state", "sleep".as_bytes());
|
||||
}
|
||||
let _ = board.mqtt_publish(&config, "/state", "sleep".as_bytes());
|
||||
|
||||
//determine next event
|
||||
//is light out of work trigger soon?
|
||||
@@ -568,19 +523,12 @@ fn safe_main() -> anyhow::Result<()> {
|
||||
//is deep sleep
|
||||
mark_app_valid();
|
||||
|
||||
if to_config {
|
||||
println!("Go to button triggerd stay alive");
|
||||
drop(board);
|
||||
let reboot_now = Arc::new(AtomicBool::new(false));
|
||||
let _webserver = httpd(reboot_now.clone());
|
||||
wait_infinity(WaitType::StayAliveBtn, reboot_now.clone());
|
||||
}
|
||||
if stay_alive {
|
||||
println!("Go to stay alive move");
|
||||
drop(board);
|
||||
let reboot_now = Arc::new(AtomicBool::new(false));
|
||||
let _webserver = httpd(reboot_now.clone());
|
||||
wait_infinity(WaitType::StayAlive, reboot_now.clone());
|
||||
wait_infinity(WaitType::Config, reboot_now.clone());
|
||||
}
|
||||
|
||||
unsafe { esp_deep_sleep(1000 * 1000 * 60 * deep_sleep_duration_minutes as u64) };
|
||||
@@ -734,8 +682,8 @@ fn determine_state_target_moisture_for_plant(
|
||||
Some(last_pump) => {
|
||||
let next_pump = last_pump + duration;
|
||||
if next_pump > cur {
|
||||
let europe_time = next_pump.with_timezone(&Berlin);
|
||||
state.next_pump = Some(europe_time);
|
||||
let local_time = next_pump.with_timezone(&TIME_ZONE);
|
||||
state.next_pump = Some(local_time);
|
||||
state.cooldown = true;
|
||||
}
|
||||
}
|
||||
@@ -777,7 +725,7 @@ fn determine_state_timer_only_for_plant(
|
||||
Some(last_pump) => {
|
||||
let next_pump = last_pump + duration;
|
||||
if next_pump > cur {
|
||||
let europe_time = next_pump.with_timezone(&Berlin);
|
||||
let europe_time = next_pump.with_timezone(&TIME_ZONE);
|
||||
state.next_pump = Some(europe_time);
|
||||
state.cooldown = true;
|
||||
} else {
|
||||
@@ -815,7 +763,7 @@ fn determine_state_timer_and_deadzone_for_plant(
|
||||
Some(last_pump) => {
|
||||
let next_pump = last_pump + duration;
|
||||
if next_pump > cur {
|
||||
let europe_time = next_pump.with_timezone(&Berlin);
|
||||
let europe_time = next_pump.with_timezone(&TIME_ZONE);
|
||||
state.next_pump = Some(europe_time);
|
||||
state.cooldown = true;
|
||||
}
|
||||
@@ -905,7 +853,7 @@ fn update_plant_state(
|
||||
) {
|
||||
for plant in 0..PLANT_COUNT {
|
||||
let state = &plantstate[plant];
|
||||
let plant_config = config.plants[plant];
|
||||
let plant_config = &config.plants[plant];
|
||||
|
||||
let mode = format!("{:?}", plant_config.mode);
|
||||
|
||||
@@ -946,20 +894,17 @@ fn update_plant_state(
|
||||
|
||||
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,
|
||||
WaitType::StayAliveBtn => 25_u32
|
||||
};
|
||||
let led_count = match wait_type {
|
||||
WaitType::InitialConfig => 8,
|
||||
WaitType::FlashError => 8,
|
||||
WaitType::NormalConfig => 4,
|
||||
WaitType::StayAlive => 2,
|
||||
WaitType::StayAliveBtn => 5
|
||||
WaitType::MissingConfig => 500_u32,
|
||||
WaitType::Config => 100_u32,
|
||||
};
|
||||
let mut led_count = 8;
|
||||
loop {
|
||||
if wait_type == WaitType::MissingConfig {
|
||||
led_count = led_count + 1;
|
||||
if led_count > 8 {
|
||||
led_count = 1;
|
||||
}
|
||||
};
|
||||
unsafe {
|
||||
//do not trigger watchdog
|
||||
for i in 0..8 {
|
||||
@@ -972,10 +917,13 @@ fn wait_infinity(wait_type: WaitType, reboot_now: Arc<AtomicBool>) -> ! {
|
||||
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);
|
||||
match wait_type {
|
||||
WaitType::MissingConfig => {}
|
||||
WaitType::Config => {
|
||||
if !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");
|
||||
@@ -1002,14 +950,14 @@ fn main() {
|
||||
}
|
||||
|
||||
fn time_to_string_utc(value_option: Option<DateTime<Utc>>) -> String {
|
||||
let converted = value_option.and_then(|utc| Some(utc.with_timezone(&Berlin)));
|
||||
let converted = value_option.and_then(|utc| Some(utc.with_timezone(&TIME_ZONE)));
|
||||
return time_to_string(converted);
|
||||
}
|
||||
|
||||
fn time_to_string(value_option: Option<DateTime<Tz>>) -> String {
|
||||
match value_option {
|
||||
Some(value) => {
|
||||
let europe_time = value.with_timezone(&Berlin);
|
||||
let europe_time = value.with_timezone(&TIME_ZONE);
|
||||
if europe_time.year() > 2023 {
|
||||
return europe_time.to_rfc3339();
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user