diff --git a/rust/src/config.rs b/rust/src/config.rs index f716189..847bcc7 100644 --- a/rust/src/config.rs +++ b/rust/src/config.rs @@ -79,6 +79,7 @@ pub struct PlantControllerConfig { pub tank: TankConfig, pub night_lamp: NightLampConfig, pub plants: [PlantConfig; PLANT_COUNT], + pub timezone: Option, } #[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, // Optional min frequency + pub moisture_sensor_max_frequency: Option, // 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 } } -} +} \ No newline at end of file diff --git a/rust/src/main.rs b/rust/src/main.rs index 20d7161..f811190 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -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> = Lazy::new(|| PlantHal::create().unwrap()); pub static STAY_ALIVE: Lazy = 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::().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) -> ! { 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); diff --git a/rust/src/plant_state.rs b/rust/src/plant_state.rs index 98b3fb1..de94140 100644 --- a/rust/src/plant_state.rs +++ b/rust/src/plant_state.rs @@ -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 { - if s < MOIST_SENSOR_MIN_FREQUENCY { +fn map_range_moisture( + s: f32, + min_frequency: Option, + max_frequency: Option, +) -> Result { + // 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>, /// next time when pump should activate next_pump: Option>, -} +} \ No newline at end of file diff --git a/rust/src/webserver/webserver.rs b/rust/src/webserver/webserver.rs index f47a0fa..38ddb0f 100644 --- a/rust/src/webserver/webserver.rs +++ b/rust/src/webserver/webserver.rs @@ -77,6 +77,39 @@ fn write_time( anyhow::Ok(None) } +fn get_time( + _request: &mut Request<&mut EspHttpConnection>, +) -> Result, 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, 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, anyhow::Error> { @@ -106,27 +139,7 @@ fn get_live_moisture( anyhow::Ok(Some(json)) } -fn get_data( - _request: &mut Request<&mut EspHttpConnection>, -) -> Result, 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 { 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) -> Box> { .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) -> Box> { }) .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) -} +} \ No newline at end of file diff --git a/rust/src_webpack/src/api.ts b/rust/src_webpack/src/api.ts index 13bc265..09c34f3 100644 --- a/rust/src_webpack/src/api.ts +++ b/rust/src_webpack/src/api.ts @@ -69,6 +69,7 @@ interface PlantControllerConfig { tank: TankConfig, night_lamp: NightLampConfig, plants: PlantConfig[] + timezone?: string, } interface PlantConfig { @@ -80,6 +81,9 @@ interface PlantConfig { pump_hour_end: number, sensor_b: boolean, max_consecutive_pump_count: number, + moisture_sensor_min_frequency?: number; + moisture_sensor_max_frequency?: number; + } diff --git a/rust/src_webpack/src/main.ts b/rust/src_webpack/src/main.ts index 2ba04e2..e369a32 100644 --- a/rust/src_webpack/src/main.ts +++ b/rust/src_webpack/src/main.ts @@ -65,6 +65,17 @@ export class Controller { console.log(error); }); } + + populateTimezones(): Promise { + return fetch('/timezones') + .then(response => response.json()) + .then(json => json as string[]) + .then(timezones => { + controller.timeView.timezones(timezones) + }) + .catch(error => console.error('Error fetching timezones:', error)); + } + updateFileList() : Promise { return fetch(PUBLIC_URL + "/files") .then(response => response.json()) @@ -308,7 +319,8 @@ export class Controller { network: controller.networkView.getConfig(), tank: controller.tankView.getConfig(), night_lamp: controller.nightLampView.getConfig(), - plants: controller.plantViews.getConfig() + plants: controller.plantViews.getConfig(), + timezone: controller.timeView.getTimeZone() } } @@ -350,6 +362,7 @@ export class Controller { this.networkView.setConfig(current.network); this.nightLampView.setConfig(current.night_lamp); this.plantViews.setConfig(current.plants); + this.timeView.setTimeZone(current.timezone); } measure_moisture() { @@ -459,30 +472,34 @@ export class Controller { } const controller = new Controller(); controller.progressview.removeProgress("rebooting"); -controller.progressview.addProgress("initial", 0, "read rtc"); -controller.updateRTCData().then(_ => { - controller.progressview.addProgress("initial", 20, "read battery"); - controller.updateBatteryData().then(_ => { - controller.progressview.addProgress("initial", 40, "read config"); - controller.downloadConfig().then(_ => { - controller.progressview.addProgress("initial", 50, "read version"); - controller.version().then(_ => { - controller.progressview.addProgress("initial", 70, "read filelist"); - controller.updateFileList().then(_ => { - controller.progressview.addProgress("initial", 90, "read backupinfo"); - controller.getBackupInfo().then(_ => { - controller.loadLogLocaleConfig().then(_ => { - controller.loadTankInfo().then(_ => { - controller.progressview.removeProgress("initial") + +controller.progressview.addProgress("initial", 0, "read timezones"); +controller.populateTimezones().then(_ => { + controller.progressview.addProgress("initial", 10, "read rtc"); + controller.updateRTCData().then(_ => { + controller.progressview.addProgress("initial", 20, "read battery"); + controller.updateBatteryData().then(_ => { + controller.progressview.addProgress("initial", 40, "read config"); + controller.downloadConfig().then(_ => { + controller.progressview.addProgress("initial", 50, "read version"); + controller.version().then(_ => { + controller.progressview.addProgress("initial", 70, "read filelist"); + controller.updateFileList().then(_ => { + controller.progressview.addProgress("initial", 90, "read backupinfo"); + controller.getBackupInfo().then(_ => { + controller.loadLogLocaleConfig().then(_ => { + controller.loadTankInfo().then(_ => { + controller.progressview.removeProgress("initial") + }) }) }) }) }) - }) - }); - }) -}) -; + }); + }); + }); +}); + //controller.measure_moisture(); diff --git a/rust/src_webpack/src/plant.html b/rust/src_webpack/src/plant.html index 5151c27..8acdc1b 100644 --- a/rust/src_webpack/src/plant.html +++ b/rust/src_webpack/src/plant.html @@ -59,9 +59,17 @@
Warn Pump Count:
-
+
+
Min Frequency Override
+ +
+
+
Max Frequency Override
+ +
Sensor B installed:
diff --git a/rust/src_webpack/src/plant.ts b/rust/src_webpack/src/plant.ts index 93df81a..100e11e 100644 --- a/rust/src_webpack/src/plant.ts +++ b/rust/src_webpack/src/plant.ts @@ -1,4 +1,3 @@ - const PLANT_COUNT = 8; @@ -43,6 +42,8 @@ export class PlantViews { } export class PlantView { + private readonly moistureSensorMinFrequency: HTMLInputElement; + private readonly moistureSensorMaxFrequency: HTMLInputElement; private readonly plantId: number; private readonly plantDiv: HTMLDivElement; private readonly header: HTMLElement; @@ -136,6 +137,19 @@ export class PlantView { this.maxConsecutivePumpCount.onchange = function(){ controller.configChanged() } + + this.moistureSensorMinFrequency = document.getElementById("plant_"+plantId+"_min_frequency") as HTMLInputElement; + this.moistureSensorMinFrequency.onchange = function(){ + controller.configChanged() + } + this.moistureSensorMinFrequency.onchange = () => { + controller.configChanged(); + }; + + this.moistureSensorMaxFrequency = document.getElementById("plant_"+plantId+"_max_frequency") as HTMLInputElement; + this.moistureSensorMaxFrequency.onchange = () => { + controller.configChanged(); + }; } update(a: number, b: number) { @@ -159,23 +173,31 @@ export class PlantView { this.pumpCooldown.value = plantConfig.pump_cooldown_min.toString(); this.pumpHourStart.value = plantConfig.pump_hour_start.toString(); this.pumpHourEnd.value = plantConfig.pump_hour_end.toString(); - this.sensorBInstalled.checked = plantConfig.sensor_b + this.sensorBInstalled.checked = plantConfig.sensor_b; this.maxConsecutivePumpCount.value = plantConfig.max_consecutive_pump_count.toString(); + + // Set new fields + this.moistureSensorMinFrequency.value = + plantConfig.moisture_sensor_min_frequency?.toString() || ""; + this.moistureSensorMaxFrequency.value = + plantConfig.moisture_sensor_max_frequency?.toString() || ""; } - getConfig() :PlantConfig { - const rv:PlantConfig = { - mode: this.mode.value, - target_moisture: this.targetMoisture.valueAsNumber, - pump_time_s: this.pumpTimeS.valueAsNumber, - pump_cooldown_min: this.pumpCooldown.valueAsNumber, - pump_hour_start: +this.pumpHourStart.value, - pump_hour_end: +this.pumpHourEnd.value, - sensor_b: this.sensorBInstalled.checked, - max_consecutive_pump_count: this.maxConsecutivePumpCount.valueAsNumber - } - return rv - } + getConfig(): PlantConfig { + const rv: PlantConfig = { + mode: this.mode.value, + target_moisture: this.targetMoisture.valueAsNumber, + pump_time_s: this.pumpTimeS.valueAsNumber, + pump_cooldown_min: this.pumpCooldown.valueAsNumber, + pump_hour_start: +this.pumpHourStart.value, + pump_hour_end: +this.pumpHourEnd.value, + sensor_b: this.sensorBInstalled.checked, + max_consecutive_pump_count: this.maxConsecutivePumpCount.valueAsNumber, + moisture_sensor_min_frequency: this.moistureSensorMinFrequency.valueAsNumber || undefined, + moisture_sensor_max_frequency: this.moistureSensorMaxFrequency.valueAsNumber || undefined, + }; + return rv; + } setMoistureA(a: number) { this.moistureA.innerText = String(a); diff --git a/rust/src_webpack/src/timeview.html b/rust/src_webpack/src/timeview.html index 01656ed..9e0ed68 100644 --- a/rust/src_webpack/src/timeview.html +++ b/rust/src_webpack/src/timeview.html @@ -18,4 +18,11 @@
Local time
+
+ Timezone: + +
+ diff --git a/rust/src_webpack/src/timeview.ts b/rust/src_webpack/src/timeview.ts index 473f26c..b68d2ea 100644 --- a/rust/src_webpack/src/timeview.ts +++ b/rust/src_webpack/src/timeview.ts @@ -8,9 +8,14 @@ export class TimeView { auto_refresh: HTMLInputElement; controller: Controller; timer: NodeJS.Timeout | undefined; + timezoneSelect: HTMLSelectElement; constructor(controller:Controller) { (document.getElementById("timeview") as HTMLElement).innerHTML = require("./timeview.html") + this.timezoneSelect = document.getElementById('timezone_select') as HTMLSelectElement; + this.timezoneSelect.onchange = function(){ + controller.configChanged() + } this.auto_refresh = document.getElementById("timeview_auto_refresh") as HTMLInputElement; this.esp_time = document.getElementById("timeview_esp_time") as HTMLDivElement; @@ -44,4 +49,26 @@ export class TimeView { } } - } \ No newline at end of file + + timezones(timezones: string[]) { + timezones.forEach(tz => { + const option = document.createElement('option'); + option.value = tz; + option.textContent = tz; + this.timezoneSelect.appendChild(option); + }); + + } + + getTimeZone() { + return this.timezoneSelect.value; + } + + setTimeZone(timezone: string | undefined) { + if (timezone != undefined) { + this.timezoneSelect.value = timezone; + } else { + this.timezoneSelect.value = "UTC"; + } + } +} \ No newline at end of file