1116 lines
36 KiB
Rust
1116 lines
36 KiB
Rust
#![no_std]
|
|
#![no_main]
|
|
#![feature(never_type)]
|
|
#![feature(string_from_utf8_lossy_owned)]
|
|
#![feature(impl_trait_in_assoc_type)]
|
|
#![deny(
|
|
clippy::mem_forget,
|
|
reason = "mem::forget is generally not safe to do with esp_hal types, especially those \
|
|
holding buffers for the duration of a data transfer."
|
|
)]
|
|
|
|
//TODO insert version here and read it in other parts, also read this for the ota webview
|
|
esp_bootloader_esp_idf::esp_app_desc!();
|
|
use esp_backtrace as _;
|
|
|
|
use crate::config::{NetworkConfig, PlantConfig};
|
|
use crate::fat_error::FatResult;
|
|
use crate::hal::esp::MQTT_STAY_ALIVE;
|
|
use crate::hal::{esp_time, TIME_ACCESS};
|
|
use crate::log::LOG_ACCESS;
|
|
use crate::tank::{determine_tank_state, TankError, TankState, WATER_FROZEN_THRESH};
|
|
use crate::webserver::http_server;
|
|
use crate::{
|
|
config::BoardVersion::INITIAL,
|
|
hal::{PlantHal, HAL, PLANT_COUNT},
|
|
};
|
|
use ::log::{info, warn};
|
|
use alloc::borrow::ToOwned;
|
|
use alloc::string::{String, ToString};
|
|
use alloc::sync::Arc;
|
|
use alloc::{format, vec};
|
|
use chrono::{DateTime, Datelike, Timelike, Utc};
|
|
use chrono_tz::Tz::{self, UTC};
|
|
use core::sync::atomic::{AtomicBool, Ordering};
|
|
use embassy_executor::Spawner;
|
|
use embassy_net::Stack;
|
|
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
|
use embassy_sync::mutex::{Mutex, MutexGuard};
|
|
use embassy_sync::once_lock::OnceLock;
|
|
use embassy_time::Timer;
|
|
use esp_hal::rom::ets_delay_us;
|
|
use esp_hal::system::software_reset;
|
|
use esp_println::{logger, println};
|
|
use hal::battery::BatteryState;
|
|
use log::LogMessage;
|
|
use option_lock::OptionLock;
|
|
use plant_state::PlantState;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[no_mangle]
|
|
extern "C" fn custom_halt() -> ! {
|
|
println!("Fatal error occurred. Restarting in 10 seconds...");
|
|
|
|
for _delay in 0..30 {
|
|
ets_delay_us(1_000_000);
|
|
}
|
|
println!("resetting");
|
|
//give serial transmit time to finish
|
|
ets_delay_us(500_000);
|
|
software_reset()
|
|
}
|
|
|
|
//use tank::*;
|
|
mod config;
|
|
mod fat_error;
|
|
mod hal;
|
|
mod log;
|
|
mod plant_state;
|
|
mod tank;
|
|
mod webserver;
|
|
|
|
extern crate alloc;
|
|
//mod webserver;
|
|
|
|
pub static BOARD_ACCESS: OnceLock<Mutex<CriticalSectionRawMutex, HAL<'static>>> = OnceLock::new();
|
|
|
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
|
enum WaitType {
|
|
MissingConfig,
|
|
ConfigButton,
|
|
MqttConfig,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
|
struct Solar {
|
|
current_ma: u32,
|
|
voltage_ma: u32,
|
|
}
|
|
|
|
impl WaitType {
|
|
fn blink_pattern(&self) -> u64 {
|
|
match self {
|
|
WaitType::MissingConfig => 500_u64,
|
|
WaitType::ConfigButton => 100_u64,
|
|
WaitType::MqttConfig => 200_u64,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
|
|
/// Light State tracking data for mqtt
|
|
struct LightState {
|
|
/// is enabled in config
|
|
enabled: bool,
|
|
/// led is on
|
|
active: bool,
|
|
/// led should not be on at this time of day
|
|
out_of_work_hour: bool,
|
|
/// the battery is low so do not use led
|
|
battery_low: bool,
|
|
/// the sun is up
|
|
is_day: bool,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
|
|
///mqtt struct to track pump activities
|
|
struct PumpInfo {
|
|
enabled: bool,
|
|
pump_ineffective: bool,
|
|
median_current_ma: u16,
|
|
max_current_ma: u16,
|
|
min_current_ma: u16,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct PumpResult {
|
|
median_current_ma: u16,
|
|
max_current_ma: u16,
|
|
min_current_ma: u16,
|
|
error: bool,
|
|
flow_value_ml: f32,
|
|
flow_value_count: i16,
|
|
pump_time_s: u16,
|
|
}
|
|
|
|
#[derive(Serialize, Debug, PartialEq)]
|
|
enum SntpMode {
|
|
OFFLINE,
|
|
SYNC { current: DateTime<Utc> },
|
|
}
|
|
|
|
#[derive(Serialize, Debug, PartialEq)]
|
|
enum NetworkMode {
|
|
WIFI {
|
|
sntp: SntpMode,
|
|
mqtt: bool,
|
|
ip_address: String,
|
|
},
|
|
OFFLINE,
|
|
}
|
|
|
|
async fn safe_main(spawner: Spawner) -> FatResult<()> {
|
|
info!("Startup Rust");
|
|
|
|
let mut to_config = false;
|
|
let mut board = BOARD_ACCESS.get().await.lock().await;
|
|
|
|
let version = get_version(&mut board).await;
|
|
info!(
|
|
"Version using git has {} build on {}",
|
|
version.git_hash, version.build_time
|
|
);
|
|
|
|
board.board_hal.general_fault(false).await;
|
|
let cur = match board.board_hal.get_rtc_module().get_rtc_time().await {
|
|
Ok(value) => {
|
|
{
|
|
let guard = TIME_ACCESS.get().await.lock().await;
|
|
guard.set_current_time_us(value.timestamp_micros() as u64);
|
|
}
|
|
value
|
|
}
|
|
Err(err) => {
|
|
info!("rtc module error: {:?}", err);
|
|
board.board_hal.general_fault(true).await;
|
|
esp_time().await
|
|
}
|
|
};
|
|
|
|
//check if we know the time current > 2020 (plausibility checks, this code is newer than 2020)
|
|
if cur.year() < 2020 {
|
|
to_config = true;
|
|
LOG_ACCESS
|
|
.lock()
|
|
.await
|
|
.log(LogMessage::YearInplausibleForceConfig, 0, 0, "", "")
|
|
.await;
|
|
}
|
|
info!("cur is {}", cur);
|
|
update_charge_indicator(&mut board).await;
|
|
if board.board_hal.get_esp().get_restart_to_conf() {
|
|
LOG_ACCESS
|
|
.lock()
|
|
.await
|
|
.log(LogMessage::ConfigModeSoftwareOverride, 0, 0, "", "")
|
|
.await;
|
|
for _i in 0..2 {
|
|
board.board_hal.general_fault(true).await;
|
|
Timer::after_millis(100).await;
|
|
board.board_hal.general_fault(false).await;
|
|
Timer::after_millis(100).await;
|
|
}
|
|
to_config = true;
|
|
board.board_hal.general_fault(true).await;
|
|
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_ACCESS
|
|
.lock()
|
|
.await
|
|
.log(LogMessage::ConfigModeButtonOverride, 0, 0, "", "")
|
|
.await;
|
|
for _i in 0..5 {
|
|
board.board_hal.general_fault(true).await;
|
|
Timer::after_millis(100).await;
|
|
board.board_hal.general_fault(false).await;
|
|
Timer::after_millis(100).await;
|
|
}
|
|
|
|
if board.board_hal.get_esp().mode_override_pressed() {
|
|
board.board_hal.general_fault(true).await;
|
|
to_config = true;
|
|
} else {
|
|
board.board_hal.general_fault(false).await;
|
|
}
|
|
} else {
|
|
info!("no mode override");
|
|
}
|
|
|
|
if board.board_hal.get_config().hardware.board == INITIAL
|
|
&& board.board_hal.get_config().network.ssid.is_none()
|
|
{
|
|
info!("No wifi configured, starting initial config mode");
|
|
|
|
let stack = board.board_hal.get_esp().wifi_ap().await?;
|
|
|
|
let reboot_now = Arc::new(AtomicBool::new(false));
|
|
println!("starting webserver");
|
|
|
|
spawner.spawn(http_server(reboot_now.clone(), stack))?;
|
|
wait_infinity(board, WaitType::MissingConfig, reboot_now.clone()).await;
|
|
}
|
|
|
|
let mut stack: OptionLock<Stack> = OptionLock::empty();
|
|
let network_mode = if board.board_hal.get_config().network.ssid.is_some() {
|
|
try_connect_wifi_sntp_mqtt(&mut board, &mut stack).await
|
|
} else {
|
|
info!("No wifi configured");
|
|
//the current sensors require this amount to stabilize, in the case of Wi-Fi this is already handled due to connect timings;
|
|
Timer::after_millis(100).await;
|
|
NetworkMode::OFFLINE
|
|
};
|
|
|
|
if matches!(network_mode, NetworkMode::OFFLINE) && to_config {
|
|
info!("Could not connect to station and config mode forced, switching to ap mode!");
|
|
|
|
let res = {
|
|
let esp = board.board_hal.get_esp();
|
|
esp.wifi_ap().await
|
|
};
|
|
match res {
|
|
Ok(ap_stack) => {
|
|
stack.replace(ap_stack);
|
|
info!("Started ap, continuing")
|
|
}
|
|
Err(err) => info!("Could not start config override ap mode due to {}", err),
|
|
}
|
|
}
|
|
|
|
let tz = &board.board_hal.get_config().timezone;
|
|
let timezone = match tz {
|
|
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 = UTC;
|
|
|
|
let timezone_time = cur.with_timezone(&timezone);
|
|
info!(
|
|
"Running logic at utc {} and {} {}",
|
|
cur,
|
|
timezone.name(),
|
|
timezone_time
|
|
);
|
|
|
|
if let NetworkMode::WIFI { ref ip_address, .. } = network_mode {
|
|
publish_firmware_info(&mut board, version, ip_address, &timezone_time.to_rfc3339()).await;
|
|
publish_battery_state(&mut board).await;
|
|
let _ = publish_mppt_state(&mut board).await;
|
|
}
|
|
|
|
LOG_ACCESS
|
|
.lock()
|
|
.await
|
|
.log(
|
|
LogMessage::StartupInfo,
|
|
matches!(network_mode, NetworkMode::WIFI { .. }) as u32,
|
|
matches!(
|
|
network_mode,
|
|
NetworkMode::WIFI {
|
|
sntp: SntpMode::SYNC { .. },
|
|
..
|
|
}
|
|
) as u32,
|
|
matches!(network_mode, NetworkMode::WIFI { mqtt: true, .. })
|
|
.to_string()
|
|
.as_str(),
|
|
"",
|
|
)
|
|
.await;
|
|
|
|
if to_config {
|
|
//check if client or ap mode and init Wi-Fi
|
|
info!("executing config mode override");
|
|
//config upload will trigger reboot!
|
|
let reboot_now = Arc::new(AtomicBool::new(false));
|
|
spawner.spawn(http_server(reboot_now.clone(), stack.take().unwrap()))?;
|
|
wait_infinity(board, WaitType::ConfigButton, reboot_now.clone()).await;
|
|
} else {
|
|
LOG_ACCESS
|
|
.lock()
|
|
.await
|
|
.log(LogMessage::NormalRun, 0, 0, "", "")
|
|
.await;
|
|
}
|
|
|
|
let _dry_run = false;
|
|
|
|
let tank_state = determine_tank_state(&mut board).await;
|
|
|
|
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_ACCESS
|
|
.lock()
|
|
.await
|
|
.log(
|
|
LogMessage::TankSensorMissing,
|
|
raw_value_mv as u32,
|
|
0,
|
|
"",
|
|
"",
|
|
)
|
|
.await
|
|
}
|
|
TankError::SensorValueError { value, min, max } => {
|
|
LOG_ACCESS
|
|
.lock()
|
|
.await
|
|
.log(
|
|
LogMessage::TankSensorValueRangeError,
|
|
min as u32,
|
|
max as u32,
|
|
&format!("{}", value),
|
|
"",
|
|
)
|
|
.await
|
|
}
|
|
TankError::BoardError(err) => {
|
|
LOG_ACCESS
|
|
.lock()
|
|
.await
|
|
.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_ACCESS
|
|
.lock()
|
|
.await
|
|
.log(LogMessage::TankWaterLevelLow, 0, 0, "", "")
|
|
.await;
|
|
board.board_hal.general_fault(true).await;
|
|
}
|
|
}
|
|
|
|
let mut _water_frozen = false;
|
|
let water_temp: FatResult<f32> = match board.board_hal.get_tank_sensor() {
|
|
Ok(sensor) => sensor.water_temperature_c().await,
|
|
Err(e) => Err(e),
|
|
};
|
|
|
|
if let Ok(res) = water_temp {
|
|
if res < WATER_FROZEN_THRESH {
|
|
_water_frozen = true;
|
|
}
|
|
}
|
|
info!("Water temp is {}", water_temp.as_ref().unwrap_or(&0.));
|
|
|
|
publish_tank_state(&mut board, &tank_state, water_temp).await;
|
|
|
|
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(&mut board, &timezone_time.clone(), &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 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);
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
info!("state of charg");
|
|
let is_day = board.board_hal.is_day();
|
|
let state_of_charge = board
|
|
.board_hal
|
|
.get_battery_monitor()
|
|
.state_charge_percent()
|
|
.await
|
|
.unwrap_or(0.);
|
|
|
|
// try to load full battery state if failed the battery state is unknown
|
|
let battery_state = board
|
|
.board_hal
|
|
.get_battery_monitor()
|
|
.get_battery_state()
|
|
.await
|
|
.unwrap_or(BatteryState::Unknown);
|
|
|
|
info!("Battery state is {:?}", battery_state);
|
|
let mut light_state = LightState {
|
|
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();
|
|
info!("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();
|
|
info!("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).await?;
|
|
} else {
|
|
light_state.active = true;
|
|
board.board_hal.light(true).await?;
|
|
}
|
|
}
|
|
} else if light_state.battery_low {
|
|
board.board_hal.light(false).await?;
|
|
} else {
|
|
light_state.active = true;
|
|
board.board_hal.light(true).await?;
|
|
}
|
|
} else {
|
|
light_state.active = false;
|
|
board.board_hal.light(false).await?;
|
|
}
|
|
|
|
info!("Lightstate is {:?}", light_state);
|
|
}
|
|
|
|
match &serde_json::to_string(&light_state) {
|
|
Ok(state) => {
|
|
let _ = board
|
|
.board_hal
|
|
.get_esp()
|
|
.mqtt_publish("/light", state)
|
|
.await;
|
|
}
|
|
Err(err) => {
|
|
info!("Error publishing lightstate {}", err);
|
|
}
|
|
};
|
|
|
|
let deep_sleep_duration_minutes: u32 =
|
|
// if battery soc is unknown assume battery has enough change
|
|
if state_of_charge < 10.0 && !matches!(battery_state, BatteryState::Unknown) {
|
|
let _ = board
|
|
.board_hal
|
|
.get_esp()
|
|
.mqtt_publish("/deepsleep", "low Volt 12h").await;
|
|
12 * 60
|
|
} else if is_day {
|
|
let _ = board
|
|
.board_hal
|
|
.get_esp()
|
|
.mqtt_publish("/deepsleep", "normal 20m").await;
|
|
20
|
|
} else {
|
|
let _ = board
|
|
.board_hal
|
|
.get_esp()
|
|
.mqtt_publish("/deepsleep", "night 1h").await;
|
|
60
|
|
};
|
|
let _ = board
|
|
.board_hal
|
|
.get_esp()
|
|
.mqtt_publish("/state", "sleep")
|
|
.await;
|
|
|
|
info!("Go to sleep for {} minutes", deep_sleep_duration_minutes);
|
|
//determine next event
|
|
//is light out of work trigger soon?
|
|
//is battery low ??
|
|
//is deep sleep
|
|
//TODO
|
|
//mark_app_valid();
|
|
|
|
let stay_alive = MQTT_STAY_ALIVE.load(Ordering::Relaxed);
|
|
info!("Check stay alive, current state is {}", stay_alive);
|
|
|
|
if stay_alive {
|
|
let reboot_now = Arc::new(AtomicBool::new(false));
|
|
let _webserver = http_server(reboot_now.clone(), stack.take().unwrap());
|
|
wait_infinity(board, WaitType::MqttConfig, reboot_now.clone()).await;
|
|
} else {
|
|
//TODO wait for all mqtt publishes?
|
|
Timer::after_millis(5000).await;
|
|
|
|
board.board_hal.get_esp().set_restart_to_conf(false);
|
|
board
|
|
.board_hal
|
|
.deep_sleep(1000 * 1000 * 60 * deep_sleep_duration_minutes as u64)
|
|
.await;
|
|
}
|
|
}
|
|
|
|
pub async fn do_secure_pump(
|
|
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'_>>,
|
|
plant_id: usize,
|
|
plant_config: &PlantConfig,
|
|
dry_run: bool,
|
|
) -> FatResult<PumpResult> {
|
|
let mut current_collector = vec![0_u16; plant_config.pump_time_s.into()];
|
|
let mut flow_collector = vec![0_i16; plant_config.pump_time_s.into()];
|
|
let mut error = false;
|
|
let mut first_error = true;
|
|
let mut pump_time_s = 0;
|
|
if !dry_run {
|
|
board.board_hal.get_tank_sensor()?.reset_flow_meter();
|
|
board.board_hal.get_tank_sensor()?.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()?.get_flow_meter_value();
|
|
flow_collector[step] = flow_value;
|
|
let flow_value_ml = flow_value as f32 * board.board_hal.get_config().tank.ml_per_pulse;
|
|
|
|
info!(
|
|
"Flow value is {} ml, limit is {} ml raw sensor {}",
|
|
flow_value_ml, plant_config.pump_limit_ml, flow_value
|
|
);
|
|
if flow_value_ml > plant_config.pump_limit_ml as f32 {
|
|
info!("Flow value is reached, stopping");
|
|
break;
|
|
}
|
|
|
|
let current = board.board_hal.pump_current(plant_id).await;
|
|
match current {
|
|
Ok(current) => {
|
|
let current_ma = current.as_milliamperes() as u16;
|
|
current_collector[step] = current_ma;
|
|
let high_current = current_ma > plant_config.max_pump_current_ma;
|
|
if high_current {
|
|
if first_error {
|
|
LOG_ACCESS
|
|
.lock()
|
|
.await
|
|
.log(
|
|
LogMessage::PumpOverCurrent,
|
|
plant_id as u32 + 1,
|
|
current_ma as u32,
|
|
plant_config.max_pump_current_ma.to_string().as_str(),
|
|
step.to_string().as_str(),
|
|
)
|
|
.await;
|
|
board.board_hal.general_fault(true).await;
|
|
board.board_hal.fault(plant_id, true).await?;
|
|
if !plant_config.ignore_current_error {
|
|
error = true;
|
|
break;
|
|
}
|
|
first_error = false;
|
|
}
|
|
}
|
|
let low_current = current_ma < plant_config.min_pump_current_ma;
|
|
if low_current {
|
|
if first_error {
|
|
LOG_ACCESS
|
|
.lock()
|
|
.await
|
|
.log(
|
|
LogMessage::PumpOpenLoopCurrent,
|
|
plant_id as u32 + 1,
|
|
current_ma as u32,
|
|
plant_config.min_pump_current_ma.to_string().as_str(),
|
|
step.to_string().as_str(),
|
|
)
|
|
.await;
|
|
board.board_hal.general_fault(true).await;
|
|
board.board_hal.fault(plant_id, true).await?;
|
|
if !plant_config.ignore_current_error {
|
|
error = true;
|
|
break;
|
|
}
|
|
first_error = false;
|
|
}
|
|
}
|
|
}
|
|
Err(err) => {
|
|
if !plant_config.ignore_current_error {
|
|
info!("Error getting pump current: {}", err);
|
|
LOG_ACCESS
|
|
.lock()
|
|
.await
|
|
.log(
|
|
LogMessage::PumpMissingSensorCurrent,
|
|
plant_id as u32,
|
|
0,
|
|
"",
|
|
"",
|
|
)
|
|
.await;
|
|
error = true;
|
|
break;
|
|
} else {
|
|
//e.g., v3 without a sensor ends here, do not spam
|
|
}
|
|
}
|
|
}
|
|
Timer::after_millis(1000).await;
|
|
pump_time_s += 1;
|
|
}
|
|
}
|
|
board.board_hal.get_tank_sensor()?.stop_flow_meter();
|
|
let final_flow_value = board.board_hal.get_tank_sensor()?.get_flow_meter_value();
|
|
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",
|
|
final_flow_value, flow_value_ml
|
|
);
|
|
current_collector.sort();
|
|
Ok(PumpResult {
|
|
median_current_ma: current_collector[current_collector.len() / 2],
|
|
max_current_ma: current_collector[current_collector.len() - 1],
|
|
min_current_ma: current_collector[0],
|
|
flow_value_ml,
|
|
flow_value_count: final_flow_value,
|
|
pump_time_s,
|
|
error,
|
|
})
|
|
}
|
|
|
|
async fn update_charge_indicator(
|
|
board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>,
|
|
) {
|
|
//we have mppt controller, ask it for charging current
|
|
if let Ok(current) = board.board_hal.get_mptt_current().await {
|
|
let _ = board
|
|
.board_hal
|
|
.set_charge_indicator(current.as_milliamperes() > 20_f64);
|
|
}
|
|
//fallback to battery controller and ask it instead
|
|
else if let Ok(charging) = board
|
|
.board_hal
|
|
.get_battery_monitor()
|
|
.average_current_milli_ampere()
|
|
.await
|
|
{
|
|
let _ = board.board_hal.set_charge_indicator(charging > 20);
|
|
} else {
|
|
//who knows
|
|
let _ = board.board_hal.set_charge_indicator(false);
|
|
}
|
|
}
|
|
|
|
async fn publish_tank_state(
|
|
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
|
|
tank_state: &TankState,
|
|
water_temp: FatResult<f32>,
|
|
) {
|
|
let state = serde_json::to_string(
|
|
&tank_state.as_mqtt_info(&board.board_hal.get_config().tank, &water_temp),
|
|
)
|
|
.unwrap();
|
|
let _ = board.board_hal.get_esp().mqtt_publish("/water", &*state);
|
|
}
|
|
|
|
async fn publish_plant_states(
|
|
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
|
|
timezone_time: &DateTime<Tz>,
|
|
plantstate: &[PlantState; 8],
|
|
) {
|
|
for (plant_id, (plant_state, plant_conf)) in plantstate
|
|
.iter()
|
|
.zip(&board.board_hal.get_config().plants.clone())
|
|
.enumerate()
|
|
{
|
|
let state =
|
|
serde_json::to_string(&plant_state.to_mqtt_info(plant_conf, timezone_time)).unwrap();
|
|
let plant_topic = format!("/plant{}", plant_id + 1);
|
|
let _ = board
|
|
.board_hal
|
|
.get_esp()
|
|
.mqtt_publish(&plant_topic, &state)
|
|
.await;
|
|
}
|
|
}
|
|
|
|
async fn publish_firmware_info(
|
|
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
|
|
version: VersionInfo,
|
|
ip_address: &String,
|
|
timezone_time: &String,
|
|
) {
|
|
let esp = board.board_hal.get_esp();
|
|
let _ = esp.mqtt_publish("/firmware/address", ip_address).await;
|
|
let _ = esp
|
|
.mqtt_publish("/firmware/githash", &version.git_hash)
|
|
.await;
|
|
let _ = esp
|
|
.mqtt_publish("/firmware/buildtime", &version.build_time)
|
|
.await;
|
|
let _ = esp.mqtt_publish("/firmware/last_online", timezone_time);
|
|
let state = esp.get_ota_state();
|
|
let _ = esp.mqtt_publish("/firmware/ota_state", &state).await;
|
|
let slot = esp.get_ota_slot();
|
|
let _ = esp
|
|
.mqtt_publish("/firmware/ota_slot", &format!("slot{slot}"))
|
|
.await;
|
|
let _ = esp.mqtt_publish("/state", "online").await;
|
|
}
|
|
macro_rules! mk_static {
|
|
($t:ty,$val:expr) => {{
|
|
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
|
|
#[deny(unused_attributes)]
|
|
let x = STATIC_CELL.uninit().write(($val));
|
|
x
|
|
}};
|
|
}
|
|
async fn try_connect_wifi_sntp_mqtt(
|
|
board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>,
|
|
stack_store: &mut OptionLock<Stack<'static>>,
|
|
) -> NetworkMode {
|
|
let nw_conf = &board.board_hal.get_config().network.clone();
|
|
match board.board_hal.get_esp().wifi(nw_conf).await {
|
|
Ok(stack) => {
|
|
stack_store.replace(stack);
|
|
|
|
let sntp_mode: SntpMode = match board
|
|
.board_hal
|
|
.get_esp()
|
|
.sntp(1000 * 10, stack.clone())
|
|
.await
|
|
{
|
|
Ok(new_time) => {
|
|
info!("Using time from sntp {}", new_time.to_rfc3339());
|
|
let _ = board.board_hal.get_rtc_module().set_rtc_time(&new_time);
|
|
SntpMode::SYNC { current: new_time }
|
|
}
|
|
Err(err) => {
|
|
warn!("sntp error: {}", err);
|
|
board.board_hal.general_fault(true).await;
|
|
SntpMode::OFFLINE
|
|
}
|
|
};
|
|
|
|
let mqtt_connected = if board.board_hal.get_config().network.mqtt_url.is_some() {
|
|
let nw_config = board.board_hal.get_config().network.clone();
|
|
let nw_config = mk_static!(NetworkConfig, nw_config);
|
|
match board.board_hal.get_esp().mqtt(nw_config, stack).await {
|
|
Ok(_) => {
|
|
info!("Mqtt connection ready");
|
|
true
|
|
}
|
|
Err(err) => {
|
|
warn!("Could not connect mqtt due to {}", err);
|
|
false
|
|
}
|
|
}
|
|
} else {
|
|
false
|
|
};
|
|
NetworkMode::WIFI {
|
|
sntp: sntp_mode,
|
|
mqtt: mqtt_connected,
|
|
ip_address: stack.hardware_address().to_string(),
|
|
}
|
|
}
|
|
Err(err) => {
|
|
info!("Offline mode due to {}", err);
|
|
board.board_hal.general_fault(true).await;
|
|
NetworkMode::OFFLINE
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn pump_info(
|
|
plant_id: usize,
|
|
pump_active: bool,
|
|
pump_ineffective: bool,
|
|
median_current_ma: u16,
|
|
max_current_ma: u16,
|
|
min_current_ma: u16,
|
|
_error: bool,
|
|
) {
|
|
let pump_info = PumpInfo {
|
|
enabled: pump_active,
|
|
pump_ineffective,
|
|
median_current_ma,
|
|
max_current_ma,
|
|
min_current_ma,
|
|
};
|
|
let pump_topic = format!("/pump{}", plant_id + 1);
|
|
|
|
match serde_json::to_string(&pump_info) {
|
|
Ok(state) => {
|
|
BOARD_ACCESS
|
|
.get()
|
|
.await
|
|
.lock()
|
|
.await
|
|
.board_hal
|
|
.get_esp()
|
|
.mqtt_publish(&pump_topic, &state)
|
|
.await;
|
|
}
|
|
Err(err) => {
|
|
warn!("Error publishing pump state {}", err);
|
|
}
|
|
};
|
|
}
|
|
|
|
async fn publish_mppt_state(
|
|
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
|
|
) -> FatResult<()> {
|
|
let current = board.board_hal.get_mptt_current().await?;
|
|
let voltage = board.board_hal.get_mptt_voltage().await?;
|
|
let solar_state = Solar {
|
|
current_ma: current.as_milliamperes() as u32,
|
|
voltage_ma: voltage.as_millivolts() as u32,
|
|
};
|
|
if let Ok(serialized_solar_state_bytes) = serde_json::to_string(&solar_state) {
|
|
let _ = board
|
|
.board_hal
|
|
.get_esp()
|
|
.mqtt_publish("/mppt", &serialized_solar_state_bytes);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn publish_battery_state(
|
|
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
|
|
) -> () {
|
|
let state = board
|
|
.board_hal
|
|
.get_battery_monitor()
|
|
.get_battery_state()
|
|
.await;
|
|
let value = match state {
|
|
Ok(state) => {
|
|
let json = serde_json::to_string(&state).unwrap().to_owned();
|
|
json.to_owned()
|
|
}
|
|
Err(_) => "error".to_owned(),
|
|
};
|
|
{
|
|
let _ = board
|
|
.board_hal
|
|
.get_esp()
|
|
.mqtt_publish("/battery", &*value)
|
|
.await;
|
|
}
|
|
}
|
|
|
|
async fn wait_infinity(
|
|
board: MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
|
|
wait_type: WaitType,
|
|
reboot_now: Arc<AtomicBool>,
|
|
) -> ! {
|
|
//since we force to have the lock when entering, we can release it to ensure the caller does not forget to dispose of it
|
|
drop(board);
|
|
|
|
let delay = wait_type.blink_pattern();
|
|
let mut led_count = 8;
|
|
let mut pattern_step = 0;
|
|
loop {
|
|
{
|
|
let mut board = BOARD_ACCESS.get().await.lock().await;
|
|
update_charge_indicator(&mut board).await;
|
|
|
|
match wait_type {
|
|
WaitType::MissingConfig => {
|
|
// Keep existing behavior: circular filling pattern
|
|
led_count %= 8;
|
|
led_count += 1;
|
|
for i in 0..8 {
|
|
let _ = board.board_hal.fault(i, i < led_count);
|
|
}
|
|
}
|
|
WaitType::ConfigButton => {
|
|
// Alternating pattern: 1010 1010 -> 0101 0101
|
|
pattern_step = (pattern_step + 1) % 2;
|
|
for i in 0..8 {
|
|
let _ = board.board_hal.fault(i, (i + pattern_step) % 2 == 0);
|
|
}
|
|
}
|
|
WaitType::MqttConfig => {
|
|
// Moving dot pattern
|
|
pattern_step = (pattern_step + 1) % 8;
|
|
for i in 0..8 {
|
|
let _ = board.board_hal.fault(i, i == pattern_step);
|
|
}
|
|
}
|
|
}
|
|
board.board_hal.general_fault(true).await;
|
|
}
|
|
|
|
Timer::after_millis(delay).await;
|
|
{
|
|
let mut board = BOARD_ACCESS.get().await.lock().await;
|
|
board.board_hal.general_fault(false).await;
|
|
|
|
// Clear all LEDs
|
|
for i in 0..8 {
|
|
let _ = board.board_hal.fault(i, false);
|
|
}
|
|
}
|
|
|
|
Timer::after_millis(delay).await;
|
|
|
|
if wait_type == WaitType::MqttConfig && !MQTT_STAY_ALIVE.load(Ordering::Relaxed) {
|
|
reboot_now.store(true, Ordering::Relaxed);
|
|
}
|
|
if reboot_now.load(Ordering::Relaxed) {
|
|
//ensure clean http answer
|
|
Timer::after_millis(500).await;
|
|
BOARD_ACCESS
|
|
.get()
|
|
.await
|
|
.lock()
|
|
.await
|
|
.board_hal
|
|
.deep_sleep(0)
|
|
.await;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[esp_hal_embassy::main]
|
|
async fn main(spawner: Spawner) -> ! {
|
|
// intialize embassy
|
|
logger::init_logger_from_env();
|
|
//force init here!
|
|
println!("Hal init");
|
|
match BOARD_ACCESS.init(PlantHal::create().await.unwrap()) {
|
|
Ok(_) => {}
|
|
Err(_) => {
|
|
panic!("Could not set hal to static")
|
|
}
|
|
}
|
|
println!("Hal init done, starting logic");
|
|
|
|
match safe_main(spawner).await {
|
|
// this should not get triggered, safe_main should not return but go into deep sleep or reboot
|
|
Ok(_) => {
|
|
panic!("Main app finished, but should never do, restarting");
|
|
}
|
|
// if safe_main exists with an error, rollback to a known good ota version
|
|
Err(err) => {
|
|
panic!("Failed main {}", err);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn in_time_range(cur: &DateTime<Tz>, start: u8, end: u8) -> bool {
|
|
let current_hour = cur.hour() as u8;
|
|
//eg 10-14
|
|
if start < end {
|
|
current_hour > start && current_hour < end
|
|
} else {
|
|
//eg 20-05
|
|
current_hour > start || current_hour < end
|
|
}
|
|
}
|
|
|
|
async fn get_version(
|
|
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
|
|
) -> VersionInfo {
|
|
let branch = env!("VERGEN_GIT_BRANCH").to_owned();
|
|
let hash = &env!("VERGEN_GIT_SHA")[0..8];
|
|
|
|
let board = board.board_hal.get_esp();
|
|
let ota_slot = board.get_ota_slot();
|
|
VersionInfo {
|
|
git_hash: branch + "@" + hash,
|
|
build_time: env!("VERGEN_BUILD_TIMESTAMP").to_owned(),
|
|
partition: ota_slot,
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Debug)]
|
|
struct VersionInfo {
|
|
git_hash: String,
|
|
build_time: String,
|
|
partition: String,
|
|
}
|