add ability to override frequency per plant and adjust timezone, fix missing workhour for plants
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
@@ -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>>,
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user