Compare commits
15 Commits
feature/v4
...
develop
Author | SHA1 | Date | |
---|---|---|---|
3fe9aaeb6f | |||
4f4d15e4a4 | |||
9f48b46738 | |||
171b130a29 | |||
bfc3fbc6e1 | |||
26da6b39cc | |||
a401d4de7b | |||
5fe1dc8f40 | |||
f8274ea7a8 | |||
62d8a38e86 | |||
519c8d2c52 | |||
e941a4973d | |||
2b5c1da484 | |||
cf31ce8d43 | |||
d9c3d4e13c |
@ -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"
|
||||
RUST_BACKTRACE = "full"
|
7
rust/.idea/dictionaries/project.xml
generated
Normal file
7
rust/.idea/dictionaries/project.xml
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="project">
|
||||
<words>
|
||||
<w>sntp</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
1
rust/.idea/vcs.xml
generated
1
rust/.idea/vcs.xml
generated
@ -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>
|
@ -77,7 +77,7 @@ serde = { version = "1.0.192", features = ["derive"] }
|
||||
serde_json = "1.0.108"
|
||||
|
||||
#timezone
|
||||
chrono = { version = "0.4.23", default-features = false , features = ["iana-time-zone" , "alloc"] }
|
||||
chrono = { version = "0.4.23", default-features = false , features = ["iana-time-zone" , "alloc", "serde"] }
|
||||
chrono-tz = {version="0.8.0", default-features = false , features = [ "filter-by-regex" ]}
|
||||
eeprom24x = "0.7.2"
|
||||
url = "2.5.3"
|
||||
|
@ -2,6 +2,7 @@ use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::plant_state::PlantWateringMode;
|
||||
use crate::PLANT_COUNT;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
@ -78,39 +79,39 @@ 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)]
|
||||
#[serde(default)]
|
||||
pub struct PlantConfig {
|
||||
pub mode: Mode,
|
||||
pub target_moisture: u8,
|
||||
pub mode: PlantWateringMode,
|
||||
pub target_moisture: f32,
|
||||
pub pump_time_s: u16,
|
||||
pub pump_cooldown_min: u16,
|
||||
pub pump_hour_start: u8,
|
||||
pub pump_hour_end: u8,
|
||||
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 {
|
||||
mode: Mode::OFF,
|
||||
target_moisture: 40,
|
||||
mode: PlantWateringMode::OFF,
|
||||
target_moisture: 40.,
|
||||
pump_time_s: 30,
|
||||
pump_cooldown_min: 60,
|
||||
pump_hour_start: 9,
|
||||
pump_hour_end: 20,
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
pub enum Mode {
|
||||
OFF,
|
||||
TargetMoisture,
|
||||
TimerOnly,
|
||||
TimerAndDeadzone,
|
||||
}
|
||||
|
923
rust/src/main.rs
923
rust/src/main.rs
File diff suppressed because it is too large
Load Diff
@ -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 {
|
||||
@ -603,7 +599,7 @@ impl PlantCtrlBoard<'_> {
|
||||
self.time()
|
||||
}
|
||||
|
||||
pub fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<u32> {
|
||||
pub fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<f32> {
|
||||
let sensor_channel = match sensor {
|
||||
Sensor::A => match plant {
|
||||
0 => SENSOR_A_1,
|
||||
@ -629,36 +625,32 @@ impl PlantCtrlBoard<'_> {
|
||||
},
|
||||
};
|
||||
|
||||
let mut results = [0_u32; REPEAT_MOIST_MEASURE];
|
||||
let mut results = [0_f32; REPEAT_MOIST_MEASURE];
|
||||
for repeat in 0..REPEAT_MOIST_MEASURE {
|
||||
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;
|
||||
let factor = 1000 as f32 / measurement as f32;
|
||||
let measurement = 100; // TODO what is this scaling factor? what is its purpose?
|
||||
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) as u32;
|
||||
let hz = unscaled as f32 * factor;
|
||||
log(
|
||||
LogMessage::RawMeasure,
|
||||
unscaled as u32,
|
||||
@ -668,7 +660,7 @@ impl PlantCtrlBoard<'_> {
|
||||
);
|
||||
results[repeat] = hz;
|
||||
}
|
||||
results.sort();
|
||||
results.sort_by(|a, b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord
|
||||
|
||||
let mid = results.len() / 2;
|
||||
|
||||
@ -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)
|
||||
|
306
rust/src/plant_state.rs
Normal file
306
rust/src/plant_state.rs
Normal file
@ -0,0 +1,306 @@
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
use chrono_tz::Tz;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{config::PlantConfig, in_time_range, plant_hal};
|
||||
|
||||
const MOIST_SENSOR_MAX_FREQUENCY: f32 = 7500.; // 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 {
|
||||
ShortCircuit { hz: f32, max: f32 },
|
||||
OpenLoop { hz: f32, min: f32 },
|
||||
BoardError(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize)]
|
||||
pub enum MoistureSensorState {
|
||||
Disabled,
|
||||
MoistureValue { raw_hz: f32, moisture_percent: f32 },
|
||||
SensorError(MoistureSensorError),
|
||||
}
|
||||
|
||||
impl MoistureSensorState {
|
||||
pub fn is_err(&self) -> Option<&MoistureSensorError> {
|
||||
match self {
|
||||
MoistureSensorState::SensorError(moisture_sensor_error) => Some(moisture_sensor_error),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn moisture_percent(&self) -> Option<f32> {
|
||||
if let MoistureSensorState::MoistureValue {
|
||||
raw_hz: _,
|
||||
moisture_percent,
|
||||
} = self
|
||||
{
|
||||
Some(*moisture_percent)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MoistureSensorState {}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize)]
|
||||
pub enum PumpError {
|
||||
PumpNotWorking {
|
||||
failed_attempts: usize,
|
||||
max_allowed_failures: usize,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PumpState {
|
||||
consecutive_pump_count: u32,
|
||||
previous_pump: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl PumpState {
|
||||
fn is_err(&self, plant_config: &PlantConfig) -> Option<PumpError> {
|
||||
if self.consecutive_pump_count > plant_config.max_consecutive_pump_count as u32 {
|
||||
Some(PumpError::PumpNotWorking {
|
||||
failed_attempts: self.consecutive_pump_count as usize,
|
||||
max_allowed_failures: plant_config.max_consecutive_pump_count as usize,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum PlantWateringMode {
|
||||
OFF,
|
||||
TargetMoisture,
|
||||
TimerOnly,
|
||||
}
|
||||
|
||||
pub struct PlantState {
|
||||
pub sensor_a: MoistureSensorState,
|
||||
pub sensor_b: MoistureSensorState,
|
||||
pub pump: PumpState,
|
||||
}
|
||||
|
||||
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: min_freq,
|
||||
});
|
||||
}
|
||||
if s > max_freq {
|
||||
return Err(MoistureSensorError::ShortCircuit {
|
||||
hz: s,
|
||||
max: max_freq,
|
||||
});
|
||||
}
|
||||
let moisture_percent = (s - min_freq) * 100.0 / (max_freq - min_freq);
|
||||
|
||||
Ok(moisture_percent)
|
||||
}
|
||||
|
||||
impl PlantState {
|
||||
pub fn read_hardware_state(
|
||||
plant_id: usize,
|
||||
board: &mut plant_hal::PlantCtrlBoard,
|
||||
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,
|
||||
config.moisture_sensor_min_frequency,
|
||||
config.moisture_sensor_max_frequency,
|
||||
) {
|
||||
Ok(moisture_percent) => MoistureSensorState::MoistureValue {
|
||||
raw_hz: raw,
|
||||
moisture_percent,
|
||||
},
|
||||
Err(err) => MoistureSensorState::SensorError(err),
|
||||
},
|
||||
Err(err) => MoistureSensorState::SensorError(MoistureSensorError::BoardError(
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
} 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,
|
||||
config.moisture_sensor_min_frequency,
|
||||
config.moisture_sensor_max_frequency,
|
||||
) {
|
||||
Ok(moisture_percent) => MoistureSensorState::MoistureValue {
|
||||
raw_hz: raw,
|
||||
moisture_percent,
|
||||
},
|
||||
Err(err) => MoistureSensorState::SensorError(err),
|
||||
},
|
||||
Err(err) => MoistureSensorState::SensorError(MoistureSensorError::BoardError(
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
} 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 {
|
||||
sensor_a,
|
||||
sensor_b,
|
||||
pump: PumpState {
|
||||
consecutive_pump_count,
|
||||
previous_pump,
|
||||
},
|
||||
};
|
||||
if state.is_err() {
|
||||
board.fault(plant_id, true);
|
||||
}
|
||||
state
|
||||
}
|
||||
|
||||
pub fn pump_in_timeout(&self, plant_conf: &PlantConfig, current_time: &DateTime<Tz>) -> bool {
|
||||
if matches!(plant_conf.mode, PlantWateringMode::OFF) {
|
||||
return false;
|
||||
}
|
||||
self.pump.previous_pump.is_some_and(|last_pump| {
|
||||
last_pump
|
||||
.checked_add_signed(TimeDelta::minutes(plant_conf.pump_cooldown_min.into()))
|
||||
.is_some_and(|earliest_next_allowed_pump| {
|
||||
earliest_next_allowed_pump > *current_time
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_err(&self) -> bool {
|
||||
self.sensor_a.is_err().is_some() || self.sensor_b.is_err().is_some()
|
||||
}
|
||||
|
||||
pub fn plant_moisture(
|
||||
&self,
|
||||
) -> (
|
||||
Option<f32>,
|
||||
(Option<&MoistureSensorError>, Option<&MoistureSensorError>),
|
||||
) {
|
||||
match (
|
||||
self.sensor_a.moisture_percent(),
|
||||
self.sensor_b.moisture_percent(),
|
||||
) {
|
||||
(Some(moisture_a), Some(moisture_b)) => {
|
||||
(Some((moisture_a + moisture_b) / 2.), (None, None))
|
||||
}
|
||||
(Some(moisture_percent), _) => (Some(moisture_percent), (None, self.sensor_b.is_err())),
|
||||
(_, Some(moisture_percent)) => (Some(moisture_percent), (self.sensor_a.is_err(), None)),
|
||||
_ => (None, (self.sensor_a.is_err(), self.sensor_b.is_err())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn needs_to_be_watered(
|
||||
&self,
|
||||
plant_conf: &PlantConfig,
|
||||
current_time: &DateTime<Tz>,
|
||||
) -> bool {
|
||||
match plant_conf.mode {
|
||||
PlantWateringMode::OFF => false,
|
||||
PlantWateringMode::TargetMoisture => {
|
||||
let (moisture_percent, _) = self.plant_moisture();
|
||||
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 {
|
||||
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 the plant
|
||||
false
|
||||
}
|
||||
}
|
||||
PlantWateringMode::TimerOnly => !self.pump_in_timeout(plant_conf, current_time),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_mqtt_info(&self, plant_conf: &PlantConfig, current_time: &DateTime<Tz>) -> PlantInfo {
|
||||
PlantInfo {
|
||||
sensor_a: &self.sensor_a,
|
||||
sensor_b: &self.sensor_b,
|
||||
mode: plant_conf.mode,
|
||||
do_water: self.needs_to_be_watered(plant_conf, current_time),
|
||||
dry: if let Some(moisture_percent) = self.plant_moisture().0 {
|
||||
moisture_percent < plant_conf.target_moisture
|
||||
} else {
|
||||
false
|
||||
},
|
||||
cooldown: self.pump_in_timeout(plant_conf, current_time),
|
||||
out_of_work_hour: in_time_range(
|
||||
current_time,
|
||||
plant_conf.pump_hour_start,
|
||||
plant_conf.pump_hour_end,
|
||||
),
|
||||
consecutive_pump_count: self.pump.consecutive_pump_count,
|
||||
pump_error: self.pump.is_err(plant_conf),
|
||||
last_pump: self
|
||||
.pump
|
||||
.previous_pump
|
||||
.map(|t| t.with_timezone(¤t_time.timezone())),
|
||||
next_pump: if matches!(
|
||||
plant_conf.mode,
|
||||
PlantWateringMode::TimerOnly | PlantWateringMode::TargetMoisture
|
||||
) {
|
||||
self.pump.previous_pump.and_then(|last_pump| {
|
||||
last_pump
|
||||
.checked_add_signed(TimeDelta::minutes(plant_conf.pump_cooldown_min.into()))
|
||||
.map(|t| t.with_timezone(¤t_time.timezone()))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize)]
|
||||
/// State of a single plant to be tracked
|
||||
pub struct PlantInfo<'a> {
|
||||
/// state of humidity sensor on bank a
|
||||
sensor_a: &'a MoistureSensorState,
|
||||
/// state of humidity sensor on bank b
|
||||
sensor_b: &'a MoistureSensorState,
|
||||
/// configured plant watering mode
|
||||
mode: PlantWateringMode,
|
||||
/// the plant needs to be watered
|
||||
do_water: bool,
|
||||
/// plant is considered to be dry according to settings
|
||||
dry: bool,
|
||||
/// plant irrigation cooldown is active
|
||||
cooldown: bool,
|
||||
/// plant should not be watered at this time of day TODO: does this really belong here? Isn't this a global setting?
|
||||
out_of_work_hour: bool,
|
||||
/// how often has the pump been watered without reaching target moisture
|
||||
consecutive_pump_count: u32,
|
||||
pump_error: Option<PumpError>,
|
||||
/// last time when the pump was active
|
||||
last_pump: Option<DateTime<Tz>>,
|
||||
/// next time when pump should activate
|
||||
next_pump: Option<DateTime<Tz>>,
|
||||
}
|
@ -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,18 +108,18 @@ 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),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_mqtt_info(
|
||||
&self,
|
||||
config: &TankConfig,
|
||||
water_temp: Result<f32, anyhow::Error>,
|
||||
water_temp: &anyhow::Result<f32>,
|
||||
) -> TankInfo {
|
||||
let mut tank_err: Option<TankError> = None;
|
||||
let left_ml = match self.left_ml(config) {
|
||||
@ -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 {
|
||||
@ -151,7 +151,7 @@ impl TankState {
|
||||
.as_ref()
|
||||
.is_ok_and(|temp| *temp < WATER_FROZEN_THRESH),
|
||||
water_temp: water_temp.as_ref().copied().ok(),
|
||||
temp_sensor_error: water_temp.err().map(|err| err.to_string()),
|
||||
temp_sensor_error: water_temp.as_ref().err().map(|err| err.to_string()),
|
||||
percent,
|
||||
}
|
||||
}
|
||||
@ -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>,
|
||||
|
@ -1,8 +1,8 @@
|
||||
//offer ota and config mode
|
||||
|
||||
use crate::{
|
||||
determine_tank_state, get_version, log::LogMessage, map_range_moisture, plant_hal::PLANT_COUNT,
|
||||
BOARD_ACCESS,
|
||||
determine_tank_state, get_version, log::LogMessage, plant_hal::PLANT_COUNT,
|
||||
plant_state::PlantState, BOARD_ACCESS,
|
||||
};
|
||||
use anyhow::bail;
|
||||
use chrono::DateTime;
|
||||
@ -21,6 +21,7 @@ use std::{
|
||||
use url::Url;
|
||||
|
||||
use crate::config::PlantControllerConfig;
|
||||
use crate::plant_state::MoistureSensorState;
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct SSIDList<'a> {
|
||||
@ -35,8 +36,8 @@ struct LoadData<'a> {
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct Moistures {
|
||||
moisture_a: Vec<u8>,
|
||||
moisture_b: Vec<u8>,
|
||||
moisture_a: Vec<std::string::String>,
|
||||
moisture_b: Vec<std::string::String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@ -77,63 +78,78 @@ fn write_time(
|
||||
anyhow::Ok(None)
|
||||
}
|
||||
|
||||
fn get_live_moisture(
|
||||
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 mut a: Vec<u8> = Vec::new();
|
||||
let mut b: Vec<u8> = Vec::new();
|
||||
for plant in 0..8 {
|
||||
let a_hz = board.measure_moisture_hz(plant, crate::plant_hal::Sensor::A)?;
|
||||
let b_hz = board.measure_moisture_hz(plant, crate::plant_hal::Sensor::B)?;
|
||||
let a_pct = map_range_moisture(a_hz as f32);
|
||||
|
||||
match a_pct {
|
||||
Ok(result) => {
|
||||
a.push(result);
|
||||
}
|
||||
Err(_) => {
|
||||
a.push(200);
|
||||
}
|
||||
}
|
||||
|
||||
let b_pct = map_range_moisture(b_hz as f32);
|
||||
match b_pct {
|
||||
Ok(result) => {
|
||||
b.push(result);
|
||||
}
|
||||
Err(_) => {
|
||||
b.push(200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let data = Moistures {
|
||||
moisture_a: a,
|
||||
moisture_b: b,
|
||||
let data = LoadData {
|
||||
rtc: rtc.as_str(),
|
||||
native: native.as_str(),
|
||||
};
|
||||
let json = serde_json::to_string(&data)?;
|
||||
|
||||
anyhow::Ok(Some(json))
|
||||
}
|
||||
|
||||
fn get_data(
|
||||
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> {
|
||||
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 config = board.get_config().unwrap();
|
||||
|
||||
let data = LoadData {
|
||||
rtc: rtc.as_str(),
|
||||
native: native.as_str(),
|
||||
let plant_state = Vec::from_iter(
|
||||
(0..PLANT_COUNT).map(|i| PlantState::read_hardware_state(i, &mut board, &config.plants[i])),
|
||||
);
|
||||
let a = Vec::from_iter(
|
||||
plant_state
|
||||
.iter()
|
||||
.map(|s| {
|
||||
match &s.sensor_a {
|
||||
MoistureSensorState::Disabled => "disabled".to_string(),
|
||||
MoistureSensorState::MoistureValue {raw_hz, moisture_percent } => {
|
||||
format!("{moisture_percent:.2}% {raw_hz}hz",)
|
||||
}
|
||||
MoistureSensorState::SensorError(err) => format!("{err:?}"),
|
||||
}
|
||||
})
|
||||
);
|
||||
let b = Vec::from_iter(
|
||||
plant_state
|
||||
.iter()
|
||||
.map(|s| {
|
||||
match &s.sensor_b {
|
||||
MoistureSensorState::Disabled => "disabled".to_string(),
|
||||
MoistureSensorState::MoistureValue {raw_hz, moisture_percent } => {
|
||||
format!("{moisture_percent:.2}% {raw_hz}hz",)
|
||||
}
|
||||
MoistureSensorState::SensorError(err) => format!("{err:?}"),
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let data = Moistures {
|
||||
moisture_a: a,
|
||||
moisture_b: b,
|
||||
};
|
||||
let json = serde_json::to_string(&data)?;
|
||||
|
||||
@ -165,7 +181,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()
|
||||
@ -257,7 +273,7 @@ fn tank_info(
|
||||
//should be multsampled
|
||||
let water_temp = board.water_temperature_c();
|
||||
Ok(Some(serde_json::to_string(
|
||||
&tank_info.as_mqtt_info(&config.tank, water_temp),
|
||||
&tank_info.as_mqtt_info(&config.tank, &water_temp),
|
||||
)?))
|
||||
}
|
||||
|
||||
@ -289,7 +305,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(
|
||||
@ -332,7 +348,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<()> {
|
||||
@ -371,18 +387,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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -415,7 +429,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
|
||||
@ -670,6 +684,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(
|
||||
@ -685,7 +704,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 =
|
||||
@ -710,7 +729,7 @@ fn handle_error_to500(
|
||||
cors_response(request, 500, &error_text)?;
|
||||
}
|
||||
}
|
||||
return anyhow::Ok(());
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
fn read_up_to_bytes_from_request(
|
||||
|
@ -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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -101,8 +106,8 @@ interface GetTime {
|
||||
}
|
||||
|
||||
interface Moistures {
|
||||
moisture_a: [number],
|
||||
moisture_b: [number],
|
||||
moisture_a: [string],
|
||||
moisture_b: [string],
|
||||
}
|
||||
|
||||
interface VersionInfo {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Controller } from "./main";
|
||||
import {Controller} from "./main";
|
||||
|
||||
const regex = /[^a-zA-Z0-9_.]/g;
|
||||
|
||||
@ -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;
|
||||
|
@ -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%;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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 => {
|
||||
var currentConfig = loaded as PlantControllerConfig;
|
||||
controller.setInitialConfig(currentConfig);
|
||||
controller.setConfig(currentConfig);
|
||||
//sync json view initially
|
||||
this.configChanged();
|
||||
controller.progressview.removeProgress("get_config")
|
||||
})
|
||||
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
|
||||
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;
|
||||
}
|
||||
});
|
@ -3,9 +3,9 @@ import { Controller } from "./main";
|
||||
export class NetworkConfigView {
|
||||
setScanResult(ssidList: SSIDList) {
|
||||
this.ssidlist.innerHTML = ''
|
||||
for (var ssid of ssidList.ssids) {
|
||||
var wi = document.createElement("option");
|
||||
wi.value = ssid;
|
||||
for (const ssid of ssidList.ssids) {
|
||||
const wi = document.createElement("option");
|
||||
wi.value = ssid;
|
||||
this.ssidlist.appendChild(wi);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -1,8 +1,7 @@
|
||||
|
||||
const PLANT_COUNT = 8;
|
||||
|
||||
|
||||
import { Controller } from "./main";
|
||||
import {Controller} from "./main";
|
||||
|
||||
export class PlantViews {
|
||||
private readonly measure_moisture: HTMLButtonElement;
|
||||
@ -25,7 +24,7 @@ export class PlantViews {
|
||||
}
|
||||
return rv
|
||||
}
|
||||
update(moisture_a: [number], moisture_b: [number]) {
|
||||
update(moisture_a: [string], moisture_b: [string]) {
|
||||
for (let plantId = 0; plantId < PLANT_COUNT; plantId++) {
|
||||
const a = moisture_a[plantId]
|
||||
const b = moisture_b[plantId]
|
||||
@ -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.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,20 +135,24 @@ 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) {
|
||||
if (a == 200){
|
||||
this.moistureA.innerText = "error"
|
||||
} else {
|
||||
this.moistureA.innerText = String(a)
|
||||
}
|
||||
|
||||
if (b == 200){
|
||||
this.moistureB.innerText = "error"
|
||||
} else {
|
||||
this.moistureB.innerText = String(b)
|
||||
}
|
||||
update(a: string, b: string) {
|
||||
this.moistureA.innerText = a
|
||||
this.moistureB.innerText = b
|
||||
}
|
||||
|
||||
setConfig(plantConfig: PlantConfig) {
|
||||
@ -159,29 +162,30 @@ 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
|
||||
}
|
||||
|
||||
setMoistureA(a: number) {
|
||||
this.moistureA.innerText = String(a);
|
||||
}
|
||||
|
||||
setMoistureB(b: number) {
|
||||
this.moistureB.innerText = String(b);
|
||||
getConfig(): PlantConfig {
|
||||
return {
|
||||
// hardcoded for now
|
||||
sensor_a: true,
|
||||
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 || null,
|
||||
moisture_sensor_max_frequency: this.moistureSensorMaxFrequency.valueAsNumber || null
|
||||
};
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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
9
website/.idea/website.iml
generated
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user