Compare commits

...

6 Commits

23 changed files with 539 additions and 400 deletions

View File

@ -21,6 +21,6 @@ build-std = ["std", "panic_abort"]
MCU="esp32c6"
# Note: this variable is not used by the pio builder (`cargo build --features pio`)
ESP_IDF_VERSION = "v5.2.1"
CHRONO_TZ_TIMEZONE_FILTER="UTC|Europe/Berlin"
CHRONO_TZ_TIMEZONE_FILTER = "UTC|America/New_York|America/Chicago|America/Los_Angeles|Europe/London|Europe/Berlin|Europe/Paris|Asia/Tokyo|Asia/Shanghai|Asia/Kolkata|Australia/Sydney|America/Sao_Paulo|Africa/Johannesburg|Asia/Dubai|Pacific/Auckland"
CARGO_WORKSPACE_DIR = { value = "", relative = true }
RUST_BACKTRACE = "full"

7
rust/.idea/dictionaries/project.xml generated Normal file
View File

@ -0,0 +1,7 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>sntp</w>
</words>
</dictionary>
</component>

1
rust/.idea/vcs.xml generated
View File

@ -2,5 +2,6 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
<mapping directory="$PROJECT_DIR$/../website/themes/blowfish" vcs="Git" />
</component>
</project>

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;
use chrono_tz::Tz::UTC;
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));
@ -67,7 +65,7 @@ struct LightState {
active: bool,
/// led should not be on at this time of day
out_of_work_hour: bool,
/// battery is low so do not use led
/// the battery is low so do not use led
battery_low: bool,
/// the sun is up
is_day: bool,
@ -81,8 +79,8 @@ enum SensorError {
OpenCircuit { hz: f32, min: f32 },
}
fn safe_main() -> anyhow::Result<()> {
// It is necessary to call this function once. Otherwise some patches to the runtime
fn safe_main() -> Result<()> {
// It is necessary to call this function once. Otherwise, some patches to the runtime
// implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71
esp_idf_svc::sys::link_patches();
@ -102,7 +100,7 @@ fn safe_main() -> anyhow::Result<()> {
let version = get_version();
println!(
"Version useing git has {} build on {}",
"Version using git has {} build on {}",
version.git_hash, version.build_time
);
@ -131,16 +129,16 @@ fn safe_main() -> anyhow::Result<()> {
&format!("unknown {ota_state}")
}
};
log(log::LogMessage::PartitionState, 0, 0, "", ota_state_string);
log(LogMessage::PartitionState, 0, 0, "", ota_state_string);
let mut board: std::sync::MutexGuard<'_, PlantCtrlBoard<'_>> = BOARD_ACCESS.lock().unwrap();
board.general_fault(false);
log(log::LogMessage::MountingFilesystem, 0, 0, "", "");
log(LogMessage::MountingFilesystem, 0, 0, "", "");
board.mount_file_system()?;
let free_space = board.file_system_size()?;
log(
log::LogMessage::FilesystemMount,
LogMessage::FilesystemMount,
free_space.free_size as u32,
free_space.total_size as u32,
&free_space.used_size.to_string(),
@ -159,17 +157,17 @@ fn safe_main() -> anyhow::Result<()> {
})
.unwrap();
//check if we know the time current > 2020 (plausibility check, this code is newer than 2020)
//check if we know the time current > 2020 (plausibility checks, this code is newer than 2020)
if cur.year() < 2020 {
to_config = true;
log(log::LogMessage::YearInplausibleForceConfig, 0, 0, "", "");
log(LogMessage::YearInplausibleForceConfig, 0, 0, "", "");
}
println!("cur is {}", cur);
board.update_charge_indicator();
if board.get_restart_to_conf() {
log(log::LogMessage::ConfigModeSoftwareOverride, 0, 0, "", "");
log(LogMessage::ConfigModeSoftwareOverride, 0, 0, "", "");
for _i in 0..2 {
board.general_fault(true);
Delay::new_default().delay_ms(100);
@ -179,9 +177,9 @@ fn safe_main() -> anyhow::Result<()> {
to_config = true;
board.general_fault(true);
board.set_restart_to_conf(false);
} else if board.is_mode_override() {
} else if board.mode_override_pressed() {
board.general_fault(true);
log(log::LogMessage::ConfigModeButtonOverride, 0, 0, "", "");
log(LogMessage::ConfigModeButtonOverride, 0, 0, "", "");
for _i in 0..5 {
board.general_fault(true);
Delay::new_default().delay_ms(100);
@ -189,7 +187,7 @@ fn safe_main() -> anyhow::Result<()> {
Delay::new_default().delay_ms(100);
}
if board.is_mode_override() {
if board.mode_override_pressed() {
board.general_fault(true);
to_config = true;
} else {
@ -197,27 +195,24 @@ fn safe_main() -> anyhow::Result<()> {
}
}
let config: PlantControllerConfig;
match board.get_config() {
Ok(valid) => {
config = valid;
}
let config: PlantControllerConfig = match board.get_config() {
Ok(valid) => valid,
Err(err) => {
log(
log::LogMessage::ConfigModeMissingConfig,
LogMessage::ConfigModeMissingConfig,
0,
0,
"",
&err.to_string(),
);
//config upload will trigger reboot!
let _ = board.wifi_ap(Option::None);
let _ = board.wifi_ap(None);
drop(board);
let reboot_now = Arc::new(AtomicBool::new(false));
let _webserver = httpd(reboot_now.clone());
wait_infinity(WaitType::MissingConfig, reboot_now.clone());
}
}
};
let mut wifi = false;
let mut mqtt = false;
@ -273,18 +268,23 @@ fn safe_main() -> anyhow::Result<()> {
Ok(_) => {
println!("Started ap, continuing")
}
Err(err) => println!(
"Could not start config override ap mode due to {}",
err.to_string()
),
Err(err) => println!("Could not start config override ap mode due to {}", err),
}
}
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
);
@ -314,7 +314,7 @@ fn safe_main() -> anyhow::Result<()> {
}
log(
log::LogMessage::StartupInfo,
LogMessage::StartupInfo,
wifi as u32,
sntp as u32,
&mqtt.to_string(),
@ -322,7 +322,7 @@ fn safe_main() -> anyhow::Result<()> {
);
if to_config {
//check if client or ap mode and init wifi
//check if client or ap mode and init Wi-Fi
println!("executing config mode override");
//config upload will trigger reboot!
drop(board);
@ -330,7 +330,7 @@ fn safe_main() -> anyhow::Result<()> {
let _webserver = httpd(reboot_now.clone());
wait_infinity(WaitType::ConfigButton, reboot_now.clone());
} else {
log(log::LogMessage::NormalRun, 0, 0, "", "");
log(LogMessage::NormalRun, 0, 0, "", "");
}
let dry_run = false;
@ -355,13 +355,9 @@ fn safe_main() -> anyhow::Result<()> {
&format!("{}", value),
"",
),
TankError::BoardError(err) => log(
LogMessage::TankSensorBoardError,
0,
0,
"",
&format!("{}", &err.to_string()),
),
TankError::BoardError(err) => {
log(LogMessage::TankSensorBoardError, 0, 0, "", &err.to_string())
}
}
// disabled cannot trigger this because of wrapping if is_enabled
board.general_fault(true);
@ -425,15 +421,15 @@ fn safe_main() -> anyhow::Result<()> {
let pump_required = plantstate
.iter()
.zip(&config.plants)
.any(|(it, conf)| it.needs_to_be_watered(&conf, &timezone_time))
.any(|(it, conf)| it.needs_to_be_watered(conf, &timezone_time))
&& !water_frozen;
if pump_required {
log(log::LogMessage::EnableMain, dry_run as u32, 0, "", "");
log(LogMessage::EnableMain, dry_run as u32, 0, "", "");
if !dry_run {
board.any_pump(true)?; // what does this do? Does it need to be reset?
}
for (plant_id, (state, plant_config)) in plantstate.iter().zip(&config.plants).enumerate() {
if state.needs_to_be_watered(&plant_config, &timezone_time) {
if state.needs_to_be_watered(plant_config, &timezone_time) {
let pump_count = board.consecutive_pump_count(plant_id) + 1;
board.store_consecutive_pump_count(plant_id, pump_count);
//TODO(judge) where to put this?
@ -449,7 +445,7 @@ fn safe_main() -> anyhow::Result<()> {
// board.fault(plant, true);
//}
log(
log::LogMessage::PumpPlant,
LogMessage::PumpPlant,
(plant_id + 1) as u32,
plant_config.pump_time_s as u32,
&dry_run.to_string(),
@ -463,7 +459,7 @@ fn safe_main() -> anyhow::Result<()> {
Delay::new_default().delay_ms(1000 * plant_config.pump_time_s as u32);
board.pump(plant_id, false)?;
}
} else if !state.pump_in_timeout(&plant_config, &timezone_time) {
} else if !state.pump_in_timeout(plant_config, &timezone_time) {
// plant does not need to be watered and is not in timeout
// -> reset consecutive pump count
board.store_consecutive_pump_count(plant_id, 0);
@ -497,23 +493,21 @@ fn safe_main() -> anyhow::Result<()> {
if config.night_lamp.night_lamp_only_when_dark {
if !light_state.is_day {
if light_state.battery_low {
board.light(false).unwrap();
board.light(false)?;
} else {
light_state.active = true;
board.light(true).unwrap();
board.light(true)?;
}
}
} else {
if light_state.battery_low {
board.light(false).unwrap();
} else if light_state.battery_low {
board.light(false)?;
} else {
light_state.active = true;
board.light(true).unwrap();
}
board.light(true)?;
}
} else {
light_state.active = false;
board.light(false).unwrap();
board.light(false)?;
}
println!("Lightstate is {:?}", light_state);
@ -540,10 +534,6 @@ fn safe_main() -> anyhow::Result<()> {
};
let _ = board.mqtt_publish(&config, "/state", "sleep".as_bytes());
//determine next event
//is light out of work trigger soon?
//is battery low ??
//is deep sleep
mark_app_valid();
let stay_alive_mqtt = STAY_ALIVE.load(std::sync::atomic::Ordering::Relaxed);
@ -569,7 +559,7 @@ fn publish_battery_state(
let bat = board.get_battery_state();
match serde_json::to_string(&bat) {
Ok(state) => {
let _ = board.mqtt_publish(&config, "/battery", state.as_bytes());
let _ = board.mqtt_publish(config, "/battery", state.as_bytes());
}
Err(err) => {
println!("Error publishing battery_state {}", err);
@ -579,33 +569,56 @@ 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 {
unsafe {
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;
};
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);
lock.fault(i, i < led_count);
}
BOARD_ACCESS.lock().unwrap().general_fault(true);
vTaskDelay(delay);
BOARD_ACCESS.lock().unwrap().general_fault(false);
//TODO move locking outside of loop and drop afterwards
}
WaitType::ConfigButton => {
// Alternating pattern: 1010 1010 -> 0101 0101
pattern_step = (pattern_step + 1) % 2;
for i in 0..8 {
BOARD_ACCESS.lock().unwrap().fault(i, false);
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);
}
}
}
lock.general_fault(true);
drop(lock);
vTaskDelay(delay);
if wait_type == WaitType::MqttConfig {
if !STAY_ALIVE.load(std::sync::atomic::Ordering::Relaxed) {
let mut lock = BOARD_ACCESS.lock().unwrap();
lock.general_fault(false);
// Clear all LEDs
for i in 0..8 {
lock.fault(i, false);
}
drop(lock);
vTaskDelay(delay);
if wait_type == WaitType::MqttConfig
&& !STAY_ALIVE.load(std::sync::atomic::Ordering::Relaxed)
{
reboot_now.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
if reboot_now.load(std::sync::atomic::Ordering::Relaxed) {
//ensure clean http answer
Delay::new_default().delay_ms(500);
@ -625,7 +638,7 @@ fn main() {
BOARD_ACCESS.lock().unwrap().set_restart_to_conf(false);
BOARD_ACCESS.lock().unwrap().deep_sleep(1);
}
// if safe_main exists with error, rollback to known good ota version
// if safe_main exists with an error, rollback to a known good ota version
Err(err) => {
println!("Failed main {}", err);
let _rollback_successful = rollback_and_reboot();
@ -635,22 +648,22 @@ fn main() {
}
fn to_string<T: Display>(value: Result<T>) -> String {
return match value {
match value {
Ok(v) => v.to_string(),
Err(err) => {
format!("{:?}", err)
}
};
}
}
pub fn in_time_range(cur: &DateTime<Tz>, start: u8, end: u8) -> bool {
let curhour = cur.hour() as u8;
//eg 10-14
if start < end {
return curhour > start && curhour < end;
curhour > start && curhour < end
} else {
//eg 20-05
return curhour > start || curhour < end;
curhour > start || curhour < end
}
}
@ -665,11 +678,11 @@ fn get_version() -> VersionInfo {
} else {
"ota_0 @ "
};
return VersionInfo {
git_hash: (branch + "@" + hash),
VersionInfo {
git_hash: branch + "@" + hash,
build_time: env!("VERGEN_BUILD_TIMESTAMP").to_owned(),
partition: partition.to_owned() + &address.to_string(),
};
}
}
#[derive(Serialize, Debug)]

View File

@ -60,9 +60,9 @@ use esp_idf_svc::systime::EspSystemTime;
use esp_idf_sys::{gpio_hold_dis, gpio_hold_en, vTaskDelay, EspError};
use one_wire_bus::OneWire;
use crate::config::{self, PlantControllerConfig};
use crate::config::PlantControllerConfig;
use crate::log::log;
use crate::{plant_hal, to_string, STAY_ALIVE};
use crate::{to_string, STAY_ALIVE};
//Only support for 8 right now!
pub const PLANT_COUNT: usize = 8;
@ -168,7 +168,7 @@ pub struct PlantCtrlBoard<'a> {
tank_power: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>,
general_fault: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>,
wifi_driver: EspWifi<'a>,
one_wire_bus: OneWire<PinDriver<'a, Gpio18, esp_idf_hal::gpio::InputOutput>>,
one_wire_bus: OneWire<PinDriver<'a, Gpio18, InputOutput>>,
mqtt_client: Option<EspMqttClient<'a>>,
battery_driver: Bq34z100g1Driver<MutexDevice<'a, I2cDriver<'a>>, Delay>,
rtc:
@ -229,9 +229,9 @@ impl PlantCtrlBoard<'_> {
pub fn deep_sleep(&mut self, duration_in_ms: u64) -> ! {
self.shift_register.decompose()[AWAKE].set_low().unwrap();
unsafe {
//if we dont do this here, we might just revert a newly flashed firmeware
//if we don't do this here, we might just revert newly flashed firmware
mark_app_valid();
//allow early wakup by pressing the boot button
//allow early wakeup by pressing the boot button
if duration_in_ms == 0 {
esp_restart();
} else {
@ -278,7 +278,8 @@ impl PlantCtrlBoard<'_> {
};
let header: BackupHeader = bincode::deserialize(&header_page_buffer)?;
let data_start_address = 1 * self.eeprom.page_size() as u32;
//skip page 0, used by the header
let data_start_address = self.eeprom.page_size() as u32;
let mut data_buffer = vec![0_u8; header.size];
match self.eeprom.read_data(data_start_address, &mut data_buffer) {
OkStd(_) => {}
@ -337,9 +338,9 @@ impl PlantCtrlBoard<'_> {
OkStd(_) => {}
Err(err) => bail!("Error writing eeprom {:?}", err),
};
current_page = current_page + 1;
current_page += 1;
let iter = ((current_page / 1) % 8) as usize;
let iter = (current_page % 8) as usize;
if iter != lastiter {
for i in 0..PLANT_COUNT {
self.fault(i, iter == i);
@ -350,11 +351,11 @@ impl PlantCtrlBoard<'_> {
//update led here?
delay.delay_ms(5);
}
return Ok(());
Ok(())
}
pub fn get_battery_state(&mut self) -> BatteryState {
let bat = BatteryState {
BatteryState {
voltage_milli_volt: to_string(self.voltage_milli_volt()),
current_milli_ampere: to_string(self.average_current_milli_ampere()),
cycle_count: to_string(self.cycle_count()),
@ -363,14 +364,13 @@ impl PlantCtrlBoard<'_> {
state_of_charge: to_string(self.state_charge_percent()),
state_of_health: to_string(self.state_health_percent()),
temperature: to_string(self.bat_temperature()),
};
return bat;
}
}
pub fn list_files(&self) -> FileList {
let storage = CString::new(SPIFFS_PARTITION_NAME).unwrap();
let mut file_system_corrupt = Option::None;
let mut file_system_corrupt = None;
let mut iter_error = None;
let mut result = Vec::new();
@ -386,7 +386,7 @@ impl PlantCtrlBoard<'_> {
filename: file.file_name().into_string().unwrap(),
size: file
.metadata()
.and_then(|it| core::result::Result::Ok(it.len()))
.and_then(|it| Result::Ok(it.len()))
.unwrap_or_default()
as usize,
};
@ -409,13 +409,13 @@ impl PlantCtrlBoard<'_> {
esp_spiffs_info(storage.as_ptr(), &mut total, &mut used);
}
return FileList {
FileList {
total,
used,
file_system_corrupt,
files: result,
iter_error,
};
}
}
pub fn delete_file(&self, filename: &str) -> Result<()> {
@ -430,11 +430,11 @@ impl PlantCtrlBoard<'_> {
pub fn get_file_handle(&self, filename: &str, write: bool) -> Result<File> {
let filepath = Path::new(BASE_PATH).join(Path::new(filename));
return Ok(if write {
Ok(if write {
File::create(filepath)?
} else {
File::open(filepath)?
});
})
}
pub fn is_day(&self) -> bool {
@ -522,17 +522,17 @@ impl PlantCtrlBoard<'_> {
7 => PUMP8_BIT,
_ => bail!("Invalid pump {plant}",),
};
//currently infailable error, keep for future as result anyway
//currently infallible error, keep for future as result anyway
self.shift_register.decompose()[index].set_state(enable.into())?;
Ok(())
}
pub fn last_pump_time(&self, plant: usize) -> Option<chrono::DateTime<Utc>> {
pub fn last_pump_time(&self, plant: usize) -> Option<DateTime<Utc>> {
let ts = unsafe { LAST_WATERING_TIMESTAMP }[plant];
return Some(DateTime::from_timestamp_millis(ts)?);
DateTime::from_timestamp_millis(ts)
}
pub fn store_last_pump_time(&mut self, plant: usize, time: chrono::DateTime<Utc>) {
pub fn store_last_pump_time(&mut self, plant: usize, time: DateTime<Utc>) {
unsafe {
LAST_WATERING_TIMESTAMP[plant] = time.timestamp_millis();
}
@ -545,9 +545,7 @@ impl PlantCtrlBoard<'_> {
}
pub fn consecutive_pump_count(&mut self, plant: usize) -> u32 {
unsafe {
return CONSECUTIVE_WATERING_PLANT[plant];
}
unsafe { CONSECUTIVE_WATERING_PLANT[plant] }
}
pub fn fault(&self, plant: usize, enable: bool) {
@ -568,19 +566,17 @@ impl PlantCtrlBoard<'_> {
}
pub fn low_voltage_in_cycle(&mut self) -> bool {
unsafe {
return LOW_VOLTAGE_DETECTED;
}
unsafe { LOW_VOLTAGE_DETECTED }
}
pub fn any_pump(&mut self, enable: bool) -> Result<()> {
{
self.main_pump.set_state(enable.into()).unwrap();
self.main_pump.set_state(enable.into())?;
Ok(())
}
}
pub fn time(&mut self) -> Result<chrono::DateTime<Utc>> {
pub fn time(&mut self) -> Result<DateTime<Utc>> {
let time = EspSystemTime {}.now().as_millis();
let smaller_time = time as i64;
let local_time = DateTime::from_timestamp_millis(smaller_time)
@ -588,7 +584,7 @@ impl PlantCtrlBoard<'_> {
Ok(local_time)
}
pub fn sntp(&mut self, max_wait_ms: u32) -> Result<chrono::DateTime<Utc>> {
pub fn sntp(&mut self, max_wait_ms: u32) -> Result<DateTime<Utc>> {
let sntp = sntp::EspSntp::new_default()?;
let mut counter = 0;
while sntp.get_sync_status() != SyncStatus::Completed {
@ -634,28 +630,24 @@ impl PlantCtrlBoard<'_> {
self.signal_counter.counter_pause()?;
self.signal_counter.counter_clear()?;
//Disable all
self.shift_register.decompose()[MS_4].set_high().unwrap();
self.shift_register.decompose()[MS_4].set_high()?;
self.sensor_multiplexer(sensor_channel)?;
self.shift_register.decompose()[MS_4].set_low().unwrap();
self.shift_register.decompose()[SENSOR_ON]
.set_high()
.unwrap();
self.shift_register.decompose()[MS_4].set_low()?;
self.shift_register.decompose()[SENSOR_ON].set_high()?;
let delay = Delay::new_default();
let measurement = 100; // TODO what is this scaling factor? what is its purpose?
let factor = 1000 as f32 / measurement as f32;
let factor = 1000f32 / measurement as f32;
//give some time to stabilize
delay.delay_ms(10);
self.signal_counter.counter_resume()?;
delay.delay_ms(measurement);
self.signal_counter.counter_pause()?;
self.shift_register.decompose()[MS_4].set_high().unwrap();
self.shift_register.decompose()[SENSOR_ON]
.set_low()
.unwrap();
self.shift_register.decompose()[MS_4].set_high()?;
self.shift_register.decompose()[SENSOR_ON].set_low()?;
delay.delay_ms(10);
let unscaled = self.signal_counter.get_counter_value()? as i32;
let hz = unscaled as f32 * factor;
@ -707,7 +699,7 @@ impl PlantCtrlBoard<'_> {
//TODO expect error due to invalid pw or similar! //call this during configuration and check if works, revert to config mode if not
self.wifi_driver.set_configuration(&Configuration::Client(
ClientConfiguration {
ssid: ssid,
ssid,
password: pw,
..Default::default()
},
@ -716,7 +708,7 @@ impl PlantCtrlBoard<'_> {
None => {
self.wifi_driver.set_configuration(&Configuration::Client(
ClientConfiguration {
ssid: ssid,
ssid,
auth_method: AuthMethod::None,
..Default::default()
},
@ -733,7 +725,7 @@ impl PlantCtrlBoard<'_> {
delay.delay_ms(250);
counter += 250;
if counter > max_wait {
//ignore these errors, wifi will not be used this
//ignore these errors, Wi-Fi will not be used this
self.wifi_driver.disconnect().unwrap_or(());
self.wifi_driver.stop().unwrap_or(());
bail!("Did not manage wifi connection within timeout");
@ -745,7 +737,7 @@ impl PlantCtrlBoard<'_> {
delay.delay_ms(250);
counter += 250;
if counter > max_wait {
//ignore these errors, wifi will not be used this
//ignore these errors, Wi-Fi will not be used this
self.wifi_driver.disconnect().unwrap_or(());
self.wifi_driver.stop().unwrap_or(());
bail!("Did not manage wifi connection within timeout");
@ -780,7 +772,7 @@ impl PlantCtrlBoard<'_> {
let mut total_size = 0;
let mut used_size = 0;
unsafe {
esp_idf_sys::esp!(esp_idf_sys::esp_spiffs_info(
esp_idf_sys::esp!(esp_spiffs_info(
storage.as_ptr(),
&mut total_size,
&mut used_size
@ -793,7 +785,7 @@ impl PlantCtrlBoard<'_> {
})
}
pub fn is_mode_override(&mut self) -> bool {
pub fn mode_override_pressed(&mut self) -> bool {
self.boot_button.get_level() == Level::Low
}
@ -802,17 +794,19 @@ impl PlantCtrlBoard<'_> {
let config = Path::new(CONFIG_FILE);
if config.exists() {
println!("Removing config");
std::fs::remove_file(config)?;
fs::remove_file(config)?;
}
//TODO clear eeprom
//destroy backup header
let dummy: [u8; 0] = [];
self.backup_config(&dummy)?;
Ok(())
}
pub fn get_rtc_time(&mut self) -> Result<DateTime<Utc>> {
match self.rtc.datetime() {
OkStd(rtc_time) => {
return Ok(rtc_time.and_utc());
}
OkStd(rtc_time) => Ok(rtc_time.and_utc()),
Err(err) => {
bail!("Error getting rtc time {:?}", err)
}
@ -829,7 +823,7 @@ impl PlantCtrlBoard<'_> {
}
}
pub fn get_config(&mut self) -> Result<config::PlantControllerConfig> {
pub fn get_config(&mut self) -> Result<PlantControllerConfig> {
let cfg = File::open(CONFIG_FILE)?;
let config: PlantControllerConfig = serde_json::from_reader(cfg)?;
Ok(config)
@ -889,8 +883,8 @@ impl PlantCtrlBoard<'_> {
unsafe { vTaskDelay(100) };
}
for plant in 0..PLANT_COUNT {
let a = self.measure_moisture_hz(plant, plant_hal::Sensor::A);
let b = self.measure_moisture_hz(plant, plant_hal::Sensor::B);
let a = self.measure_moisture_hz(plant, Sensor::A);
let b = self.measure_moisture_hz(plant, Sensor::B);
let aa = match a {
OkStd(a) => a as u32,
Err(_) => u32::MAX,
@ -905,11 +899,6 @@ impl PlantCtrlBoard<'_> {
Ok(())
}
pub fn is_wifi_config_file_existant(&mut self) -> bool {
let config = Path::new(CONFIG_FILE);
config.exists()
}
pub fn mqtt(&mut self, config: &PlantControllerConfig) -> Result<()> {
let base_topic = config
.network
@ -956,8 +945,8 @@ impl PlantCtrlBoard<'_> {
let round_trip_topic_copy = round_trip_topic.clone();
let round_trip_ok_copy = round_trip_ok.clone();
let client_id = mqtt_client_config.client_id.unwrap_or("not set");
log(LogMessage::MqttInfo, 0, 0, client_id, &mqtt_url);
let mut client = EspMqttClient::new_cb(&mqtt_url, &mqtt_client_config, move |event| {
log(LogMessage::MqttInfo, 0, 0, client_id, mqtt_url);
let mut client = EspMqttClient::new_cb(mqtt_url, &mqtt_client_config, move |event| {
let payload = event.payload();
match payload {
embedded_svc::mqtt::client::EventPayload::Received {
@ -977,7 +966,7 @@ impl PlantCtrlBoard<'_> {
log(LogMessage::MqttStayAliveRec, 0, 0, &data, "");
STAY_ALIVE.store(value, std::sync::atomic::Ordering::Relaxed);
} else {
log(LogMessage::UnknownTopic, 0, 0, "", &topic);
log(LogMessage::UnknownTopic, 0, 0, "", topic);
}
}
}
@ -1018,8 +1007,9 @@ impl PlantCtrlBoard<'_> {
}
})?;
let wait_for_connections_event = 0;
let mut wait_for_connections_event = 0;
while wait_for_connections_event < 100 {
wait_for_connections_event += 1;
match mqtt_connected_event_received.load(std::sync::atomic::Ordering::Relaxed) {
true => {
println!("Mqtt connection callback received, progressing");
@ -1037,8 +1027,9 @@ impl PlantCtrlBoard<'_> {
"online_test".as_bytes(),
)?;
let wait_for_roundtrip = 0;
let mut wait_for_roundtrip = 0;
while wait_for_roundtrip < 100 {
wait_for_roundtrip += 1;
match round_trip_ok.load(std::sync::atomic::Ordering::Relaxed) {
true => {
println!("Round trip registered, proceeding");
@ -1085,7 +1076,7 @@ impl PlantCtrlBoard<'_> {
let client = self.mqtt_client.as_mut().unwrap();
let mut full_topic: heapless::String<256> = heapless::String::new();
if full_topic
.push_str(&config.network.base_topic.as_ref().unwrap())
.push_str(config.network.base_topic.as_ref().unwrap())
.is_err()
{
println!("Some error assembling full_topic 1");
@ -1095,12 +1086,7 @@ impl PlantCtrlBoard<'_> {
println!("Some error assembling full_topic 2");
bail!("Some error assembling full_topic 2")
};
let publish = client.publish(
&full_topic,
embedded_svc::mqtt::client::QoS::ExactlyOnce,
true,
message,
);
let publish = client.publish(&full_topic, ExactlyOnce, true, message);
Delay::new(10).delay_ms(50);
match publish {
OkStd(message_id) => {
@ -1110,7 +1096,7 @@ impl PlantCtrlBoard<'_> {
String::from_utf8_lossy(message),
message_id
);
return Ok(());
Ok(())
}
Err(err) => {
println!(
@ -1119,13 +1105,13 @@ impl PlantCtrlBoard<'_> {
String::from_utf8_lossy(message),
err
);
return Err(err)?;
Err(err)?
}
}
};
}
pub fn get_restart_to_conf(&mut self) -> bool {
return unsafe { RESTART_TO_CONF };
unsafe { RESTART_TO_CONF }
}
pub fn set_restart_to_conf(&mut self, to_conf: bool) {
@ -1192,7 +1178,7 @@ impl PlantCtrlBoard<'_> {
pub fn bat_temperature(&mut self) -> Result<u16> {
match self.battery_driver.temperature() {
OkStd(r) => Ok(r as u16),
OkStd(r) => Ok(r),
Err(err) => bail!("Error reading Temperature {:?}", err),
}
}
@ -1241,7 +1227,7 @@ fn print_battery(
) -> Result<(), Bq34Z100Error<I2cError>> {
println!("Try communicating with battery");
let fwversion = battery_driver.fw_version().unwrap_or_else(|e| {
println!("Firmeware {:?}", e);
println!("Firmware {:?}", e);
0
});
println!("fw version is {}", fwversion);
@ -1291,10 +1277,10 @@ fn print_battery(
println!("ChemId: {} Current voltage {} and current {} with charge {}% and temp {} CVolt: {} CCur {}", chem_id, voltage, current, state, temp_c, charge_voltage, charge_current);
let _ = battery_driver.unsealed();
let _ = battery_driver.it_enable();
return Result::Ok(());
Result::Ok(())
}
pub static I2C_DRIVER: Lazy<Mutex<I2cDriver<'static>>> = Lazy::new(|| PlantHal::create_i2c());
pub static I2C_DRIVER: Lazy<Mutex<I2cDriver<'static>>> = Lazy::new(PlantHal::create_i2c);
impl PlantHal {
fn create_i2c() -> Mutex<I2cDriver<'static>> {
let peripherals = unsafe { Peripherals::new() };
@ -1315,15 +1301,15 @@ impl PlantHal {
let peripherals = Peripherals::take()?;
let mut clock = PinDriver::input_output(peripherals.pins.gpio15.downgrade())?;
clock.set_pull(Pull::Floating).unwrap();
clock.set_pull(Pull::Floating)?;
let mut latch = PinDriver::input_output(peripherals.pins.gpio3.downgrade())?;
latch.set_pull(Pull::Floating).unwrap();
latch.set_pull(Pull::Floating)?;
let mut data = PinDriver::input_output(peripherals.pins.gpio23.downgrade())?;
data.set_pull(Pull::Floating).unwrap();
let shift_register = ShiftRegister40::new(clock.into(), latch.into(), data.into());
data.set_pull(Pull::Floating)?;
let shift_register = ShiftRegister40::new(clock, latch, data);
//disable all
for mut pin in shift_register.decompose() {
pin.set_low().unwrap();
pin.set_low()?;
}
let awake = &mut shift_register.decompose()[AWAKE];
@ -1363,7 +1349,7 @@ impl PlantHal {
};
let mut one_wire_pin = PinDriver::input_output_od(peripherals.pins.gpio18)?;
one_wire_pin.set_pull(Pull::Floating).unwrap();
one_wire_pin.set_pull(Pull::Floating)?;
let rtc_time = rtc.datetime();
match rtc_time {
@ -1498,7 +1484,7 @@ impl PlantHal {
boot_button.set_pull(Pull::Floating)?;
let mut light = PinDriver::input_output(peripherals.pins.gpio10.downgrade())?;
light.set_pull(Pull::Floating).unwrap();
light.set_pull(Pull::Floating)?;
let mut main_pump = PinDriver::input_output(peripherals.pins.gpio2.downgrade())?;
main_pump.set_pull(Pull::Floating)?;
@ -1518,7 +1504,7 @@ impl PlantHal {
Err(err) => {
log(
LogMessage::BatteryCommunicationError,
0 as u32,
0u32,
0,
"",
&format!("{err:?})"),
@ -1541,16 +1527,16 @@ impl PlantHal {
signal_counter: counter_unit1,
wifi_driver,
mqtt_client: None,
battery_driver: battery_driver,
rtc: rtc,
eeprom: eeprom,
battery_driver,
rtc,
eeprom,
});
let _ = rv.lock().is_ok_and(|mut board| {
unsafe { gpio_hold_dis(board.shift_register_enable_invert.pin()) };
board.shift_register_enable_invert.set_low().unwrap();
unsafe { gpio_hold_en(board.shift_register_enable_invert.pin()) };
return true;
true
});
Ok(rv)

View File

@ -2,13 +2,10 @@ use chrono::{DateTime, TimeDelta, Utc};
use chrono_tz::Tz;
use serde::{Deserialize, Serialize};
use crate::{
config::{self, PlantConfig},
in_time_range, plant_hal,
};
use crate::{config::PlantConfig, in_time_range, plant_hal};
const MOIST_SENSOR_MAX_FREQUENCY: f32 = 5500.; // 60kHz (500Hz margin)
const MOIST_SENSOR_MIN_FREQUENCY: f32 = 150.; // this is really really dry, think like cactus levels
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)]
pub enum MoistureSensorError {
@ -87,34 +84,45 @@ 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 {
pub fn read_hardware_state(
plant_id: usize,
board: &mut plant_hal::PlantCtrlBoard,
config: &config::PlantConfig,
config: &PlantConfig,
) -> 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 +136,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 +157,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 {
@ -208,25 +222,21 @@ impl PlantState {
if let Some(moisture_percent) = moisture_percent {
if self.pump_in_timeout(plant_conf, current_time) {
false
} else {
if moisture_percent < plant_conf.target_moisture {
true
} else if moisture_percent < plant_conf.target_moisture {
in_time_range(
current_time,
plant_conf.pump_hour_start,
plant_conf.pump_hour_end,
)
} else {
false
}
}
} else {
// in case no moisture can be determined do not water plant
return false;
}
}
PlantWateringMode::TimerOnly => {
if self.pump_in_timeout(plant_conf, current_time) {
// in case no moisture can be determined, do not water the plant
false
} else {
true
}
}
PlantWateringMode::TimerOnly => !self.pump_in_timeout(plant_conf, current_time),
}
}
@ -278,9 +288,9 @@ pub struct PlantInfo<'a> {
sensor_b: &'a MoistureSensorState,
/// configured plant watering mode
mode: PlantWateringMode,
/// plant needs to be watered
/// the plant needs to be watered
do_water: bool,
/// is plant considerd to be dry according to settings
/// plant is considered to be dry according to settings
dry: bool,
/// plant irrigation cooldown is active
cooldown: bool,
@ -289,7 +299,7 @@ pub struct PlantInfo<'a> {
/// how often has the pump been watered without reaching target moisture
consecutive_pump_count: u32,
pump_error: Option<PumpError>,
/// last time when pump was active
/// last time when the pump was active
last_pump: Option<DateTime<Tz>>,
/// next time when pump should activate
next_pump: Option<DateTime<Tz>>,

View File

@ -17,12 +17,12 @@ pub enum TankError {
}
pub enum TankState {
TankSensorPresent(f32),
TankSensorError(TankError),
TankSensorDisabled,
Present(f32),
Error(TankError),
Disabled,
}
fn raw_volatge_to_divider_percent(raw_value_mv: f32) -> Result<f32, TankError> {
fn raw_voltage_to_divider_percent(raw_value_mv: f32) -> Result<f32, TankError> {
if raw_value_mv > OPEN_TANK_VOLTAGE {
return Err(TankError::SensorMissing(raw_value_mv));
}
@ -37,7 +37,7 @@ fn raw_voltage_to_tank_fill_percent(
raw_value_mv: f32,
config: &TankConfig,
) -> Result<f32, TankError> {
let divider_percent = raw_volatge_to_divider_percent(raw_value_mv)?;
let divider_percent = raw_voltage_to_divider_percent(raw_value_mv)?;
if divider_percent < config.tank_empty_percent.into()
|| divider_percent > config.tank_full_percent.into()
{
@ -56,9 +56,9 @@ fn raw_voltage_to_tank_fill_percent(
impl TankState {
pub fn left_ml(&self, config: &TankConfig) -> Result<f32, TankError> {
match self {
TankState::TankSensorDisabled => Err(TankError::SensorDisabled),
TankState::TankSensorError(err) => Err(err.clone()),
TankState::TankSensorPresent(raw_value_mv) => {
TankState::Disabled => Err(TankError::SensorDisabled),
TankState::Error(err) => Err(err.clone()),
TankState::Present(raw_value_mv) => {
let tank_fill_percent = raw_voltage_to_tank_fill_percent(*raw_value_mv, config)?;
Ok(config.tank_useable_ml as f32 * tank_fill_percent / 100.)
}
@ -66,9 +66,9 @@ impl TankState {
}
pub fn enough_water(&self, config: &TankConfig) -> Result<bool, TankError> {
match self {
TankState::TankSensorDisabled => Err(TankError::SensorDisabled),
TankState::TankSensorError(err) => Err(err.clone()),
TankState::TankSensorPresent(raw_value_mv) => {
TankState::Disabled => Err(TankError::SensorDisabled),
TankState::Error(err) => Err(err.clone()),
TankState::Present(raw_value_mv) => {
let tank_fill_percent = raw_voltage_to_tank_fill_percent(*raw_value_mv, config)?;
if tank_fill_percent > config.tank_empty_percent.into() {
Ok(true)
@ -80,14 +80,14 @@ impl TankState {
}
pub fn is_enabled(&self) -> bool {
matches!(self, TankState::TankSensorDisabled)
matches!(self, TankState::Disabled)
}
pub fn warn_level(&self, config: &TankConfig) -> Result<bool, TankError> {
match self {
TankState::TankSensorDisabled => Err(TankError::SensorDisabled),
TankState::TankSensorError(err) => Err(err.clone()),
TankState::TankSensorPresent(raw_value_mv) => {
TankState::Disabled => Err(TankError::SensorDisabled),
TankState::Error(err) => Err(err.clone()),
TankState::Present(raw_value_mv) => {
let tank_fill_percent = raw_voltage_to_tank_fill_percent(*raw_value_mv, config);
match tank_fill_percent {
Ok(value) => {
@ -108,11 +108,11 @@ impl TankState {
pub fn got_error(&self, config: &TankConfig) -> Option<TankError> {
match self {
TankState::TankSensorPresent(raw_value_mv) => {
TankState::Present(raw_value_mv) => {
raw_voltage_to_tank_fill_percent(*raw_value_mv, config).err()
}
TankState::TankSensorError(err) => Some(err.clone()),
TankState::TankSensorDisabled => Some(TankError::SensorDisabled),
TankState::Error(err) => Some(err.clone()),
TankState::Disabled => Some(TankError::SensorDisabled),
}
}
@ -130,10 +130,10 @@ impl TankState {
Ok(left_ml) => Some(left_ml),
};
let enough_water = self.enough_water(config).unwrap_or(false); //NOTE: is this correct if there is an error assume not enough water?
let warn_level = self.warn_level(config).unwrap_or(false); //NOTE: should no warn level be triggered if there is an error?
let warn_level = self.warn_level(config).unwrap_or(false); //NOTE: should warn level be triggered if there is an error?
let raw = match self {
TankState::TankSensorDisabled | TankState::TankSensorError(_) => None,
TankState::TankSensorPresent(raw_value_mv) => Some(*raw_value_mv),
TankState::Disabled | TankState::Error(_) => None,
TankState::Present(raw_value_mv) => Some(*raw_value_mv),
};
let percent = match raw {
@ -163,30 +163,30 @@ pub fn determine_tank_state(
) -> TankState {
if config.tank.tank_sensor_enabled {
match board.tank_sensor_voltage() {
Ok(raw_sensor_value_mv) => TankState::TankSensorPresent(raw_sensor_value_mv),
Err(err) => TankState::TankSensorError(TankError::BoardError(err.to_string())),
Ok(raw_sensor_value_mv) => TankState::Present(raw_sensor_value_mv),
Err(err) => TankState::Error(TankError::BoardError(err.to_string())),
}
} else {
TankState::TankSensorDisabled
TankState::Disabled
}
}
#[derive(Debug, Serialize)]
/// Information structure send to mqtt for monitoring purposes
pub struct TankInfo {
/// is there enough water in the tank
/// there is enough water in the tank
enough_water: bool,
/// warning that water needs to be refilled soon
warn_level: bool,
/// estimation how many ml are still in tank
/// estimation how many ml are still in the tank
left_ml: Option<f32>,
/// if there is was an issue with the water level sensor
/// if there is an issue with the water level sensor
sensor_error: Option<TankError>,
/// raw water sensor value
raw: Option<f32>,
/// percent value
percent: Option<f32>,
/// water in tank might be frozen
/// water in the tank might be frozen
water_frozen: bool,
/// water temperature
water_temp: Option<f32>,

View File

@ -1,9 +1,10 @@
pub trait LimitPrecision {
fn to_precision(self, presision: i32) -> Self;
fn to_precision(self, precision: i32) -> Self;
}
impl LimitPrecision for f32 {
fn to_precision(self, precision: i32) -> Self {
(self * (10_f32).powi(precision)).round() / (10_f32).powi(precision)
let factor = 10_f32.powi(precision);
(self * factor).round() / factor
}
}

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()
.map(|t| t.to_rfc3339())
.unwrap_or("error".to_string());
let rtc = board
.get_rtc_time()
.map(|t| 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,28 +139,6 @@ 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>,
) -> Result<Option<std::string::String>, anyhow::Error> {
@ -153,7 +164,7 @@ fn get_backup_config(
) -> Result<Option<std::string::String>, anyhow::Error> {
let mut board = BOARD_ACCESS.lock().unwrap();
let json = match board.get_backup_config() {
Ok(config) => std::str::from_utf8(&config)?.to_owned(),
Ok(config) => from_utf8(&config)?.to_owned(),
Err(err) => {
println!("Error get backup config {:?}", err);
err.to_string()
@ -277,7 +288,7 @@ fn list_files(
let board = BOARD_ACCESS.lock().unwrap();
let result = board.list_files();
let file_list_json = serde_json::to_string(&result)?;
return anyhow::Ok(Some(file_list_json));
anyhow::Ok(Some(file_list_json))
}
fn ota(
@ -320,7 +331,7 @@ fn ota(
board.set_restart_to_conf(true);
drop(board);
finalizer.set_as_boot_partition()?;
return anyhow::Ok(None);
anyhow::Ok(None)
}
fn flash_bq(filename: &str, dryrun: bool) -> anyhow::Result<()> {
@ -359,18 +370,16 @@ fn flash_bq(filename: &str, dryrun: bool) -> anyhow::Result<()> {
}
println!("Finished flashing file {line} lines processed");
board.general_fault(false);
return anyhow::Ok(());
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();
let value = parsed.query_pairs().filter(|it| it.0 == param_name).next();
let value = parsed.query_pairs().find(|it| it.0 == param_name);
match value {
Some(found) => {
return Some(found.1.into_owned());
}
None => return None,
Some(found) => Some(found.1.into_owned()),
None => None,
}
}
@ -403,7 +412,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,6 +667,11 @@ 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(
@ -673,7 +687,7 @@ fn cors_response(
let mut response = request.into_response(status, None, &headers)?;
response.write(body.as_bytes())?;
response.flush()?;
return anyhow::Ok(());
anyhow::Ok(())
}
type AnyhowHandler =
@ -698,7 +712,7 @@ fn handle_error_to500(
cors_response(request, 500, &error_text)?;
}
}
return anyhow::Ok(());
anyhow::Ok(())
}
fn read_up_to_bytes_from_request(

View File

@ -69,6 +69,7 @@ interface PlantControllerConfig {
tank: TankConfig,
night_lamp: NightLampConfig,
plants: PlantConfig[]
timezone?: string,
}
interface PlantConfig {
@ -78,8 +79,12 @@ interface PlantConfig {
pump_cooldown_min: number,
pump_hour_start: number,
pump_hour_end: number,
sensor_a: boolean,
sensor_b: boolean,
max_consecutive_pump_count: number,
moisture_sensor_min_frequency: number | null;
moisture_sensor_max_frequency: number | null;
}

View File

@ -24,7 +24,7 @@ export class FileView {
let fileuploadname = document.getElementById("fileuploadname") as HTMLInputElement
let fileuploadbtn = document.getElementById("fileuploadbtn") as HTMLInputElement
fileuploadfile.onchange = () => {
var selectedFile = fileuploadfile.files?.[0];
const selectedFile = fileuploadfile.files?.[0];
if (selectedFile == null) {
//TODO error dialog here
return
@ -42,7 +42,7 @@ export class FileView {
}
fileuploadbtn.onclick = () => {
var selectedFile = fileuploadfile.files?.[0];
const selectedFile = fileuploadfile.files?.[0];
if (selectedFile == null) {
//TODO error dialog here
return
@ -77,8 +77,7 @@ class FileEntry {
this.view.classList.add("fileentryouter")
const template = require('./fileviewentry.html') as string;
const fileRaw = template.replaceAll("${fileid}", String(fileid));
this.view.innerHTML = fileRaw
this.view.innerHTML = template.replaceAll("${fileid}", String(fileid))
let name = document.getElementById("file_" + fileid + "_name") as HTMLElement;
let size = document.getElementById("file_" + fileid + "_size") as HTMLElement;

View File

@ -14,7 +14,7 @@
}
.progress {
height: 1.5em;
height: 2.5em;
width: 100%;
background-color: #555;
position: relative;
@ -29,7 +29,7 @@
font-size: 0.8em;
position: absolute;
text-align: center;
top: 5px;
top: 10px;
left: 0;
right: 0;
}
@ -45,7 +45,7 @@
display: inline-block;
height: 100%;
animation: indeterminateAnimation 1s infinite linear;
transform-origin: 0% 50%;
transform-origin: 0 50%;
}

View File

@ -1,4 +1,3 @@
import { deepEqual } from 'fast-equals';
declare var PUBLIC_URL: string;
@ -8,7 +7,7 @@ document.body.innerHTML = require('./main.html') as string;
import { TimeView } from "./timeview";
import { PlantView, PlantViews } from "./plant";
import { PlantViews } from "./plant";
import { NetworkConfigView } from "./network";
import { NightLampView } from "./nightlightview";
import { TankConfigView } from "./tankview";
@ -65,6 +64,17 @@ export class Controller {
console.log(error);
});
}
populateTimezones(): Promise<void> {
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<void> {
return fetch(PUBLIC_URL + "/files")
.then(response => response.json())
@ -195,18 +205,16 @@ export class Controller {
})
}
downloadConfig() :Promise<void> {
async downloadConfig(): Promise<void> {
controller.progressview.addIndeterminate("get_config", "Downloading Config")
return fetch(PUBLIC_URL + "/get_config")
.then(response => response.json())
.then(loaded => {
const response = await fetch(PUBLIC_URL + "/get_config");
const loaded = await response.json();
var currentConfig = loaded as PlantControllerConfig;
controller.setInitialConfig(currentConfig);
controller.setConfig(currentConfig);
//sync json view initially
this.configChanged();
controller.progressview.removeProgress("get_config")
})
controller.configChanged();
controller.progressview.removeProgress("get_config");
}
setInitialConfig(currentConfig: PlantControllerConfig) {
this.initialConfig = currentConfig
@ -250,9 +258,9 @@ export class Controller {
configChanged() {
const current = controller.getConfig();
var pretty = JSON.stringify(current, undefined, 0);
var initial = JSON.stringify(this.initialConfig, undefined, 0);
controller.submitView.setJson(pretty);
if (deepEqual(current, controller.initialConfig)) {
document.title = "PlantCtrl"
} else {
@ -296,7 +304,7 @@ export class Controller {
})
.then(response => response.text())
.then(
text => {
_ => {
clearTimeout(timerId);
controller.progressview.removeProgress("test_pump");
}
@ -308,7 +316,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 +359,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,34 +469,57 @@ 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.measure_moisture();
const tasks = [
{ task: controller.populateTimezones, displayString: "Populating Timezones" },
{ task: controller.updateRTCData, displayString: "Updating RTC Data" },
{ task: controller.updateBatteryData, displayString: "Updating Battery Data" },
{ task: controller.downloadConfig, displayString: "Downloading Configuration" },
{ task: controller.version, displayString: "Fetching Version Information" },
{ task: controller.updateFileList, displayString: "Updating File List" },
{ task: controller.getBackupInfo, displayString: "Fetching Backup Information" },
{ task: controller.loadLogLocaleConfig, displayString: "Loading Log Localization Config" },
{ task: controller.loadTankInfo, displayString: "Loading Tank Information" },
];
async function executeTasksSequentially() {
let current = 0;
for (const { task, displayString } of tasks) {
current++;
let ratio = current / tasks.length;
controller.progressview.addProgress("initial", ratio * 100, displayString);
try {
await task();
} catch (error) {
console.error(`Error executing task '${displayString}':`, error);
// Optionally, you can decide whether to continue or break on errors
break;
}
}
}
executeTasksSequentially().then(r => {
controller.progressview.removeProgress("initial")
});
controller.progressview.removeProgress("rebooting");
window.addEventListener("beforeunload", (event) => {
const currentConfig = controller.getConfig();
// Check if the current state differs from the initial configuration
if (!deepEqual(currentConfig, controller.initialConfig)) {
const confirmationMessage = "You have unsaved changes. Are you sure you want to leave this page?";
// Standard behavior for displaying the confirmation dialog
event.preventDefault();
event.returnValue = confirmationMessage; // This will trigger the browser's default dialog
return confirmationMessage;
}
});

View File

@ -3,8 +3,8 @@ import { Controller } from "./main";
export class NetworkConfigView {
setScanResult(ssidList: SSIDList) {
this.ssidlist.innerHTML = ''
for (var ssid of ssidList.ssids) {
var wi = document.createElement("option");
for (const ssid of ssidList.ssids) {
const wi = document.createElement("option");
wi.value = ssid;
this.ssidlist.appendChild(wi);
}

View File

@ -17,7 +17,7 @@ export class OTAView {
const file = document.getElementById("firmware_file") as HTMLInputElement;
this.file1Upload = file
this.file1Upload.onchange = () => {
var selectedFile = file.files?.[0];
const selectedFile = file.files?.[0];
if (selectedFile == null) {
//TODO error dialog here
return

View File

@ -14,7 +14,7 @@
}
.plantcheckbox{
min-width: 20px;
margin: 0px;
margin: 0;
}
</style>
@ -59,9 +59,17 @@
</div>
<div class="flexcontainer">
<div class="plantkey">Warn Pump Count:</div>
<input class="plantvalue" id="plant_${plantId}_max_consecutive_pump_count" type="number" min="1" , max="50" ,
<input class="plantvalue" id="plant_${plantId}_max_consecutive_pump_count" type="number" min="1" max="50"
placeholder="10">
</div>
<div class="flexcontainer">
<div class="plantkey">Min Frequency Override</div>
<input class="plantvalue" id="plant_${plantId}_min_frequency" type="number" min="1000" max="25000">
</div>
<div class="flexcontainer">
<div class="plantkey">Max Frequency Override</div>
<input class="plantvalue" id="plant_${plantId}_max_frequency" type="number" min="1000" max="25000" >
</div>
<div class="flexcontainer">
<div class="plantkey">Sensor B installed:</div>

View File

@ -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;
@ -60,12 +61,10 @@ export class PlantView {
constructor(plantId: number, parent:HTMLDivElement, controller:Controller) {
const dummy = this;
this.plantId = plantId;
this.plantDiv = document.createElement("div")! as HTMLDivElement
const template = require('./plant.html') as string;
const plantRaw = template.replaceAll("${plantId}", String(plantId));
this.plantDiv.innerHTML = plantRaw
this.plantDiv.innerHTML = template.replaceAll("${plantId}", String(plantId))
this.plantDiv.classList.add("plantcontainer")
parent.appendChild(this.plantDiv)
@ -136,6 +135,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,12 +171,20 @@ 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 = {
return {
// hardcoded for now
sensor_a: true,
mode: this.mode.value,
target_moisture: this.targetMoisture.valueAsNumber,
pump_time_s: this.pumpTimeS.valueAsNumber,
@ -172,16 +192,9 @@ export class PlantView {
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
}
setMoistureA(a: number) {
this.moistureA.innerText = String(a);
}
setMoistureB(b: number) {
this.moistureB.innerText = String(b);
max_consecutive_pump_count: this.maxConsecutivePumpCount.valueAsNumber,
moisture_sensor_min_frequency: this.moistureSensorMinFrequency.valueAsNumber || null,
moisture_sensor_max_frequency: this.moistureSensorMaxFrequency.valueAsNumber || null
};
}
}

View File

@ -1,14 +1,14 @@
<style>
.tankcheckbox {
min-width: 20px;
margin: 0px;
margin: 0;
}
.tankkey{
min-width: 250px;
}
.tankvalue{
flex-grow: 1;
margin: 0px;
margin: 0;
}
.hidden {
display: none;

View File

@ -18,4 +18,11 @@
<div id="timeview_browser_time" style="text-wrap: nowrap; flex-grow: 1;">Local time</div>
</div>
<div style="display:flex">
<span style="min-width: 50px;">Timezone:</span>
<select id="timezone_select" style="text-wrap: nowrap; flex-grow: 1;">
<option value="" disabled selected>Select Timezone</option>
</select>
</div>
<button id="timeview_time_upload">Store Browser time into esp and rtc</button>

View File

@ -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;
@ -33,7 +38,7 @@ export class TimeView {
update(native: string, rtc: string) {
this.esp_time.innerText = native;
this.rtc_time.innerText = rtc;
var date = new Date();
const date = new Date();
this.browser_time.innerText = date.toISOString();
if(this.auto_refresh.checked){
this.timer = setTimeout(this.controller.updateRTCData, 1000);
@ -44,4 +49,26 @@ export class TimeView {
}
}
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";
}
}
}

9
website/.idea/website.iml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>