Switch println! to log::info in water.rs; add dynamic deep-sleep adjustment based on plant state; update heap metrics and OTA UI.

This commit is contained in:
Kai Börnert
2026-04-28 15:19:56 +02:00
parent e0b8acd55c
commit e802af2a7a
6 changed files with 111 additions and 26 deletions

View File

@@ -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<f32, FatError> = loop {

View File

@@ -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<u32> = 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,
}

View File

@@ -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<Tz>,
) -> Option<u32> {
if matches!(plant_conf.mode, PlantWateringMode::Off) {
return None;
}
let mut earliest: Option<u32> = 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()
}

View File

@@ -185,7 +185,6 @@ export interface VersionInfo {
slot1_state: string,
heap_total: number,
heap_used: number,
heap_free: number,
heap_max_used: number,
}

View File

@@ -55,8 +55,8 @@
<div></div>
</div>
<div class="flexcontainer">
<span class="otakey">Free:</span>
<span class="otavalue" id="heap_free"></span>
<span class="otakey">Minimum Free:</span>
<span class="otavalue" id="heap_min_free"></span>
</div>
<div class="flexcontainer">
<span class="otakey">Used:</span>

View File

@@ -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);