From db0f7daa4c6a44af33246a24874257480ae2cd11 Mon Sep 17 00:00:00 2001 From: Empire Date: Thu, 30 Apr 2026 22:09:04 +0200 Subject: [PATCH] feat: add fertilizer cooldown functionality with web UI, HAL integration, and configuration support --- Software/MainBoard/rust/src/config.rs | 2 ++ Software/MainBoard/rust/src/hal/esp.rs | 10 ++++++ Software/MainBoard/rust/src/log/mod.rs | 4 +++ Software/MainBoard/rust/src/main.rs | 32 +++++++++++++++---- Software/MainBoard/rust/src/plant_state.rs | 7 ++++ .../MainBoard/rust/src_webpack/src/api.ts | 1 + .../MainBoard/rust/src_webpack/src/plant.html | 5 +++ .../MainBoard/rust/src_webpack/src/plant.ts | 8 +++++ 8 files changed, 63 insertions(+), 6 deletions(-) diff --git a/Software/MainBoard/rust/src/config.rs b/Software/MainBoard/rust/src/config.rs index b74bdb3..5ae2d5d 100644 --- a/Software/MainBoard/rust/src/config.rs +++ b/Software/MainBoard/rust/src/config.rs @@ -130,6 +130,7 @@ pub struct PlantConfig { pub max_pump_current_ma: u16, pub ignore_current_error: bool, pub fertilizer_s: u16, + pub fertilizer_cooldown_min: u16, } impl Default for PlantConfig { @@ -152,6 +153,7 @@ impl Default for PlantConfig { max_pump_current_ma: 3000, ignore_current_error: true, fertilizer_s: 0, + fertilizer_cooldown_min: 1440, // 1 day default } } } diff --git a/Software/MainBoard/rust/src/hal/esp.rs b/Software/MainBoard/rust/src/hal/esp.rs index 503cd52..56e0e2d 100644 --- a/Software/MainBoard/rust/src/hal/esp.rs +++ b/Software/MainBoard/rust/src/hal/esp.rs @@ -51,6 +51,8 @@ static mut LAST_WATERING_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT]; #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))] static mut CONSECUTIVE_WATERING_PLANT: [u32; PLANT_COUNT] = [0; PLANT_COUNT]; #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))] +static mut LAST_FERTILIZER_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT]; +#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))] static mut LOW_VOLTAGE_DETECTED: i8 = 0; #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))] static mut RESTART_TO_CONF: i8 = 0; @@ -342,6 +344,14 @@ impl Esp<'_> { LAST_WATERING_TIMESTAMP[plant] = time.timestamp_millis(); } } + pub(crate) fn last_fertilizer_time(&self, plant: usize) -> i64 { + unsafe { LAST_FERTILIZER_TIMESTAMP[plant] } + } + pub(crate) fn store_last_fertilizer_time(&mut self, plant: usize, time: DateTime) { + unsafe { + LAST_FERTILIZER_TIMESTAMP[plant] = time.timestamp_millis(); + } + } pub(crate) fn set_low_voltage_in_cycle(&mut self) { unsafe { LOW_VOLTAGE_DETECTED = 1; diff --git a/Software/MainBoard/rust/src/log/mod.rs b/Software/MainBoard/rust/src/log/mod.rs index 24604ff..a1df0dd 100644 --- a/Software/MainBoard/rust/src/log/mod.rs +++ b/Software/MainBoard/rust/src/log/mod.rs @@ -311,6 +311,10 @@ pub enum LogMessage { PumpOpenLoopCurrent, #[strum(serialize = "Pump Open current sensor required but did not work: ${number_a}")] PumpMissingSensorCurrent, + #[strum( + serialize = "Fertilizer applied for ${number_a}s on plant ${number_b} (last application ${txt_short} minutes ago)" + )] + FertilizerApplied, #[strum(serialize = "MPPT Current sensor could not be reached")] MPPTError, #[strum( diff --git a/Software/MainBoard/rust/src/main.rs b/Software/MainBoard/rust/src/main.rs index 9b42c27..7619bff 100644 --- a/Software/MainBoard/rust/src/main.rs +++ b/Software/MainBoard/rust/src/main.rs @@ -715,13 +715,33 @@ pub async fn do_secure_pump( let mut pump_time_ms: u32 = 0; if !dry_run { - // Run fertilizer pump first if configured + // Run fertilizer pump first if configured and not in cooldown if plant_config.fertilizer_s > 0 { - info!("Starting fertilizer pump for {} seconds", plant_config.fertilizer_s); - board.board_hal.extra2(true).await?; - Timer::after_millis(plant_config.fertilizer_s as u64 * 1000).await; - board.board_hal.extra2(false).await?; - info!("Fertilizer pump stopped"); + let current_time = board.board_hal.get_time().await; + let last_fertilizer = board.board_hal.get_esp().last_fertilizer_time(plant_id); + let elapsed_minutes = (current_time.timestamp() - last_fertilizer) / 60; + + if elapsed_minutes >= plant_config.fertilizer_cooldown_min as i64 { + info!("Starting fertilizer pump for {} seconds (last fertilizer was {} minutes ago)", + plant_config.fertilizer_s, elapsed_minutes); + log( + LogMessage::FertilizerApplied, + plant_config.fertilizer_s as u32, + (plant_id + 1) as u32, + &elapsed_minutes.to_string(), + "", + ); + board.board_hal.extra2(true).await?; + Timer::after_millis(plant_config.fertilizer_s as u64 * 1000).await; + board.board_hal.extra2(false).await?; + info!("Fertilizer pump stopped"); + + // Store the current time as last fertilizer time + board.board_hal.get_esp().store_last_fertilizer_time(plant_id, current_time); + } else { + let remaining_minutes = plant_config.fertilizer_cooldown_min as i64 - elapsed_minutes; + info!("Skipping fertilizer (cooldown: {} minutes remaining)", remaining_minutes); + } } board.board_hal.get_tank_sensor()?.reset_flow_meter(); diff --git a/Software/MainBoard/rust/src/plant_state.rs b/Software/MainBoard/rust/src/plant_state.rs index e612895..b8d6d8b 100644 --- a/Software/MainBoard/rust/src/plant_state.rs +++ b/Software/MainBoard/rust/src/plant_state.rs @@ -89,6 +89,8 @@ pub struct PlantState { pub sensor_a_firmware_build_minutes: Option, /// Last known firmware build timestamp for sensor B. pub sensor_b_firmware_build_minutes: Option, + /// Last time fertilizer was applied (Unix timestamp in seconds). + pub last_fertilizer_time: i64, } fn map_range_moisture( @@ -162,6 +164,7 @@ impl PlantState { let previous_pump = board.board_hal.get_esp().last_pump_time(plant_id); let consecutive_pump_count = board.board_hal.get_esp().consecutive_pump_count(plant_id); + let last_fertilizer_time = board.board_hal.get_esp().last_fertilizer_time(plant_id); let (a_builds, b_builds) = board.board_hal.get_sensor_build_minutes(); let state = Self { sensor_a, @@ -172,6 +175,7 @@ impl PlantState { }, sensor_a_firmware_build_minutes: a_builds[plant_id], sensor_b_firmware_build_minutes: b_builds[plant_id], + last_fertilizer_time, }; if state.is_err() { let _ = board.board_hal.fault(plant_id, true).await; @@ -296,6 +300,7 @@ impl PlantState { }, sensor_a_firmware_build_minutes: self.sensor_a_firmware_build_minutes, sensor_b_firmware_build_minutes: self.sensor_b_firmware_build_minutes, + last_fertilizer_time: self.last_fertilizer_time } } } @@ -328,4 +333,6 @@ pub struct PlantInfo<'a> { sensor_a_firmware_build_minutes: Option, /// firmware build timestamp of sensor B (minutes since Unix epoch); None if unknown sensor_b_firmware_build_minutes: Option, + /// last time when fertilizer was applied + last_fertilizer_time: i64, } diff --git a/Software/MainBoard/rust/src_webpack/src/api.ts b/Software/MainBoard/rust/src_webpack/src/api.ts index 1bbb99b..ec0e4fa 100644 --- a/Software/MainBoard/rust/src_webpack/src/api.ts +++ b/Software/MainBoard/rust/src_webpack/src/api.ts @@ -129,6 +129,7 @@ export interface PlantConfig { pump_time_s: number, pump_cooldown_min: number, fertilizer_s: number, + fertilizer_cooldown_min: number, pump_hour_start: number, pump_hour_end: number, pump_limit_ml: number, diff --git a/Software/MainBoard/rust/src_webpack/src/plant.html b/Software/MainBoard/rust/src_webpack/src/plant.html index 9b0f92b..1ef21b6 100644 --- a/Software/MainBoard/rust/src_webpack/src/plant.html +++ b/Software/MainBoard/rust/src_webpack/src/plant.html @@ -83,6 +83,11 @@ +
+
Fertilizer Cooldown (m):
+ +
"Pump Hour Start":
diff --git a/Software/MainBoard/rust/src_webpack/src/plant.ts b/Software/MainBoard/rust/src_webpack/src/plant.ts index 6c762fb..4c763fc 100644 --- a/Software/MainBoard/rust/src_webpack/src/plant.ts +++ b/Software/MainBoard/rust/src_webpack/src/plant.ts @@ -80,6 +80,7 @@ export class PlantView { private readonly pumpTimeS: HTMLInputElement; private readonly pumpCooldown: HTMLInputElement; private readonly fertilizerS: HTMLInputElement; + private readonly fertilizerCooldownMin: HTMLInputElement; private readonly pumpHourStart: HTMLSelectElement; private readonly pumpHourEnd: HTMLSelectElement; private readonly sensorAInstalled: HTMLInputElement; @@ -186,6 +187,11 @@ export class PlantView { controller.configChanged() } + this.fertilizerCooldownMin = document.getElementById("plant_" + plantId + "_fertilizer_cooldown_min") as HTMLInputElement; + this.fertilizerCooldownMin.onchange = function () { + controller.configChanged() + } + this.pumpHourStart = document.getElementById("plant_" + plantId + "_pump_hour_start") as HTMLSelectElement; this.pumpHourStart.onchange = function () { controller.configChanged() @@ -335,6 +341,7 @@ export class PlantView { this.pumpTimeS.value = plantConfig.pump_time_s.toString(); this.pumpCooldown.value = plantConfig.pump_cooldown_min.toString(); this.fertilizerS.value = plantConfig.fertilizer_s?.toString() || "0"; + this.fertilizerCooldownMin.value = plantConfig.fertilizer_cooldown_min?.toString() || "1440"; this.pumpHourStart.value = plantConfig.pump_hour_start.toString(); this.pumpHourEnd.value = plantConfig.pump_hour_end.toString(); this.sensorBInstalled.checked = plantConfig.sensor_b; @@ -363,6 +370,7 @@ export class PlantView { pump_limit_ml: 5000, pump_cooldown_min: this.pumpCooldown.valueAsNumber, fertilizer_s: this.fertilizerS.valueAsNumber || 0, + fertilizer_cooldown_min: this.fertilizerCooldownMin.valueAsNumber || 1440, pump_hour_start: +this.pumpHourStart.value, pump_hour_end: +this.pumpHourEnd.value, sensor_b: this.sensorBInstalled.checked,