diff --git a/Software/MainBoard/rust/src/config.rs b/Software/MainBoard/rust/src/config.rs index 06c9f55..2f1b66c 100644 --- a/Software/MainBoard/rust/src/config.rs +++ b/Software/MainBoard/rust/src/config.rs @@ -96,6 +96,8 @@ pub enum BoardVersion { pub struct BoardHardware { pub board: BoardVersion, pub battery: BatteryBoardVersion, + #[serde(default)] + pub pump_corrosion_protection: bool, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default, Encode, Decode)] diff --git a/Software/MainBoard/rust/src/hal/esp.rs b/Software/MainBoard/rust/src/hal/esp.rs index 72ac67f..3210642 100644 --- a/Software/MainBoard/rust/src/hal/esp.rs +++ b/Software/MainBoard/rust/src/hal/esp.rs @@ -55,6 +55,8 @@ static mut CONSECUTIVE_WATERING_PLANT: [u32; PLANT_COUNT] = [0; PLANT_COUNT]; static mut LOW_VOLTAGE_DETECTED: i8 = 0; #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))] static mut RESTART_TO_CONF: i8 = 0; +#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))] +static mut LAST_CORROSION_PROTECTION_CHECK_DAY: i8 = -1; const NTP_SERVER: &str = "pool.ntp.org"; @@ -341,6 +343,14 @@ impl Esp<'_> { } } } + pub(crate) fn get_last_corrosion_protection_check_day(&self) -> i8 { + unsafe { LAST_CORROSION_PROTECTION_CHECK_DAY } + } + pub(crate) fn set_last_corrosion_protection_check_day(&mut self, day: i8) { + unsafe { + LAST_CORROSION_PROTECTION_CHECK_DAY = day; + } + } pub(crate) async fn wifi_ap(&mut self, spawner: Spawner) -> FatResult> { let ssid = match self.load_config().await { @@ -595,6 +605,7 @@ impl Esp<'_> { } else { RESTART_TO_CONF = 0; } + LAST_CORROSION_PROTECTION_CHECK_DAY = -1; }; } else { unsafe { diff --git a/Software/MainBoard/rust/src/hal/savegame_manager.rs b/Software/MainBoard/rust/src/hal/savegame_manager.rs index 42af9cf..92ff03f 100644 --- a/Software/MainBoard/rust/src/hal/savegame_manager.rs +++ b/Software/MainBoard/rust/src/hal/savegame_manager.rs @@ -64,8 +64,8 @@ impl Flash for SavegameFlashAdapter<'_> { data.len() ); let error = NorFlash::write(self.region, addr, data); - if error.is_err() { - error!("error {:?}", error.unwrap_err()) + if let Err(err) = error { + error!("error {:?}", err); } error.map_err(SavegameFlashError) } diff --git a/Software/MainBoard/rust/src/log/interceptor.rs b/Software/MainBoard/rust/src/log/interceptor.rs index 196ebbd..62611df 100644 --- a/Software/MainBoard/rust/src/log/interceptor.rs +++ b/Software/MainBoard/rust/src/log/interceptor.rs @@ -34,8 +34,7 @@ impl InterceptorLogger { } pub fn init(&'static self) { - match log::set_logger(self) - .map(|()| log::set_max_level(LevelFilter::Info)) { + match log::set_logger(self).map(|()| log::set_max_level(LevelFilter::Info)) { Ok(()) => {} Err(e) => { error!("Logger already set: {}", e); diff --git a/Software/MainBoard/rust/src/main.rs b/Software/MainBoard/rust/src/main.rs index 1b097aa..ec20507 100644 --- a/Software/MainBoard/rust/src/main.rs +++ b/Software/MainBoard/rust/src/main.rs @@ -122,6 +122,7 @@ struct PumpInfo { median_current_ma: u16, max_current_ma: u16, min_current_ma: u16, + error: String, } #[derive(Serialize)] @@ -129,7 +130,7 @@ pub struct PumpResult { median_current_ma: u16, max_current_ma: u16, min_current_ma: u16, - error: bool, + error: String, flow_value_ml: f32, flow_value_count: i16, pump_time_s: u16, @@ -239,7 +240,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { println!("starting webserver"); let _ = http_server(reboot_now.clone(), stack); - wait_infinity(board, WaitType::MissingConfig, reboot_now.clone()).await; + wait_infinity(board, WaitType::MissingConfig, reboot_now.clone(), UTC).await; } let mut stack: OptionLock = OptionLock::empty(); @@ -276,7 +277,6 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { }), None => UTC, // Fallback to UTC if no timezone is set }; - let _timezone = UTC; let timezone_time = cur.with_timezone(&timezone); info!( @@ -321,7 +321,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { } else { bail!("Network stack missing, hard abort") } - wait_infinity(board, WaitType::ConfigButton, reboot_now.clone()).await; + wait_infinity(board, WaitType::ConfigButton, reboot_now.clone(), timezone).await; } else { log(LogMessage::NormalRun, 0, 0, "", ""); } @@ -445,21 +445,40 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { board.board_hal.get_esp().last_pump_time(plant_id); //state.active = true; - pump_info(&mut board, plant_id, true, pump_ineffective, 0, 0, 0).await; + pump_info(&mut board, plant_id, true, pump_ineffective, 0, 0, 0, String::new()).await; - let result = do_secure_pump(&mut board, plant_id, plant_config, dry_run).await?; + let result = do_secure_pump(&mut board, plant_id, plant_config, dry_run).await; + match result { + Ok(state) => { + pump_info( + &mut board, + plant_id, + false, + pump_ineffective, + state.median_current_ma, + state.max_current_ma, + state.min_current_ma, + state.error, + ) + .await; + } + Err(err) => { + pump_info( + &mut board, + plant_id, + false, + pump_ineffective, + 0, + 0, + 0, + format!("{err:?}"), + ) + .await; + } + } //stop pump regardless of prior result//todo refactor to inner? board.board_hal.pump(plant_id, false).await?; - pump_info( - &mut board, - plant_id, - false, - pump_ineffective, - result.median_current_ma, - result.max_current_ma, - result.min_current_ma, - ) - .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 @@ -469,6 +488,33 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { .store_consecutive_pump_count(plant_id, 0); } } + } else { + // Pump corrosion protection: pulses each pump once a week for 2s around midday. + let last_check_day = board.board_hal.get_esp().get_last_corrosion_protection_check_day(); + if board.board_hal.get_config().hardware.pump_corrosion_protection { + let current_day = timezone_time.weekday().number_from_monday() as i8; + let current_hour = timezone_time.hour(); + + // Monday (1) and around midday (11-13) + if current_day == 1 && (11..14).contains(¤t_hour) { + if last_check_day != current_day { + info!("Running pump corrosion protection"); + for plant_id in 0..PLANT_COUNT { + let mut plant_config = board.board_hal.get_config().plants[plant_id].clone(); + plant_config.pump_time_s = 2; + plant_config.pump_limit_ml = 1000; // high limit to ensure it runs for 2s + + log(LogMessage::PumpPlant, (plant_id + 1) as u32, 2, "corrosion_prot", ""); + let _ = do_secure_pump(&mut board, plant_id, &plant_config, dry_run).await; + let _ = board.board_hal.pump(plant_id, false).await; + } + board.board_hal.get_esp().set_last_corrosion_protection_check_day(current_day); + } + } else if last_check_day != current_day && current_day != 1 { + // Reset check day if it's a different day (and not Monday), so it can trigger again next week + board.board_hal.get_esp().set_last_corrosion_protection_check_day(-1); + } + } } info!("state of charg"); @@ -578,19 +624,18 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { .mqtt_publish("/deepsleep", "night 1h").await; 60 }; + let _ = board .board_hal .get_esp() .mqtt_publish("/state", "sleep") .await; - info!("Go to sleep for {deep_sleep_duration_minutes} 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}"); @@ -599,7 +644,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { let reboot_now = Arc::new(AtomicBool::new(false)); if let Some(s) = stack.take() { spawner.spawn(http_server(reboot_now.clone(), s))?; - wait_infinity(board, WaitType::MqttConfig, reboot_now.clone()).await; + wait_infinity(board, WaitType::MqttConfig, reboot_now.clone(), timezone).await; } else { bail!("Network Stack missing, hard abort"); } @@ -628,7 +673,7 @@ pub async fn do_secure_pump( let mut current_collector = vec![0_u16; steps_in_50ms]; let mut flow_collector = vec![0_i16; steps_in_50ms]; - let mut error = false; + let mut error = String::new(); let mut first_error = true; let mut pump_time_ms: u32 = 0; @@ -662,6 +707,7 @@ pub async fn do_secure_pump( && high_current && current_ma > STARTUP_ABORT_CURRENT_MA { + let err_msg = format!("OverCurrent startup: {}mA", current_ma); log( LogMessage::PumpOverCurrent, plant_id as u32 + 1, @@ -669,8 +715,9 @@ pub async fn do_secure_pump( plant_config.max_pump_current_ma.to_string().as_str(), step.to_string().as_str(), ); - error = true; + error = err_msg; } else if high_current && first_error { + let err_msg = format!("OverCurrent: {}mA", current_ma); log( LogMessage::PumpOverCurrent, plant_id as u32 + 1, @@ -681,13 +728,14 @@ pub async fn do_secure_pump( board.board_hal.general_fault(true).await; board.board_hal.fault(plant_id, true).await?; if !plant_config.ignore_current_error { - error = true; + error = err_msg; break; } first_error = false; } let low_current = current_ma < plant_config.min_pump_current_ma; if low_current && first_error { + let err_msg = format!("OpenLoop: {}mA", current_ma); log( LogMessage::PumpOpenLoopCurrent, plant_id as u32 + 1, @@ -698,7 +746,7 @@ pub async fn do_secure_pump( board.board_hal.general_fault(true).await; board.board_hal.fault(plant_id, true).await?; if !plant_config.ignore_current_error { - error = true; + error = err_msg; break; } first_error = false; @@ -707,6 +755,7 @@ pub async fn do_secure_pump( Err(err) => { if !plant_config.ignore_current_error { info!("Error getting pump current: {err}"); + let err_msg = format!("MissingSensor: {err:?}"); log( LogMessage::PumpMissingSensorCurrent, plant_id as u32, @@ -714,7 +763,7 @@ pub async fn do_secure_pump( "", "", ); - error = true; + error = err_msg; break; } else { error!("Error getting pump current: {err}"); @@ -740,6 +789,9 @@ pub async fn do_secure_pump( Timer::after(sleep_time).await; pump_time_ms += 50; } + } else { + //noticable dummy value + pump_time_ms = 1337; } board.board_hal.get_tank_sensor()?.stop_flow_meter(); let final_flow_value = board.board_hal.get_tank_sensor()?.get_flow_meter_value(); @@ -907,6 +959,7 @@ async fn pump_info( median_current_ma: u16, max_current_ma: u16, min_current_ma: u16, + error: String, ) { let pump_info = PumpInfo { enabled: pump_active, @@ -914,6 +967,7 @@ async fn pump_info( median_current_ma, max_current_ma, min_current_ma, + error, }; let pump_topic = format!("/pump{}", plant_id + 1); @@ -975,6 +1029,7 @@ async fn wait_infinity( board: MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, wait_type: WaitType, reboot_now: Arc, + timezone: Tz, ) -> ! { //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); @@ -984,6 +1039,7 @@ async fn wait_infinity( let mut pattern_step = 0; let serial_config_receive = AtomicBool::new(false); let mut suppress_further_mppt_error = false; + let mut last_mqtt_update: Option = None; // Long-press exit (for webserver config modes): hold boot button for 5 seconds. let mut exit_hold_started: Option = None; @@ -1060,6 +1116,22 @@ async fn wait_infinity( } } + // MQTT updates in config mode + let now = Instant::now(); + if last_mqtt_update.is_none() + || now.duration_since(last_mqtt_update.unwrap_or(Instant::from_secs(0))) + >= Duration::from_secs(60) + { + let cur = board.board_hal.get_time().await; + let timezone_time = cur.with_timezone(&timezone); + + let esp = board.board_hal.get_esp(); + esp.mqtt_publish("/firmware/state", "config").await; + esp.mqtt_publish("/firmware/last_online", &timezone_time.to_rfc3339()) + .await; + last_mqtt_update = Some(now); + } + // Skip default blink code when a progress display is active if !PROGRESS_ACTIVE.load(Ordering::Relaxed) { match wait_type { diff --git a/Software/MainBoard/rust/src_webpack/src/api.ts b/Software/MainBoard/rust/src_webpack/src/api.ts index e5aa625..37e61fb 100644 --- a/Software/MainBoard/rust/src_webpack/src/api.ts +++ b/Software/MainBoard/rust/src_webpack/src/api.ts @@ -98,6 +98,7 @@ export enum BoardVersion { export interface BoardHardware { board: BoardVersion, battery: BatteryBoardVersion, + pump_corrosion_protection: boolean, } export interface PlantControllerConfig { diff --git a/Software/MainBoard/rust/src_webpack/src/hardware.html b/Software/MainBoard/rust/src_webpack/src/hardware.html index 0b2a24b..25e0e50 100644 --- a/Software/MainBoard/rust/src_webpack/src/hardware.html +++ b/Software/MainBoard/rust/src_webpack/src/hardware.html @@ -18,3 +18,7 @@ +
+
Pump corrosion protection (weekly)
+ +
diff --git a/Software/MainBoard/rust/src_webpack/src/hardware.ts b/Software/MainBoard/rust/src_webpack/src/hardware.ts index 86758f2..9123b76 100644 --- a/Software/MainBoard/rust/src_webpack/src/hardware.ts +++ b/Software/MainBoard/rust/src_webpack/src/hardware.ts @@ -4,6 +4,7 @@ import {BatteryBoardVersion, BoardHardware, BoardVersion} from "./api"; export class HardwareConfigView { private readonly hardware_board_value: HTMLSelectElement; private readonly hardware_battery_value: HTMLSelectElement; + private readonly hardware_pump_corrosion_protection: HTMLInputElement; constructor(controller:Controller){ (document.getElementById("hardwareview") as HTMLElement).innerHTML = require('./hardware.html') as string; @@ -29,17 +30,22 @@ export class HardwareConfigView { option.innerText = version.toString(); this.hardware_battery_value.appendChild(option); }) + + this.hardware_pump_corrosion_protection = document.getElementById("hardware_pump_corrosion_protection") as HTMLInputElement; + this.hardware_pump_corrosion_protection.onchange = controller.configChanged } setConfig(hardware: BoardHardware) { this.hardware_board_value.value = hardware.board.toString() this.hardware_battery_value.value = hardware.battery.toString() + this.hardware_pump_corrosion_protection.checked = hardware.pump_corrosion_protection } getConfig(): BoardHardware { return { board : BoardVersion[this.hardware_board_value.value as keyof typeof BoardVersion], battery : BatteryBoardVersion[this.hardware_battery_value.value as keyof typeof BatteryBoardVersion], + pump_corrosion_protection : this.hardware_pump_corrosion_protection.checked, } } }