feat: add pump corrosion protection feature, extend error handling for pump operations, and enhance configuration options

This commit is contained in:
2026-04-16 21:56:46 +02:00
parent b740574c68
commit 0f6cb5243c
6 changed files with 95 additions and 18 deletions

View File

@@ -96,6 +96,8 @@ pub enum BoardVersion {
pub struct BoardHardware { pub struct BoardHardware {
pub board: BoardVersion, pub board: BoardVersion,
pub battery: BatteryBoardVersion, pub battery: BatteryBoardVersion,
#[serde(default)]
pub pump_corrosion_protection: bool,
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default, Encode, Decode)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default, Encode, Decode)]

View File

@@ -55,6 +55,8 @@ static mut CONSECUTIVE_WATERING_PLANT: [u32; PLANT_COUNT] = [0; PLANT_COUNT];
static mut LOW_VOLTAGE_DETECTED: i8 = 0; static mut LOW_VOLTAGE_DETECTED: i8 = 0;
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))] #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut RESTART_TO_CONF: i8 = 0; 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"; 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<Stack<'static>> { pub(crate) async fn wifi_ap(&mut self, spawner: Spawner) -> FatResult<Stack<'static>> {
let ssid = match self.load_config().await { let ssid = match self.load_config().await {
@@ -595,6 +605,7 @@ impl Esp<'_> {
} else { } else {
RESTART_TO_CONF = 0; RESTART_TO_CONF = 0;
} }
LAST_CORROSION_PROTECTION_CHECK_DAY = -1;
}; };
} else { } else {
unsafe { unsafe {

View File

@@ -122,6 +122,7 @@ struct PumpInfo {
median_current_ma: u16, median_current_ma: u16,
max_current_ma: u16, max_current_ma: u16,
min_current_ma: u16, min_current_ma: u16,
error: String,
} }
#[derive(Serialize)] #[derive(Serialize)]
@@ -129,7 +130,7 @@ pub struct PumpResult {
median_current_ma: u16, median_current_ma: u16,
max_current_ma: u16, max_current_ma: u16,
min_current_ma: u16, min_current_ma: u16,
error: bool, error: String,
flow_value_ml: f32, flow_value_ml: f32,
flow_value_count: i16, flow_value_count: i16,
pump_time_s: u16, pump_time_s: u16,
@@ -444,21 +445,40 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
board.board_hal.get_esp().last_pump_time(plant_id); board.board_hal.get_esp().last_pump_time(plant_id);
//state.active = true; //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;
//stop pump regardless of prior result//todo refactor to inner? match result {
board.board_hal.pump(plant_id, false).await?; Ok(state) => {
pump_info( pump_info(
&mut board, &mut board,
plant_id, plant_id,
false, false,
pump_ineffective, pump_ineffective,
result.median_current_ma, state.median_current_ma,
result.max_current_ma, state.max_current_ma,
result.min_current_ma, state.min_current_ma,
state.error,
) )
.await; .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?;
} else if !state.pump_in_timeout(plant_config, &timezone_time) { } else if !state.pump_in_timeout(plant_config, &timezone_time) {
// plant does not need to be watered and is not in timeout // plant does not need to be watered and is not in timeout
// -> reset consecutive pump count // -> reset consecutive pump count
@@ -468,6 +488,33 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
.store_consecutive_pump_count(plant_id, 0); .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(&current_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"); info!("state of charg");
@@ -626,7 +673,7 @@ pub async fn do_secure_pump(
let mut current_collector = vec![0_u16; steps_in_50ms]; let mut current_collector = vec![0_u16; steps_in_50ms];
let mut flow_collector = vec![0_i16; 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 first_error = true;
let mut pump_time_ms: u32 = 0; let mut pump_time_ms: u32 = 0;
@@ -660,6 +707,7 @@ pub async fn do_secure_pump(
&& high_current && high_current
&& current_ma > STARTUP_ABORT_CURRENT_MA && current_ma > STARTUP_ABORT_CURRENT_MA
{ {
let err_msg = format!("OverCurrent startup: {}mA", current_ma);
log( log(
LogMessage::PumpOverCurrent, LogMessage::PumpOverCurrent,
plant_id as u32 + 1, plant_id as u32 + 1,
@@ -667,8 +715,9 @@ pub async fn do_secure_pump(
plant_config.max_pump_current_ma.to_string().as_str(), plant_config.max_pump_current_ma.to_string().as_str(),
step.to_string().as_str(), step.to_string().as_str(),
); );
error = true; error = err_msg;
} else if high_current && first_error { } else if high_current && first_error {
let err_msg = format!("OverCurrent: {}mA", current_ma);
log( log(
LogMessage::PumpOverCurrent, LogMessage::PumpOverCurrent,
plant_id as u32 + 1, plant_id as u32 + 1,
@@ -679,13 +728,14 @@ pub async fn do_secure_pump(
board.board_hal.general_fault(true).await; board.board_hal.general_fault(true).await;
board.board_hal.fault(plant_id, true).await?; board.board_hal.fault(plant_id, true).await?;
if !plant_config.ignore_current_error { if !plant_config.ignore_current_error {
error = true; error = err_msg;
break; break;
} }
first_error = false; first_error = false;
} }
let low_current = current_ma < plant_config.min_pump_current_ma; let low_current = current_ma < plant_config.min_pump_current_ma;
if low_current && first_error { if low_current && first_error {
let err_msg = format!("OpenLoop: {}mA", current_ma);
log( log(
LogMessage::PumpOpenLoopCurrent, LogMessage::PumpOpenLoopCurrent,
plant_id as u32 + 1, plant_id as u32 + 1,
@@ -696,7 +746,7 @@ pub async fn do_secure_pump(
board.board_hal.general_fault(true).await; board.board_hal.general_fault(true).await;
board.board_hal.fault(plant_id, true).await?; board.board_hal.fault(plant_id, true).await?;
if !plant_config.ignore_current_error { if !plant_config.ignore_current_error {
error = true; error = err_msg;
break; break;
} }
first_error = false; first_error = false;
@@ -705,6 +755,7 @@ pub async fn do_secure_pump(
Err(err) => { Err(err) => {
if !plant_config.ignore_current_error { if !plant_config.ignore_current_error {
info!("Error getting pump current: {err}"); info!("Error getting pump current: {err}");
let err_msg = format!("MissingSensor: {err:?}");
log( log(
LogMessage::PumpMissingSensorCurrent, LogMessage::PumpMissingSensorCurrent,
plant_id as u32, plant_id as u32,
@@ -712,7 +763,7 @@ pub async fn do_secure_pump(
"", "",
"", "",
); );
error = true; error = err_msg;
break; break;
} else { } else {
error!("Error getting pump current: {err}"); error!("Error getting pump current: {err}");
@@ -908,6 +959,7 @@ async fn pump_info(
median_current_ma: u16, median_current_ma: u16,
max_current_ma: u16, max_current_ma: u16,
min_current_ma: u16, min_current_ma: u16,
error: String,
) { ) {
let pump_info = PumpInfo { let pump_info = PumpInfo {
enabled: pump_active, enabled: pump_active,
@@ -915,6 +967,7 @@ async fn pump_info(
median_current_ma, median_current_ma,
max_current_ma, max_current_ma,
min_current_ma, min_current_ma,
error,
}; };
let pump_topic = format!("/pump{}", plant_id + 1); let pump_topic = format!("/pump{}", plant_id + 1);

View File

@@ -98,6 +98,7 @@ export enum BoardVersion {
export interface BoardHardware { export interface BoardHardware {
board: BoardVersion, board: BoardVersion,
battery: BatteryBoardVersion, battery: BatteryBoardVersion,
pump_corrosion_protection: boolean,
} }
export interface PlantControllerConfig { export interface PlantControllerConfig {

View File

@@ -18,3 +18,7 @@
<select class="boardvalue" id="hardware_battery_value"> <select class="boardvalue" id="hardware_battery_value">
</select> </select>
</div> </div>
<div class="flexcontainer">
<div class="boardkey">Pump corrosion protection (weekly)</div>
<input type="checkbox" id="hardware_pump_corrosion_protection">
</div>

View File

@@ -4,6 +4,7 @@ import {BatteryBoardVersion, BoardHardware, BoardVersion} from "./api";
export class HardwareConfigView { export class HardwareConfigView {
private readonly hardware_board_value: HTMLSelectElement; private readonly hardware_board_value: HTMLSelectElement;
private readonly hardware_battery_value: HTMLSelectElement; private readonly hardware_battery_value: HTMLSelectElement;
private readonly hardware_pump_corrosion_protection: HTMLInputElement;
constructor(controller:Controller){ constructor(controller:Controller){
(document.getElementById("hardwareview") as HTMLElement).innerHTML = require('./hardware.html') as string; (document.getElementById("hardwareview") as HTMLElement).innerHTML = require('./hardware.html') as string;
@@ -29,17 +30,22 @@ export class HardwareConfigView {
option.innerText = version.toString(); option.innerText = version.toString();
this.hardware_battery_value.appendChild(option); 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) { setConfig(hardware: BoardHardware) {
this.hardware_board_value.value = hardware.board.toString() this.hardware_board_value.value = hardware.board.toString()
this.hardware_battery_value.value = hardware.battery.toString() this.hardware_battery_value.value = hardware.battery.toString()
this.hardware_pump_corrosion_protection.checked = hardware.pump_corrosion_protection
} }
getConfig(): BoardHardware { getConfig(): BoardHardware {
return { return {
board : BoardVersion[this.hardware_board_value.value as keyof typeof BoardVersion], board : BoardVersion[this.hardware_board_value.value as keyof typeof BoardVersion],
battery : BatteryBoardVersion[this.hardware_battery_value.value as keyof typeof BatteryBoardVersion], battery : BatteryBoardVersion[this.hardware_battery_value.value as keyof typeof BatteryBoardVersion],
pump_corrosion_protection : this.hardware_pump_corrosion_protection.checked,
} }
} }
} }