add ability to override frequency per plant and adjust timezone, fix missing workhour for plants

This commit is contained in:
2025-05-06 22:33:33 +02:00
parent f8274ea7a8
commit 5fe1dc8f40
10 changed files with 257 additions and 94 deletions

View File

@@ -79,6 +79,7 @@ pub struct PlantControllerConfig {
pub tank: TankConfig,
pub night_lamp: NightLampConfig,
pub plants: [PlantConfig; PLANT_COUNT],
pub timezone: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
@@ -93,7 +94,10 @@ pub struct PlantConfig {
pub sensor_a: bool,
pub sensor_b: bool,
pub max_consecutive_pump_count: u8,
pub moisture_sensor_min_frequency: Option<f32>, // Optional min frequency
pub moisture_sensor_max_frequency: Option<f32>, // Optional max frequency
}
impl Default for PlantConfig {
fn default() -> Self {
Self {
@@ -106,6 +110,8 @@ impl Default for PlantConfig {
sensor_a: true,
sensor_b: false,
max_consecutive_pump_count: 10,
moisture_sensor_min_frequency: None, // No override by default
moisture_sensor_max_frequency: None, // No override by default
}
}
}
}

View File

@@ -5,8 +5,8 @@ use std::{
use anyhow::{bail, Result};
use chrono::{DateTime, Datelike, Timelike};
use chrono_tz::{Europe::Berlin, Tz};
use chrono_tz::Tz::UTC;
use chrono_tz::Tz;
use esp_idf_hal::delay::Delay;
use esp_idf_sys::{
esp_ota_get_app_partition_count, esp_ota_get_running_partition, esp_ota_get_state_partition,
@@ -32,8 +32,6 @@ pub mod util;
use plant_state::PlantState;
use tank::*;
const TIME_ZONE: Tz = Berlin;
pub static BOARD_ACCESS: Lazy<Mutex<PlantCtrlBoard>> = Lazy::new(|| PlantHal::create().unwrap());
pub static STAY_ALIVE: Lazy<AtomicBool> = Lazy::new(|| AtomicBool::new(false));
@@ -280,11 +278,20 @@ fn safe_main() -> anyhow::Result<()> {
}
}
let timezone_time = cur.with_timezone(&TIME_ZONE);
let timezone = match &config.timezone {
Some(tz_str) => tz_str.parse::<Tz>().unwrap_or_else(|_| {
println!("Invalid timezone '{}', falling back to UTC", tz_str);
UTC
}),
None => UTC, // Fallback to UTC if no timezone is set
};
let timezone_time = cur.with_timezone(&timezone);
println!(
"Running logic at utc {} and {} {}",
cur,
TIME_ZONE.name(),
timezone.name(),
timezone_time
);
@@ -579,28 +586,51 @@ fn publish_battery_state(
fn wait_infinity(wait_type: WaitType, reboot_now: Arc<AtomicBool>) -> ! {
let delay = wait_type.blink_pattern();
let mut led_count = 8;
let mut pattern_step = 0;
loop {
// TODO implement actually different blink patterns instead of modulating blink duration
if wait_type == WaitType::MissingConfig {
led_count %= 8;
led_count += 1;
};
unsafe {
BOARD_ACCESS.lock().unwrap().update_charge_indicator();
//do not trigger watchdog
for i in 0..8 {
BOARD_ACCESS.lock().unwrap().fault(i, i < led_count);
let mut lock = BOARD_ACCESS.lock().unwrap();
lock.update_charge_indicator();
match wait_type {
WaitType::MissingConfig => {
// Keep existing behavior: circular filling pattern
led_count %= 8;
led_count += 1;
for i in 0..8 {
lock.fault(i, i < led_count);
}
}
WaitType::ConfigButton => {
// Alternating pattern: 1010 1010 -> 0101 0101
pattern_step = (pattern_step + 1) % 2;
for i in 0..8 {
lock.fault(i, (i + pattern_step) % 2 == 0);
}
}
WaitType::MqttConfig => {
// Moving dot pattern
pattern_step = (pattern_step + 1) % 8;
for i in 0..8 {
lock.fault(i, i == pattern_step);
}
}
}
BOARD_ACCESS.lock().unwrap().general_fault(true);
lock.general_fault(true);
drop(lock);
vTaskDelay(delay);
BOARD_ACCESS.lock().unwrap().general_fault(false);
//TODO move locking outside of loop and drop afterwards
let mut lock = BOARD_ACCESS.lock().unwrap();
lock.general_fault(false);
// Clear all LEDs
for i in 0..8 {
BOARD_ACCESS.lock().unwrap().fault(i, false);
lock.fault(i, false);
}
drop(lock);
vTaskDelay(delay);
if wait_type == WaitType::MqttConfig {
if !STAY_ALIVE.load(std::sync::atomic::Ordering::Relaxed) {
reboot_now.store(true, std::sync::atomic::Ordering::Relaxed);

View File

@@ -7,7 +7,7 @@ use crate::{
in_time_range, plant_hal,
};
const MOIST_SENSOR_MAX_FREQUENCY: f32 = 5500.; // 60kHz (500Hz margin)
const MOIST_SENSOR_MAX_FREQUENCY: f32 = 6500.; // 60kHz (500Hz margin)
const MOIST_SENSOR_MIN_FREQUENCY: f32 = 150.; // this is really really dry, think like cactus levels
#[derive(Debug, PartialEq, Serialize)]
@@ -87,23 +87,30 @@ pub struct PlantState {
pub pump: PumpState,
}
fn map_range_moisture(s: f32) -> Result<f32, MoistureSensorError> {
if s < MOIST_SENSOR_MIN_FREQUENCY {
fn map_range_moisture(
s: f32,
min_frequency: Option<f32>,
max_frequency: Option<f32>,
) -> Result<f32, MoistureSensorError> {
// Use overrides if provided, otherwise fallback to defaults
let min_freq = min_frequency.unwrap_or(MOIST_SENSOR_MIN_FREQUENCY);
let max_freq = max_frequency.unwrap_or(MOIST_SENSOR_MAX_FREQUENCY);
if s < min_freq {
return Err(MoistureSensorError::OpenLoop {
hz: s,
min: MOIST_SENSOR_MIN_FREQUENCY,
min: min_freq,
});
}
if s > MOIST_SENSOR_MAX_FREQUENCY {
if s > max_freq {
return Err(MoistureSensorError::ShortCircuit {
hz: s,
max: MOIST_SENSOR_MAX_FREQUENCY,
max: max_freq,
});
}
let moisture_percent = (s - MOIST_SENSOR_MIN_FREQUENCY) * 100.0
/ (MOIST_SENSOR_MAX_FREQUENCY - MOIST_SENSOR_MIN_FREQUENCY);
let moisture_percent = (s - min_freq) * 100.0 / (max_freq - min_freq);
return Ok(moisture_percent);
Ok(moisture_percent)
}
impl PlantState {
@@ -114,7 +121,11 @@ impl PlantState {
) -> Self {
let sensor_a = if config.sensor_a {
match board.measure_moisture_hz(plant_id, plant_hal::Sensor::A) {
Ok(raw) => match map_range_moisture(raw) {
Ok(raw) => match map_range_moisture(
raw,
config.moisture_sensor_min_frequency,
config.moisture_sensor_max_frequency,
) {
Ok(moisture_percent) => MoistureSensorState::MoistureValue {
raw_hz: raw,
moisture_percent,
@@ -128,9 +139,14 @@ impl PlantState {
} else {
MoistureSensorState::Disabled
};
let sensor_b = if config.sensor_b {
match board.measure_moisture_hz(plant_id, plant_hal::Sensor::B) {
Ok(raw) => match map_range_moisture(raw) {
Ok(raw) => match map_range_moisture(
raw,
config.moisture_sensor_min_frequency,
config.moisture_sensor_max_frequency,
) {
Ok(moisture_percent) => MoistureSensorState::MoistureValue {
raw_hz: raw,
moisture_percent,
@@ -144,6 +160,7 @@ impl PlantState {
} else {
MoistureSensorState::Disabled
};
let previous_pump = board.last_pump_time(plant_id);
let consecutive_pump_count = board.consecutive_pump_count(plant_id);
let state = Self {
@@ -210,7 +227,11 @@ impl PlantState {
false
} else {
if moisture_percent < plant_conf.target_moisture {
true
in_time_range(
current_time,
plant_conf.pump_hour_start,
plant_conf.pump_hour_end,
)
} else {
false
}
@@ -293,4 +314,4 @@ pub struct PlantInfo<'a> {
last_pump: Option<DateTime<Tz>>,
/// next time when pump should activate
next_pump: Option<DateTime<Tz>>,
}
}

View File

@@ -77,6 +77,39 @@ fn write_time(
anyhow::Ok(None)
}
fn get_time(
_request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
let mut board = BOARD_ACCESS.lock().unwrap();
let native = board
.time()
.and_then(|t| Ok(t.to_rfc3339()))
.unwrap_or("error".to_string());
let rtc = board
.get_rtc_time()
.and_then(|t| Ok(t.to_rfc3339()))
.unwrap_or("error".to_string());
let data = LoadData {
rtc: rtc.as_str(),
native: native.as_str(),
};
let json = serde_json::to_string(&data)?;
anyhow::Ok(Some(json))
}
fn get_timezones(
_request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
// Get all timezones using chrono-tz
let timezones: Vec<&'static str> = chrono_tz::TZ_VARIANTS.iter().map(|tz| tz.name()).collect();
// Convert to JSON
let json = serde_json::to_string(&timezones)?;
anyhow::Ok(Some(json))
}
fn get_live_moisture(
_request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
@@ -106,27 +139,7 @@ fn get_live_moisture(
anyhow::Ok(Some(json))
}
fn get_data(
_request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
let mut board = BOARD_ACCESS.lock().unwrap();
let native = board
.time()
.and_then(|t| Ok(t.to_rfc3339()))
.unwrap_or("error".to_string());
let rtc = board
.get_rtc_time()
.and_then(|t| Ok(t.to_rfc3339()))
.unwrap_or("error".to_string());
let data = LoadData {
rtc: rtc.as_str(),
native: native.as_str(),
};
let json = serde_json::to_string(&data)?;
anyhow::Ok(Some(json))
}
fn get_config(
_request: &mut Request<&mut EspHttpConnection>,
@@ -362,6 +375,8 @@ fn flash_bq(filename: &str, dryrun: bool) -> anyhow::Result<()> {
return anyhow::Ok(());
}
fn query_param(uri: &str, param_name: &str) -> Option<std::string::String> {
println!("{uri} get {param_name}");
let parsed = Url::parse(&format!("http://127.0.0.1/{uri}")).unwrap();
@@ -403,7 +418,7 @@ pub fn httpd(reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
.unwrap();
server
.fn_handler("/time", Method::Get, |request| {
handle_error_to500(request, get_data)
handle_error_to500(request, get_time)
})
.unwrap();
server
@@ -658,8 +673,14 @@ pub fn httpd(reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
})
.unwrap();
server
.fn_handler("/timezones", Method::Get, move |request| {
handle_error_to500(request, get_timezones)
})
.unwrap();
server
}
fn cors_response(
request: Request<&mut EspHttpConnection>,
status: u16,
@@ -724,4 +745,4 @@ fn read_up_to_bytes_from_request(
let allvec = data_store.concat();
println!("Raw data {}", from_utf8(&allvec)?);
Ok(allvec)
}
}