it's alive

This commit is contained in:
2025-09-13 01:39:47 +02:00
parent 79087c9353
commit 9de85b6e37
19 changed files with 1567 additions and 1488 deletions

View File

@@ -5,6 +5,10 @@
reason = "mem::forget is generally not safe to do with esp_hal types, especially those \
holding buffers for the duration of a data transfer."
)]
esp_bootloader_esp_idf::esp_app_desc!();
use esp_backtrace as _;
use crate::config::PlantConfig;
use crate::{
config::BoardVersion::INITIAL,
@@ -16,24 +20,25 @@ use alloc::borrow::ToOwned;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::{format, vec};
use core::any::Any;
use anyhow::{bail, Context};
use chrono::{DateTime, Datelike, Timelike, Utc};
use chrono_tz::Tz::{self, UTC};
use chrono_tz::Tz::{self};
use core::sync::atomic::Ordering;
use embassy_executor::Spawner;
use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex};
use embassy_sync::lazy_lock::LazyLock;
use embassy_sync::mutex::Mutex;
use embassy_sync::mutex::MutexGuard;
use embassy_time::Timer;
use esp_hal::{clock::CpuClock, delay::Delay, timer::systimer::SystemTimer};
use esp_println::logger;
use esp_println::{logger, println};
use hal::battery::BatteryState;
use log::{log, LogMessage};
use plant_state::PlantState;
use portable_atomic::AtomicBool;
use serde::{Deserialize, Serialize};
use tank::*;
use crate::tank::{TankError, WATER_FROZEN_THRESH};
//use tank::*;
mod config;
mod hal;
mod log;
@@ -44,7 +49,9 @@ extern crate alloc;
//mod webserver;
pub static BOARD_ACCESS: LazyLock<Mutex<CriticalSectionRawMutex, HAL>> =
LazyLock::new(|| PlantHal::create().unwrap());
LazyLock::new(|| {
PlantHal::create().unwrap()
});
pub static STAY_ALIVE: AtomicBool = AtomicBool::new(false);
#[derive(Serialize, Deserialize, Debug, PartialEq)]
@@ -163,10 +170,10 @@ async fn safe_main() -> anyhow::Result<()> {
//};
//log(LogMessage::PartitionState, 0, 0, "", ota_state_string);
let ota_state_string = "unknown";
println!("faul led");
let mut board = BOARD_ACCESS.get().lock().await;
board.board_hal.general_fault(false).await;
println!("faul led2");
let cur = board
.board_hal
.get_rtc_module()
@@ -204,7 +211,7 @@ async fn safe_main() -> anyhow::Result<()> {
board.board_hal.get_esp().set_restart_to_conf(false);
} else if board.board_hal.get_esp().mode_override_pressed() {
board.board_hal.general_fault(true).await;
log(LogMessage::ConfigModeButtonOverride, 0, 0, "", "");
log(LogMessage::ConfigModeButtonOverride, 0, 0, "", "").await;
for _i in 0..5 {
board.board_hal.general_fault(true).await;
Timer::after_millis(100).await;
@@ -251,19 +258,21 @@ async fn safe_main() -> anyhow::Result<()> {
}
}
let timezone = match &board.board_hal.get_config().timezone {
Some(tz_str) => tz_str.parse::<Tz>().unwrap_or_else(|_| {
info!("Invalid timezone '{}', falling back to UTC", tz_str);
UTC
}),
None => UTC, // Fallback to UTC if no timezone is set
};
let timezone_time = cur.with_timezone(&timezone);
// let timezone = match &board.board_hal.get_config().timezone {
// Some(tz_str) => tz_str.parse::<Tz>().unwrap_or_else(|_| {
// info!("Invalid timezone '{}', falling back to UTC", tz_str);
// UTC
// }),
// None => UTC, // Fallback to UTC if no timezone is set
// };
let timezone = Tz::UTC;
let timezone_time = cur;//TODO.with_timezone(&timezone);
info!(
"Running logic at utc {} and {} {}",
cur,
timezone.name(),
"todo timezone.name()",
timezone_time
);
@@ -273,7 +282,7 @@ async fn safe_main() -> anyhow::Result<()> {
partition_address,
ota_state_string,
ip_address,
timezone_time,
&timezone_time.to_rfc3339(),
)
.await;
publish_battery_state().await;
@@ -294,9 +303,10 @@ async fn safe_main() -> anyhow::Result<()> {
.to_string()
.as_str(),
"",
);
).await;
drop(board);
//TODO must drop board here?
//drop(board);
if to_config {
//check if client or ap mode and init Wi-Fi
@@ -307,52 +317,54 @@ async fn safe_main() -> anyhow::Result<()> {
//let _webserver = httpd(reboot_now.clone());
wait_infinity(WaitType::ConfigButton, reboot_now.clone()).await;
} else {
log(LogMessage::NormalRun, 0, 0, "", "");
log(LogMessage::NormalRun, 0, 0, "", "").await;
}
let dry_run = false;
let tank_state = determine_tank_state(&mut board);
if tank_state.is_enabled() {
if let Some(err) = tank_state.got_error(&board.board_hal.get_config().tank) {
match err {
TankError::SensorDisabled => { /* unreachable */ }
TankError::SensorMissing(raw_value_mv) => log(
LogMessage::TankSensorMissing,
raw_value_mv as u32,
0,
"",
"",
),
TankError::SensorValueError { value, min, max } => log(
LogMessage::TankSensorValueRangeError,
min as u32,
max as u32,
&format!("{}", value),
"",
),
TankError::BoardError(err) => {
log(LogMessage::TankSensorBoardError, 0, 0, "", &err.to_string())
}
}
// disabled cannot trigger this because of wrapping if is_enabled
board.board_hal.general_fault(true);
} else if tank_state
.warn_level(&board.board_hal.get_config().tank)
.is_ok_and(|warn| warn)
{
log(LogMessage::TankWaterLevelLow, 0, 0, "", "");
board.board_hal.general_fault(true);
}
}
//
// let tank_state = determine_tank_state(&mut board);
//
// if tank_state.is_enabled() {
// if let Some(err) = tank_state.got_error(&board.board_hal.get_config().tank) {
// match err {
// TankError::SensorDisabled => { /* unreachable */ }
// TankError::SensorMissing(raw_value_mv) => log(
// LogMessage::TankSensorMissing,
// raw_value_mv as u32,
// 0,
// "",
// "",
// ).await,
// TankError::SensorValueError { value, min, max } => log(
// LogMessage::TankSensorValueRangeError,
// min as u32,
// max as u32,
// &format!("{}", value),
// "",
// ).await,
// TankError::BoardError(err) => {
// log(LogMessage::TankSensorBoardError, 0, 0, "", &err.to_string()).await
// }
// }
// // disabled cannot trigger this because of wrapping if is_enabled
// board.board_hal.general_fault(true).await;
// } else if tank_state
// .warn_level(&board.board_hal.get_config().tank)
// .is_ok_and(|warn| warn)
// {
// log(LogMessage::TankWaterLevelLow, 0, 0, "", "").await;
// board.board_hal.general_fault(true).await;
// }
// }
let mut water_frozen = false;
let water_temp = board
.board_hal
.get_tank_sensor()
.context("no sensor")
.and_then(async |f| f.water_temperature_c().await);
//TODO
let water_temp = anyhow::Ok(12_f32);
// board
// .board_hal
// .get_tank_sensor()
// .context("no sensor")
// .and_then(async |f| f.water_temperature_c().await);
if let Ok(res) = water_temp {
if res < WATER_FROZEN_THRESH {
@@ -360,80 +372,89 @@ async fn safe_main() -> anyhow::Result<()> {
}
}
publish_tank_state(&tank_state, &water_temp);
//publish_tank_state(&tank_state, &water_temp).await;
let plantstate: [PlantState; PLANT_COUNT] =
core::array::from_fn(|i| PlantState::read_hardware_state(i, &mut board).await);
publish_plant_states(&timezone_time, &plantstate).await;
let pump_required = plantstate
.iter()
.zip(&board.board_hal.get_config().plants)
.any(|(it, conf)| it.needs_to_be_watered(conf, &timezone_time))
&& !water_frozen;
if pump_required {
log(LogMessage::EnableMain, dry_run as u32, 0, "", "");
for (plant_id, (state, plant_config)) in plantstate
.iter()
.zip(&board.board_hal.get_config().plants.clone())
.enumerate()
{
if state.needs_to_be_watered(plant_config, &timezone_time) {
let pump_count = board.board_hal.get_esp().consecutive_pump_count(plant_id) + 1;
board
.board_hal
.get_esp()
.store_consecutive_pump_count(plant_id, pump_count);
let plantstate: [PlantState; PLANT_COUNT] = [
PlantState::read_hardware_state(0, &mut board).await,
PlantState::read_hardware_state(1, &mut board).await,
PlantState::read_hardware_state(2, &mut board).await,
PlantState::read_hardware_state(3, &mut board).await,
PlantState::read_hardware_state(4, &mut board).await,
PlantState::read_hardware_state(5, &mut board).await,
PlantState::read_hardware_state(6, &mut board).await,
PlantState::read_hardware_state(7, &mut board).await,
];
//publish_plant_states(&timezone_time.clone(), &plantstate).await;
let pump_ineffective = pump_count > plant_config.max_consecutive_pump_count as u32;
if pump_ineffective {
log(
LogMessage::ConsecutivePumpCountLimit,
pump_count,
plant_config.max_consecutive_pump_count as u32,
&(plant_id + 1).to_string(),
"",
);
board.board_hal.fault(plant_id, true).await?;
}
log(
LogMessage::PumpPlant,
(plant_id + 1) as u32,
plant_config.pump_time_s as u32,
&dry_run.to_string(),
"",
);
board
.board_hal
.get_esp()
.store_last_pump_time(plant_id, cur);
board.board_hal.get_esp().last_pump_time(plant_id);
//state.active = true;
pump_info(plant_id, true, pump_ineffective, 0, 0, 0, false).await;
let result = do_secure_pump(plant_id, plant_config, dry_run).await?;
board.board_hal.pump(plant_id, false).await?;
pump_info(
plant_id,
false,
pump_ineffective,
result.median_current_ma,
result.max_current_ma,
result.min_current_ma,
result.error,
)
.await;
} else if !state.pump_in_timeout(plant_config, &timezone_time) {
// plant does not need to be watered and is not in timeout
// -> reset consecutive pump count
board
.board_hal
.get_esp()
.store_consecutive_pump_count(plant_id, 0);
}
}
}
// let pump_required = plantstate
// .iter()
// .zip(&board.board_hal.get_config().plants)
// .any(|(it, conf)| it.needs_to_be_watered(conf, &timezone_time))
// && !water_frozen;
// if pump_required {
// log(LogMessage::EnableMain, dry_run as u32, 0, "", "");
// for (plant_id, (state, plant_config)) in plantstate
// .iter()
// .zip(&board.board_hal.get_config().plants.clone())
// .enumerate()
// {
// if state.needs_to_be_watered(plant_config, &timezone_time) {
// let pump_count = board.board_hal.get_esp().consecutive_pump_count(plant_id) + 1;
// board
// .board_hal
// .get_esp()
// .store_consecutive_pump_count(plant_id, pump_count);
//
// let pump_ineffective = pump_count > plant_config.max_consecutive_pump_count as u32;
// if pump_ineffective {
// log(
// LogMessage::ConsecutivePumpCountLimit,
// pump_count,
// plant_config.max_consecutive_pump_count as u32,
// &(plant_id + 1).to_string(),
// "",
// );
// board.board_hal.fault(plant_id, true).await?;
// }
// log(
// LogMessage::PumpPlant,
// (plant_id + 1) as u32,
// plant_config.pump_time_s as u32,
// &dry_run.to_string(),
// "",
// );
// board
// .board_hal
// .get_esp()
// .store_last_pump_time(plant_id, cur);
// board.board_hal.get_esp().last_pump_time(plant_id);
// //state.active = true;
//
// pump_info(plant_id, true, pump_ineffective, 0, 0, 0, false).await;
//
// let result = do_secure_pump(plant_id, plant_config, dry_run).await?;
// board.board_hal.pump(plant_id, false).await?;
// pump_info(
// plant_id,
// false,
// pump_ineffective,
// result.median_current_ma,
// result.max_current_ma,
// result.min_current_ma,
// result.error,
// )
// .await;
// } else if !state.pump_in_timeout(plant_config, &timezone_time) {
// // plant does not need to be watered and is not in timeout
// // -> reset consecutive pump count
// board
// .board_hal
// .get_esp()
// .store_consecutive_pump_count(plant_id, 0);
// }
// }
// }
let is_day = board.board_hal.is_day();
let state_of_charge = board
@@ -455,67 +476,67 @@ async fn safe_main() -> anyhow::Result<()> {
enabled: board.board_hal.get_config().night_lamp.enabled,
..Default::default()
};
if light_state.enabled {
light_state.is_day = is_day;
light_state.out_of_work_hour = !in_time_range(
&timezone_time,
board
.board_hal
.get_config()
.night_lamp
.night_lamp_hour_start,
board.board_hal.get_config().night_lamp.night_lamp_hour_end,
);
if state_of_charge
< board
.board_hal
.get_config()
.night_lamp
.low_soc_cutoff
.into()
{
board.board_hal.get_esp().set_low_voltage_in_cycle();
} else if state_of_charge
> board
.board_hal
.get_config()
.night_lamp
.low_soc_restore
.into()
{
board.board_hal.get_esp().clear_low_voltage_in_cycle();
}
light_state.battery_low = board.board_hal.get_esp().low_voltage_in_cycle();
if !light_state.out_of_work_hour {
if board
.board_hal
.get_config()
.night_lamp
.night_lamp_only_when_dark
{
if !light_state.is_day {
if light_state.battery_low {
board.board_hal.light(false)?;
} else {
light_state.active = true;
board.board_hal.light(true)?;
}
}
} else if light_state.battery_low {
board.board_hal.light(false)?;
} else {
light_state.active = true;
board.board_hal.light(true)?;
}
} else {
light_state.active = false;
board.board_hal.light(false)?;
}
info!("Lightstate is {:?}", light_state);
}
// if light_state.enabled {
// light_state.is_day = is_day;
// light_state.out_of_work_hour = !in_time_range(
// &timezone_time,
// board
// .board_hal
// .get_config()
// .night_lamp
// .night_lamp_hour_start,
// board.board_hal.get_config().night_lamp.night_lamp_hour_end,
// );
//
// if state_of_charge
// < board
// .board_hal
// .get_config()
// .night_lamp
// .low_soc_cutoff
// .into()
// {
// board.board_hal.get_esp().set_low_voltage_in_cycle();
// } else if state_of_charge
// > board
// .board_hal
// .get_config()
// .night_lamp
// .low_soc_restore
// .into()
// {
// board.board_hal.get_esp().clear_low_voltage_in_cycle();
// }
// light_state.battery_low = board.board_hal.get_esp().low_voltage_in_cycle();
//
// if !light_state.out_of_work_hour {
// if board
// .board_hal
// .get_config()
// .night_lamp
// .night_lamp_only_when_dark
// {
// if !light_state.is_day {
// if light_state.battery_low {
// board.board_hal.light(false)?;
// } else {
// light_state.active = true;
// board.board_hal.light(true)?;
// }
// }
// } else if light_state.battery_low {
// board.board_hal.light(false)?;
// } else {
// light_state.active = true;
// board.board_hal.light(true)?;
// }
// } else {
// light_state.active = false;
// board.board_hal.light(false)?;
// }
//
// info!("Lightstate is {:?}", light_state);
// }
match serde_json::to_string(&light_state) {
Ok(state) => {
@@ -596,24 +617,25 @@ pub async fn do_secure_pump(
let mut pump_time_s = 0;
let board = &mut BOARD_ACCESS.get().lock().await;
if !dry_run {
board
.board_hal
.get_tank_sensor()
.unwrap()
.reset_flow_meter();
board
.board_hal
.get_tank_sensor()
.unwrap()
.start_flow_meter();
// board
// .board_hal
// .get_tank_sensor()
// .unwrap()
// .reset_flow_meter();
// board
// .board_hal
// .get_tank_sensor()
// .unwrap()
// .start_flow_meter();
board.board_hal.pump(plant_id, true).await?;
Timer::after_millis(10).await;
for step in 0..plant_config.pump_time_s as usize {
let flow_value = board
.board_hal
.get_tank_sensor()
.unwrap()
.get_flow_meter_value();
// let flow_value = board
// .board_hal
// .get_tank_sensor()
// .unwrap()
// .get_flow_meter_value();
let flow_value = 1;
flow_collector[step] = flow_value;
let flow_value_ml = flow_value as f32 * board.board_hal.get_config().tank.ml_per_pulse;
@@ -691,12 +713,13 @@ pub async fn do_secure_pump(
pump_time_s += 1;
}
}
board.board_hal.get_tank_sensor().unwrap().stop_flow_meter();
let final_flow_value = board
.board_hal
.get_tank_sensor()
.unwrap()
.get_flow_meter_value();
// board.board_hal.get_tank_sensor().unwrap().stop_flow_meter();
// let final_flow_value = board
// .board_hal
// .get_tank_sensor()
// .unwrap()
// .get_flow_meter_value();
let final_flow_value = 12;
let flow_value_ml = final_flow_value as f32 * board.board_hal.get_config().tank.ml_per_pulse;
info!(
"Final flow value is {} with {} ml",
@@ -715,8 +738,42 @@ pub async fn do_secure_pump(
}
async fn update_charge_indicator() {
let board = BOARD_ACCESS.get().lock().await;
let board = &mut BOARD_ACCESS.get().lock().await;
//we have mppt controller, ask it for charging current
// let tank_state = determine_tank_state(&mut board);
//
// if tank_state.is_enabled() {
// if let Some(err) = tank_state.got_error(&board.board_hal.get_config().tank) {
// match err {
// TankError::SensorDisabled => { /* unreachable */ }
// TankError::SensorMissing(raw_value_mv) => log(
// LogMessage::TankSensorMissing,
// raw_value_mv as u32,
// 0,
// "",
// "",
// ).await,
// TankError::SensorValueError { value, min, max } => log(
// LogMessage::TankSensorValueRangeError,
// min as u32,
// max as u32,
// &format!("{}", value),
// "",
// ).await,
// TankError::BoardError(err) => {
// log(LogMessage::TankSensorBoardError, 0, 0, "", &err.to_string()).await
// }
// }
// // disabled cannot trigger this because of wrapping if is_enabled
// board.board_hal.general_fault(true).await;
// } else if tank_state
// .warn_level(&board.board_hal.get_config().tank)
// .is_ok_and(|warn| warn)
// {
// log(LogMessage::TankWaterLevelLow, 0, 0, "", "").await;
// board.board_hal.general_fault(true).await;
// }
// }
if let Ok(current) = board.board_hal.get_mptt_current().await {
let _ = board
.board_hal
@@ -726,7 +783,7 @@ async fn update_charge_indicator() {
else if let Ok(charging) = board
.board_hal
.get_battery_monitor()
.average_current_milli_ampere()
.average_current_milli_ampere().await
{
let _ = board.board_hal.set_charge_indicator(charging > 20);
} else {
@@ -734,55 +791,55 @@ async fn update_charge_indicator() {
let _ = board.board_hal.set_charge_indicator(false);
}
}
//
// async fn publish_tank_state(tank_state: &TankState, water_temp: &anyhow::Result<f32>) {
// let board = &mut BOARD_ACCESS.get().lock().await;
// match serde_json::to_string(
// &tank_state.as_mqtt_info(&board.board_hal.get_config().tank, water_temp),
// ) {
// Ok(state) => {
// let _ = board
// .board_hal
// .get_esp()
// .mqtt_publish("/water", state.as_bytes());
// }
// Err(err) => {
// info!("Error publishing tankstate {}", err);
// }
// };
// }
async fn publish_tank_state(tank_state: &TankState, water_temp: &anyhow::Result<f32>) {
let board = &mut BOARD_ACCESS.get().lock().await;
match serde_json::to_string(
&tank_state.as_mqtt_info(&board.board_hal.get_config().tank, water_temp),
) {
Ok(state) => {
let _ = board
.board_hal
.get_esp()
.mqtt_publish("/water", state.as_bytes());
}
Err(err) => {
info!("Error publishing tankstate {}", err);
}
};
}
async fn publish_plant_states(timezone_time: &DateTime<Tz>, plantstate: &[PlantState; 8]) {
let board = &mut BOARD_ACCESS.get().lock().await;
for (plant_id, (plant_state, plant_conf)) in plantstate
.iter()
.zip(&board.board_hal.get_config().plants.clone())
.enumerate()
{
match serde_json::to_string(&plant_state.to_mqtt_info(plant_conf, timezone_time)) {
Ok(state) => {
let plant_topic = format!("/plant{}", plant_id + 1);
let _ = board
.board_hal
.get_esp()
.mqtt_publish(&plant_topic, state.as_bytes())
.await;
//TODO? reduce speed as else messages will be dropped
Timer::after_millis(200).await
}
Err(err) => {
error!("Error publishing plant state {}", err);
}
};
}
}
// async fn publish_plant_states(timezone_time: &DateTime<Tz>, plantstate: &[PlantState; 8]) {
// let board = &mut BOARD_ACCESS.get().lock().await;
// for (plant_id, (plant_state, plant_conf)) in plantstate
// .iter()
// .zip(&board.board_hal.get_config().plants.clone())
// .enumerate()
// {
// match serde_json::to_string(&plant_state.to_mqtt_info(plant_conf, timezone_time)) {
// Ok(state) => {
// let plant_topic = format!("/plant{}", plant_id + 1);
// let _ = board
// .board_hal
// .get_esp()
// .mqtt_publish(&plant_topic, state.as_bytes())
// .await;
// //TODO? reduce speed as else messages will be dropped
// Timer::after_millis(200).await
// }
// Err(err) => {
// error!("Error publishing plant state {}", err);
// }
// };
// }
// }
async fn publish_firmware_info(
version: VersionInfo,
address: u32,
ota_state_string: &str,
ip_address: &String,
timezone_time: DateTime<Tz>,
timezone_time: &String,
) {
let board = &mut BOARD_ACCESS.get().lock().await;
let _ = board
@@ -802,7 +859,7 @@ async fn publish_firmware_info(
.await;
let _ = board.board_hal.get_esp().mqtt_publish(
"/firmware/last_online",
timezone_time.to_rfc3339().as_bytes(),
timezone_time.as_bytes(),
);
let _ = board
.board_hal
@@ -821,7 +878,7 @@ async fn publish_firmware_info(
}
async fn try_connect_wifi_sntp_mqtt() -> NetworkMode {
let board = BOARD_ACCESS.get().lock().await;
let board = &mut BOARD_ACCESS.get().lock().await;
let nw_conf = &board.board_hal.get_config().network.clone();
match board.board_hal.get_esp().wifi(nw_conf).await {
Ok(ip_info) => {
@@ -860,7 +917,7 @@ async fn try_connect_wifi_sntp_mqtt() -> NetworkMode {
}
Err(_) => {
info!("Offline mode");
board.board_hal.general_fault(true);
board.board_hal.general_fault(true).await;
NetworkMode::OFFLINE
}
}
@@ -922,8 +979,8 @@ async fn publish_mppt_state() -> anyhow::Result<()> {
}
async fn publish_battery_state() -> () {
let board = BOARD_ACCESS.get().lock().await;
let state = board.board_hal.get_battery_monitor().get_battery_state();
let board = &mut BOARD_ACCESS.get().lock().await;
let state = board.board_hal.get_battery_monitor().get_battery_state().await;
if let Ok(serialized_battery_state_bytes) =
serde_json::to_string(&state).map(|s| s.into_bytes())
{
@@ -995,13 +1052,10 @@ async fn wait_infinity(wait_type: WaitType, reboot_now: Arc<AtomicBool>) -> ! {
async fn main(spawner: Spawner) {
// intialize embassy
logger::init_logger_from_env();
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
esp_alloc::heap_allocator!(size: 64 * 1024);
let timer0 = SystemTimer::new(peripherals.SYSTIMER);
esp_hal_embassy::init(timer0.alarm0);
//force init here!
let board = BOARD_ACCESS.get().lock().await;
drop(board);
println!("test");
info!("Embassy initialized!");