diff --git a/Software/MainBoard/rust/src/hal/water.rs b/Software/MainBoard/rust/src/hal/water.rs index 5f3b457..9098bfa 100644 --- a/Software/MainBoard/rust/src/hal/water.rs +++ b/Software/MainBoard/rust/src/hal/water.rs @@ -11,6 +11,7 @@ use esp_hal::pcnt::unit::Unit; use esp_hal::peripherals::GPIO5; use esp_hal::Async; use esp_println::println; +use log::info; use onewire::{ds18b20, Device, DeviceSearch, OneWire, DS18B20}; unsafe impl Send for TankSensor<'_> {} @@ -90,9 +91,9 @@ impl<'a> TankSensor<'a> { let mut delay = Delay::new(); let presence = self.one_wire_bus.reset(&mut delay)?; - println!("OneWire: reset presence pulse = {}", presence); + info!("OneWire: reset presence pulse = {}", presence); if !presence { - println!("OneWire: no device responded to reset — check pull-up resistor and wiring"); + info!("OneWire: no device responded to reset — check pull-up resistor and wiring"); } let mut search = DeviceSearch::new(); @@ -100,7 +101,7 @@ impl<'a> TankSensor<'a> { let mut devices_found = 0u8; while let Some(device) = self.one_wire_bus.search_next(&mut search, &mut delay)? { devices_found += 1; - println!( + info!( "OneWire: found device #{} family=0x{:02X} addr={:02X?}", devices_found, device.address[0], device.address ); @@ -108,16 +109,16 @@ impl<'a> TankSensor<'a> { water_temp_sensor = Some(device); break; } else { - println!("OneWire: skipping device — not a DS18B20 (family 0x{:02X} != 0x{:02X})", device.address[0], ds18b20::FAMILY_CODE); + info!("OneWire: skipping device — not a DS18B20 (family 0x{:02X} != 0x{:02X})", device.address[0], ds18b20::FAMILY_CODE); } } if devices_found == 0 { - println!("OneWire: search found zero devices on the bus"); + info!("OneWire: search found zero devices on the bus"); } match water_temp_sensor { Some(device) => { - println!("Found one wire device: {:?}", device); + info!("Found one wire device: {:?}", device); let mut water_temp_sensor = DS18B20::new(device)?; let water_temp: Result = loop { diff --git a/Software/MainBoard/rust/src/main.rs b/Software/MainBoard/rust/src/main.rs index d86bb12..7438d1b 100644 --- a/Software/MainBoard/rust/src/main.rs +++ b/Software/MainBoard/rust/src/main.rs @@ -641,18 +641,44 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { .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 base_duration: u32 = if is_day { 20 } else { 60 }; + + // Shorten sleep if any plant has a pending action sooner than the base duration + let min_plant_wakeup: Option = plantstate + .iter() + .zip(&board.board_hal.get_config().plants) + .filter_map(|(state, conf)| { + state.minutes_until_actionable(conf, &timezone_time) + }) + .min(); + + let duration = min_plant_wakeup + .map(|m| base_duration.min(m.max(1))) + .unwrap_or(base_duration); + + if duration < base_duration { + let msg = format!("reduced {}m", duration); + let _ = board + .board_hal + .get_esp() + .mqtt_publish("/deepsleep", &msg) + .await; + } else if is_day { + let _ = board + .board_hal + .get_esp() + .mqtt_publish("/deepsleep", "normal 20m") + .await; + } else { + let _ = board + .board_hal + .get_esp() + .mqtt_publish("/deepsleep", "night 1h") + .await; + } + + duration }; let _ = board @@ -1336,7 +1362,6 @@ async fn get_version( slot1_state: format!("{:?}", board.slot1_state), heap_total: heap.size, heap_used: heap.current_usage, - heap_free: heap.size.saturating_sub(heap.current_usage), heap_max_used: heap.max_usage, } } @@ -1350,6 +1375,5 @@ struct VersionInfo { slot1_state: String, heap_total: usize, heap_used: usize, - heap_free: usize, heap_max_used: usize, } diff --git a/Software/MainBoard/rust/src/plant_state.rs b/Software/MainBoard/rust/src/plant_state.rs index e612895..91c3a0c 100644 --- a/Software/MainBoard/rust/src/plant_state.rs +++ b/Software/MainBoard/rust/src/plant_state.rs @@ -1,6 +1,6 @@ use crate::hal::Moistures; use crate::{config::PlantConfig, hal::HAL, in_time_range}; -use chrono::{DateTime, TimeDelta, Utc}; +use chrono::{DateTime, TimeDelta, Timelike, Utc}; use chrono_tz::Tz; use serde::{Deserialize, Serialize}; @@ -192,6 +192,67 @@ impl PlantState { }) } + /// Returns how many minutes until this plant becomes actionable (could be watered), + /// or `None` if no action is pending within any reasonable horizon. + /// + /// Used to dynamically shorten deep-sleep so cooldowns are not prolonged. + pub fn minutes_until_actionable( + &self, + plant_conf: &PlantConfig, + current_time: &DateTime, + ) -> Option { + if matches!(plant_conf.mode, PlantWateringMode::Off) { + return None; + } + + let mut earliest: Option = None; + + // When does the current cooldown expire? + if self.pump_in_timeout(plant_conf, current_time) { + if let Some(last_pump) = self.pump.previous_pump { + if let Some(expiry) = last_pump + .checked_add_signed(TimeDelta::minutes(plant_conf.pump_cooldown_min.into())) + { + let diff = expiry.signed_duration_since(current_time.with_timezone(&Utc)); + let mins = diff.num_minutes().max(0) as u32; + earliest = Some(earliest.map_or(mins, |e| e.min(mins))); + } + } + } + + // For moisture-based modes: when does the watering window open, if the plant is dry + // but currently outside its time window (and not in cooldown)? + if matches!( + plant_conf.mode, + PlantWateringMode::TargetMoisture | PlantWateringMode::MinMoisture + ) { + let (moisture, _) = self.plant_moisture(); + if let Some(moisture) = moisture { + if moisture < plant_conf.target_moisture + && !self.pump_in_timeout(plant_conf, current_time) + && !in_time_range( + current_time, + plant_conf.pump_hour_start, + plant_conf.pump_hour_end, + ) + { + // in_time_range uses `hour > start`, so the window opens at start+1:00 + let cur_hour = current_time.hour() as u32; + let cur_minute = current_time.minute() as u32; + let open_at_hour = (plant_conf.pump_hour_start as u32 + 1) % 24; + let mins = if open_at_hour > cur_hour { + (open_at_hour - cur_hour) * 60 - cur_minute + } else { + (24 - cur_hour + open_at_hour) * 60 - cur_minute + }; + earliest = Some(earliest.map_or(mins, |e| e.min(mins))); + } + } + } + + earliest + } + pub fn is_err(&self) -> bool { self.sensor_a.is_err().is_some() || self.sensor_b.is_err().is_some() } diff --git a/Software/MainBoard/rust/src_webpack/src/api.ts b/Software/MainBoard/rust/src_webpack/src/api.ts index 7e2aa68..e72260b 100644 --- a/Software/MainBoard/rust/src_webpack/src/api.ts +++ b/Software/MainBoard/rust/src_webpack/src/api.ts @@ -185,7 +185,6 @@ export interface VersionInfo { slot1_state: string, heap_total: number, heap_used: number, - heap_free: number, heap_max_used: number, } diff --git a/Software/MainBoard/rust/src_webpack/src/ota.html b/Software/MainBoard/rust/src_webpack/src/ota.html index 9c95f8e..286317a 100644 --- a/Software/MainBoard/rust/src_webpack/src/ota.html +++ b/Software/MainBoard/rust/src_webpack/src/ota.html @@ -55,8 +55,8 @@
- Free: - + Minimum Free: +
Used: diff --git a/Software/MainBoard/rust/src_webpack/src/ota.ts b/Software/MainBoard/rust/src_webpack/src/ota.ts index b7c6f69..7049a0f 100644 --- a/Software/MainBoard/rust/src_webpack/src/ota.ts +++ b/Software/MainBoard/rust/src_webpack/src/ota.ts @@ -12,7 +12,7 @@ export class OTAView { readonly firmware_partition: HTMLDivElement; readonly firmware_state0: HTMLDivElement; readonly firmware_state1: HTMLDivElement; - readonly heap_free: HTMLDivElement; + readonly heap_min_free: HTMLDivElement; readonly heap_used: HTMLDivElement; readonly heap_total: HTMLDivElement; readonly heap_max_used: HTMLDivElement; @@ -28,7 +28,7 @@ export class OTAView { this.firmware_partition = document.getElementById("firmware_partition") as HTMLDivElement; this.firmware_state0 = document.getElementById("firmware_state0") as HTMLDivElement; this.firmware_state1 = document.getElementById("firmware_state1") as HTMLDivElement; - this.heap_free = document.getElementById("heap_free") as HTMLDivElement; + this.heap_min_free = document.getElementById("heap_min_free") as HTMLDivElement; this.heap_used = document.getElementById("heap_used") as HTMLDivElement; this.heap_total = document.getElementById("heap_total") as HTMLDivElement; this.heap_max_used = document.getElementById("heap_max_used") as HTMLDivElement; @@ -59,7 +59,7 @@ export class OTAView { this.firmware_partition.innerText = versionInfo.current; this.firmware_state0.innerText = versionInfo.slot0_state; this.firmware_state1.innerText = versionInfo.slot1_state; - this.heap_free.innerText = fmtBytes(versionInfo.heap_free); + this.heap_min_free.innerText = fmtBytes(versionInfo.heap_total - versionInfo.heap_max_used); this.heap_used.innerText = fmtBytes(versionInfo.heap_used); this.heap_total.innerText = fmtBytes(versionInfo.heap_total); this.heap_max_used.innerText = fmtBytes(versionInfo.heap_max_used);