diff --git a/rust/build.rs b/rust/build.rs index 5eaa3e2..af70f78 100644 --- a/rust/build.rs +++ b/rust/build.rs @@ -3,16 +3,23 @@ use std::process::Command; use vergen::EmitBuilder; fn main() { println!("cargo:rerun-if-changed=./src/src_webpack"); - Command::new("rm").arg("./src/webserver/bundle.js").output().expect("failed to execute process"); - - let output = Command::new("npx").arg("webpack").current_dir("./src_webpack").output().expect("failed to execute process"); - + Command::new("rm") + .arg("./src/webserver/bundle.js") + .output() + .expect("failed to execute process"); + + let output = Command::new("npx") + .arg("webpack") + .current_dir("./src_webpack") + .output() + .expect("failed to execute process"); + println!("status: {}", output.status); println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); println!("stderr: {}", String::from_utf8_lossy(&output.stderr)); - + assert!(output.status.success()); embuild::espidf::sysenv::output(); let _ = EmitBuilder::builder().all_git().emit(); -} \ No newline at end of file +} diff --git a/rust/partitions.csv b/rust/partitions.csv index 3b2aae0..56d5cd7 100644 --- a/rust/partitions.csv +++ b/rust/partitions.csv @@ -1,6 +1,6 @@ nvs, data, nvs, , 16k, otadata, data, ota, , 8k, phy_init, data, phy, , 4k, -ota_0, app, ota_0, , 1792K, +ota_0, app, ota_0, , 1792K, ota_1, app, ota_1, , 1792K, storage, data, spiffs, , 400K, \ No newline at end of file diff --git a/rust/rust-toolchain.toml b/rust/rust-toolchain.toml index a2f5ab5..a25e7e0 100644 --- a/rust/rust-toolchain.toml +++ b/rust/rust-toolchain.toml @@ -1,2 +1,3 @@ [toolchain] -channel = "esp" +channel = "nightly" + diff --git a/rust/sdkconfig.defaults b/rust/sdkconfig.defaults index bbd6cb1..a007ecf 100644 --- a/rust/sdkconfig.defaults +++ b/rust/sdkconfig.defaults @@ -1,5 +1,5 @@ # Rust often needs a bit of an extra main task stack size compared to C (the default is 3K) -CONFIG_ESP_MAIN_TASK_STACK_SIZE=8000 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=20000 # Use this to set FreeRTOS kernel tick frequency to 1000 Hz (100 Hz by default). # This allows to use 1 ms granuality for thread sleeps (10 ms by default). diff --git a/rust/src/config.rs b/rust/src/config.rs index b862051..e99f837 100644 --- a/rust/src/config.rs +++ b/rust/src/config.rs @@ -1,54 +1,78 @@ use std::fmt; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; use crate::PLANT_COUNT; - -#[derive(Serialize, Deserialize)] -#[derive(Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct Config { - tank_sensor_enabled: bool, - tank_full_ml: u32, - tank_warn_percent: u8, + pub mqtt_url: heapless::String<128>, + pub base_topic: heapless::String<64>, + pub max_consecutive_pump_count: u8, - night_lamp_hour_start: u8, - night_lamp_hour_end: u8, - night_lamp_only_when_dark: bool, + pub tank_allow_pumping_if_sensor_error: bool, + pub tank_sensor_enabled: bool, + pub tank_useable_ml: u32, + pub tank_warn_percent: u8, + pub tank_empty_mv: f32, + pub tank_full_mv: f32, - plants: [Plant;PLANT_COUNT] + pub night_lamp_hour_start: u8, + pub night_lamp_hour_end: u8, + pub night_lamp_only_when_dark: bool, + + pub plants: [Plant; PLANT_COUNT], } impl Default for Config { - fn default() -> Self { - Self { tank_sensor_enabled: true, - tank_full_ml: 5000, - tank_warn_percent: 50, - night_lamp_hour_start: 21, - night_lamp_hour_end: 2, - night_lamp_only_when_dark: true, - plants: [Plant::default();PLANT_COUNT] + fn default() -> Self { + Self { + base_topic: "plant/one".into(), + mqtt_url: "mqtt://192.168.1.1:1883".into(), + tank_allow_pumping_if_sensor_error: true, + tank_sensor_enabled: true, + tank_warn_percent: 50, + night_lamp_hour_start: 21, + night_lamp_hour_end: 2, + night_lamp_only_when_dark: true, + plants: [Plant::default(); PLANT_COUNT], + max_consecutive_pump_count: 15, + tank_useable_ml: 5000, + tank_empty_mv: 0.1, + tank_full_mv: 3.3, + } + } +} +#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)] +pub enum Mode { + OFF, + TargetMoisture, + TimerOnly, +} + +#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)] +pub struct Plant { + pub mode: Mode, + pub target_moisture: u8, + pub pump_time_s: u16, + pub pump_cooldown_min: u16, + pub pump_hour_start: u8, + pub pump_hour_end: u8, +} +impl Default for Plant { + fn default() -> Self { + Self { + target_moisture: 40, + pump_time_s: 60, + pump_cooldown_min: 60, + pump_hour_start: 8, + pump_hour_end: 20, + mode: Mode::OFF, } } } -#[derive(Serialize, Deserialize, Copy, Clone)] -#[derive(Debug)] -pub struct Plant{ - target_moisture: u8, - pump_time_s: u16, - pump_cooldown_min: u16, - pump_hour_start: u8, - pump_hour_end: u8 -} -impl Default for Plant { - fn default() -> Self { - Self { target_moisture: 40, pump_time_s: 60, pump_cooldown_min: 60, pump_hour_start: 8, pump_hour_end: 20 } - } -} - -#[derive(Serialize, Deserialize)] -#[derive(Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct WifiConfig { pub ssid: heapless::String<32>, pub password: Option>, @@ -58,4 +82,4 @@ impl fmt::Display for WifiConfig { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, ****)", self.ssid) } -} \ No newline at end of file +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 6c53449..334fce6 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,4 +1,4 @@ #![allow(dead_code)] extern crate embedded_hal as hal; -pub mod sipo; \ No newline at end of file +pub mod sipo; diff --git a/rust/src/main.rs b/rust/src/main.rs index 2297ef5..b6456d5 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -1,60 +1,97 @@ -use std::{sync::{Arc, Mutex, atomic::AtomicBool}, env}; +use std::{ + env, + sync::{atomic::AtomicBool, Arc, Mutex}, +}; -use chrono::{Datelike, NaiveDateTime, Timelike}; -use once_cell::sync::Lazy; use anyhow::Result; +use chrono::{Datelike, Duration, NaiveDateTime, Timelike}; use chrono_tz::Europe::Berlin; use esp_idf_hal::delay::Delay; -use esp_idf_sys::{esp_restart, vTaskDelay}; +use esp_idf_sys::{esp_restart, uxTaskGetStackHighWaterMark, vTaskDelay}; +use esp_ota::rollback_and_reboot; +use log::error; +use once_cell::sync::Lazy; use plant_hal::{CreatePlantHal, PlantCtrlBoard, PlantCtrlBoardInteraction, PlantHal, PLANT_COUNT}; +use serde::{Deserialize, Serialize}; -use crate::{config::{Config, WifiConfig}, webserver::webserver::{httpd_initial, httpd}}; +use crate::{ + config::{Config, WifiConfig}, + webserver::webserver::{httpd, httpd_initial}, +}; mod config; pub mod plant_hal; + +const MOIST_SENSOR_MAX_FREQUENCY: u32 = 5200; // 60kHz (500Hz margin) +const MOIST_SENSOR_MIN_FREQUENCY: u32 = 500; // 0.5kHz (500Hz margin) + +const FROM: (f32, f32) = ( + MOIST_SENSOR_MIN_FREQUENCY as f32, + MOIST_SENSOR_MAX_FREQUENCY as f32, +); +const TO: (f32, f32) = (0_f32, 100_f32); + mod webserver { pub mod webserver; } -#[derive(PartialEq)] +#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)] enum OnlineMode { Offline, Wifi, SnTp, - Mqtt, - MqttRoundtrip } -enum WaitType{ +#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)] +enum WaitType { InitialConfig, FlashError, - NormalConfig + NormalConfig, + StayAlive, } -fn wait_infinity(wait_type:WaitType, reboot_now:Arc) -> !{ +#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Default)] +struct PlantState { + a: u8, + b: u8, + p: u8, + after_p: u8, + dry: bool, + active: bool, + pump_error: bool, + not_effective: bool, + cooldown: bool, + no_water: bool, +} + +fn wait_infinity(wait_type: WaitType, reboot_now: Arc) -> ! { let delay = match wait_type { WaitType::InitialConfig => 250_u32, WaitType::FlashError => 100_u32, - WaitType::NormalConfig => 500_u32 + WaitType::NormalConfig => 500_u32, + WaitType::StayAlive => 1000_u32, }; let led_count = match wait_type { WaitType::InitialConfig => 8, WaitType::FlashError => 8, - WaitType::NormalConfig => 4 + WaitType::NormalConfig => 4, + WaitType::StayAlive => 2, }; - BOARD_ACCESS.lock().unwrap().light(true).unwrap(); loop { unsafe { //do not trigger watchdog for i in 0..8 { - BOARD_ACCESS.lock().unwrap().fault(i, i ) -> !{ } } -pub static BOARD_ACCESS: Lazy> = Lazy::new(|| { - PlantHal::create().unwrap() -}); +pub static BOARD_ACCESS: Lazy> = Lazy::new(|| PlantHal::create().unwrap()); +pub static STAY_ALIVE: Lazy = Lazy::new(|| AtomicBool::new(false)); + +fn map_range(from_range: (f32, f32), to_range: (f32, f32), s: f32) -> f32 { + to_range.0 + (s - from_range.0) * (to_range.1 - to_range.0) / (from_range.1 - from_range.0) +} fn main() -> Result<()> { // It is necessary to call this function once. Otherwise some patches to the runtime @@ -75,16 +115,45 @@ fn main() -> Result<()> { // Bind the log crate to the ESP Logging facilities esp_idf_svc::log::EspLogger::initialize_default(); + if esp_idf_sys::CONFIG_MAIN_TASK_STACK_SIZE < 20000 { + error!( + "stack too small: {} bail!", + esp_idf_sys::CONFIG_MAIN_TASK_STACK_SIZE + ); + return Ok(()); + } + log::info!("Startup Rust"); let git_hash = env!("VERGEN_GIT_DESCRIBE"); println!("Version useing git has {}", git_hash); + let mut partition_state: embedded_svc::ota::SlotState = embedded_svc::ota::SlotState::Unknown; + // match esp_idf_svc::ota::EspOta::new() { + // Ok(ota) => { + // match ota.get_running_slot(){ + // Ok(slot) => { + // partition_state = slot.state; + // println!( + // "Booting from {} with state {:?}", + // slot.label, partition_state + // ); + // }, + // Err(err) => { + // println!("Error getting running slot {}", err); + // }, + // } + // }, + // Err(err) => { + // println!("Error obtaining ota info {}", err); + // }, + // } + println!("Board hal init"); - let mut board = BOARD_ACCESS.lock().unwrap(); + let mut board: std::sync::MutexGuard<'_, PlantCtrlBoard<'_>> = BOARD_ACCESS.lock().unwrap(); println!("Mounting filesystem"); - board.mountFileSystem()?; - let free_space = board.fileSystemSize()?; + board.mount_file_system()?; + let free_space = board.file_system_size()?; println!( "Mounted, total space {} used {} free {}", free_space.total_size, free_space.used_size, free_space.free_size @@ -111,8 +180,15 @@ fn main() -> Result<()> { println!("cur is {}", cur); if board.is_config_reset() { + board.general_fault(true); println!("Reset config is pressed, waiting 5s"); - Delay::new_default().delay_ms(5000); + for i in 0..25 { + board.general_fault(true); + Delay::new_default().delay_ms(50); + board.general_fault(false); + Delay::new_default().delay_ms(50); + } + if board.is_config_reset() { println!("Reset config is still pressed, deleting configs and reboot"); match board.remove_configs() { @@ -126,17 +202,29 @@ fn main() -> Result<()> { wait_infinity(WaitType::FlashError, Arc::new(AtomicBool::new(false))); } } + } else { + board.general_fault(false); } } let mut online_mode = OnlineMode::Offline; let wifi_conf = board.get_wifi(); let wifi: WifiConfig; - match wifi_conf{ + match wifi_conf { Ok(conf) => { wifi = conf; - }, + } Err(err) => { + if board.is_wifi_config_file_existant() { + match partition_state { + embedded_svc::ota::SlotState::Invalid + | embedded_svc::ota::SlotState::Unverified => { + println!("Config seem to be unparsable after upgrade, reverting"); + rollback_and_reboot()?; + } + _ => {} + } + } println!("Missing wifi config, entering initial config mode {}", err); board.wifi_ap().unwrap(); //config upload will trigger reboot! @@ -144,48 +232,9 @@ fn main() -> Result<()> { let reboot_now = Arc::new(AtomicBool::new(false)); let _webserver = httpd_initial(reboot_now.clone()); wait_infinity(WaitType::InitialConfig, reboot_now.clone()); - }, + } }; - - //check if we have a config file - // if not found or parsing error -> error very fast blink general fault - //if this happens after a firmeware upgrade (check image state), mark as invalid - //blink general fault error_reading_config_after_upgrade, reboot after - // open accesspoint with webserver for wlan mqtt setup - //blink general fault error_no_config_after_upgrade - //once config is set store it and reboot - - //if proceed.tank_sensor_enabled() { - - //} - //is tank sensor enabled in config? - //measure tank level (without wifi due to interference) - //TODO this should be a result// detect invalid measurement value - let tank_value = board.tank_sensor_mv(); - match tank_value { - Ok(tank_raw) => { - println!("Tank sensor returned {}", tank_raw); - } - Err(_) => { - //if not possible value, blink general fault error_tank_sensor_fault - board.general_fault(true); - //set general fault persistent - //set tank sensor state to fault - } - } - - //measure each plant moisture - let mut initial_measurements_a: [i32; PLANT_COUNT] = [0; PLANT_COUNT]; - let mut initial_measurements_b: [i32; PLANT_COUNT] = [0; PLANT_COUNT]; - let mut initial_measurements_p: [i32; PLANT_COUNT] = [0; PLANT_COUNT]; - for plant in 0..PLANT_COUNT { - initial_measurements_a[plant] = board.measure_moisture_hz(plant, plant_hal::Sensor::A)?; - initial_measurements_b[plant] = board.measure_moisture_hz(plant, plant_hal::Sensor::B)?; - initial_measurements_p[plant] = - board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)?; - } - println!("attempting to connect wifi"); match board.wifi(&wifi.ssid, wifi.password.as_deref(), 10000) { Ok(_) => { @@ -202,7 +251,7 @@ fn main() -> Result<()> { Ok(new_time) => { cur = new_time; online_mode = OnlineMode::SnTp; - }, + } Err(err) => { println!("sntp error: {}", err); board.general_fault(true); @@ -213,11 +262,11 @@ fn main() -> Result<()> { println!("Running logic at europe/berlin {}", europe_time); } - let config:Config; - match (board.get_config()){ + let config: Config; + match board.get_config() { Ok(valid) => { config = valid; - }, + } Err(err) => { println!("Missing normal config, entering config mode {}", err); //config upload will trigger reboot! @@ -225,49 +274,150 @@ fn main() -> Result<()> { let reboot_now = Arc::new(AtomicBool::new(false)); let _webserver = httpd(reboot_now.clone()); wait_infinity(WaitType::NormalConfig, reboot_now.clone()); - }, + } } + //do mqtt before config check, as mqtt might configure if online_mode == OnlineMode::SnTp { - //mqtt here + match board.mqtt(&config) { + Ok(_) => { + println!("Mqtt connection ready"); + } + Err(err) => { + println!("Could not connect mqtt due to {}", err); + } + } } - if online_mode == OnlineMode::Mqtt { - //mqtt roundtrip here + + match board.battery_state() { + Ok(_state) => {} + Err(err) => { + board.general_fault(true); + println!("Could not read battery state, assuming low power {}", err); + } + } + + let mut enough_water = true; + if config.tank_sensor_enabled { + let tank_value = board.tank_sensor_mv(); + match tank_value { + Ok(tank_raw) => { + //FIXME clear + let percent = map_range( + (config.tank_empty_mv, config.tank_full_mv), + (0_f32, 100_f32), + tank_raw.into(), + ); + let left_ml = ((percent / 100_f32) * config.tank_useable_ml as f32) as u32; + println!( + "Tank sensor returned mv {} as {}% leaving {} ml useable", + tank_raw, percent as u8, left_ml + ); + if config.tank_warn_percent > percent as u8 { + board.general_fault(true); + println!( + "Low water, current percent is {}, minimum warn level is {}", + percent as u8, config.tank_warn_percent + ); + //FIXME warn here + } + if config.tank_warn_percent <= 0 { + enough_water = false; + } + } + Err(_) => { + board.general_fault(true); + if !config.tank_allow_pumping_if_sensor_error { + enough_water = false; + } + //set tank sensor state to fault + } + } + } + + let plantstate = [PlantState { + ..Default::default() + }; PLANT_COUNT]; + for plant in 0..PLANT_COUNT { + let mut state = plantstate[plant]; + //return mapf(mMoisture_raw.getMedian(), MOIST_SENSOR_MIN_FRQ, MOIST_SENSOR_MAX_FRQ, 0, 100); + state.a = map_range( + FROM, + TO, + board.measure_moisture_hz(plant, plant_hal::Sensor::A)? as f32, + ) as u8; + state.b = map_range( + FROM, + TO, + board.measure_moisture_hz(plant, plant_hal::Sensor::B)? as f32, + ) as u8; + state.p = map_range( + FROM, + TO, + board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32, + ) as u8; + let plant_config = config.plants[plant]; + + //FIXME how to average analyze whatever? + if state.a < plant_config.target_moisture || state.b < plant_config.target_moisture { + state.dry = true; + if !enough_water { + state.no_water = true; + } + } + + let duration = Duration::minutes((60 * plant_config.pump_cooldown_min).into()); + if (board.last_pump_time(plant)? + duration) > cur { + state.cooldown = true; + } + + if state.dry { + let consecutive_pump_count = board.consecutive_pump_count(plant) + 1; + board.store_consecutive_pump_count(plant, consecutive_pump_count); + if consecutive_pump_count > config.max_consecutive_pump_count.into() { + state.not_effective = true; + board.fault(plant, true); + } + } else { + board.store_consecutive_pump_count(plant, 0); + } + + //TODO update mqtt state here! + } + + if (STAY_ALIVE.load(std::sync::atomic::Ordering::Relaxed)) { + drop(board); + let reboot_now = Arc::new(AtomicBool::new(false)); + let _webserver = httpd(reboot_now.clone()); + wait_infinity(WaitType::StayAlive, reboot_now.clone()); + } + + 'eachplant: for plant in 0..PLANT_COUNT { + let mut state = plantstate[plant]; + if (state.dry && !state.cooldown) { + println!("Trying to pump with pump {} now", plant); + let plant_config = config.plants[plant]; + + board.any_pump(true)?; + board.store_last_pump_time(plant, cur); + board.pump(plant, true)?; + board.last_pump_time(plant)?; + state.active = true; + unsafe { vTaskDelay(plant_config.pump_time_s.into()) }; + state.after_p = map_range( + FROM, + TO, + board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32, + ) as u8; + if state.after_p < state.p + 5 { + state.pump_error = true; + board.fault(plant, true); + } + break 'eachplant; + } } - //TODO configmode webserver logic here /* - - - //if config battery mode - //read battery level - //if not possible set general fault persistent, but do continue - //else - //assume 12v and max capacity - - //if tank sensor is enabled - //if tank sensor fault abort if config require is set - //check if water is > minimum allowed || fault - //if not, set all plants requiring water to persistent fault - - //for each plant - //check if moisture is < target - //state += dry - //check if in cooldown - //state += cooldown - //check if consecutive pumps > limit - //state += notworking - //set plant fault persistent - - //pump one cycle - // set last pump time to now - //during pump state += active - //after pump check if Pump moisture value is increased by config delta x - // state -= active - // state += cooldown - // if not set plant error persistent fault - // state += notworking - //set consecutive pumps+=1 //check if during light time //lightstate += out of worktime @@ -287,7 +437,7 @@ fn main() -> Result<()> { } */ //deepsleep here? - return Ok(()); + Ok(()) } //error codes diff --git a/rust/src/plant_hal.rs b/rust/src/plant_hal.rs index 25001d5..b5eb449 100644 --- a/rust/src/plant_hal.rs +++ b/rust/src/plant_hal.rs @@ -1,9 +1,12 @@ //mod config; use embedded_svc::wifi::{ - AccessPointConfiguration, AuthMethod, ClientConfiguration, Configuration, AccessPointInfo, + AccessPointConfiguration, AccessPointInfo, AuthMethod, ClientConfiguration, Configuration, }; + use esp_idf_svc::eventloop::EspSystemEventLoop; +use esp_idf_svc::mqtt::client::QoS::ExactlyOnce; +use esp_idf_svc::mqtt::client::{EspMqttClient, MqttClientConfiguration}; use esp_idf_svc::nvs::EspDefaultNvsPartition; use esp_idf_svc::wifi::config::{ScanConfig, ScanType}; use esp_idf_svc::wifi::EspWifi; @@ -11,13 +14,13 @@ use plant_ctrl2::sipo::ShiftRegister40; use anyhow::anyhow; use anyhow::{bail, Ok, Result}; -use strum::EnumString; +use serde::{Deserialize, Serialize}; use std::ffi::CString; use std::fs::File; -use std::io::Read; use std::path::Path; -use std::str::from_utf8; -use std::sync::{Mutex, Arc}; + +use std::sync::atomic::AtomicBool; +use std::sync::{Arc, Mutex}; use std::time::Duration; use chrono::{DateTime, NaiveDateTime, Utc}; @@ -34,10 +37,11 @@ use esp_idf_hal::prelude::Peripherals; use esp_idf_hal::reset::ResetReason; use esp_idf_svc::sntp::{self, SyncStatus}; use esp_idf_svc::systime::EspSystemTime; -use esp_idf_sys::{EspError, vTaskDelay}; +use esp_idf_sys::{vTaskDelay, EspError}; use one_wire_bus::OneWire; -use crate::config::{self, WifiConfig, Config}; +use crate::config::{self, Config, WifiConfig}; +use crate::STAY_ALIVE; pub const PLANT_COUNT: usize = 8; const PINS_PER_PLANT: usize = 5; @@ -58,8 +62,9 @@ static mut CONSECUTIVE_WATERING_PLANT: [u32; PLANT_COUNT] = [0; PLANT_COUNT]; #[link_section = ".rtc.data"] static mut LOW_VOLTAGE_DETECTED: bool = false; +#[derive(Serialize, Deserialize, Debug)] pub struct BatteryState { - state_charge_percent: u8, + pub state_charge_percent: u8, max_error_percent: u8, remaining_milli_ampere_hour: u32, max_milli_ampere_hour: u32, @@ -74,18 +79,37 @@ pub struct BatteryState { state_health_percent: u8, } +impl Default for BatteryState { + fn default() -> Self { + BatteryState { + state_charge_percent: 50, + max_error_percent: 100, + remaining_milli_ampere_hour: 100, + max_milli_ampere_hour: 200, + design_milli_ampere_hour: 200, + voltage_milli_volt: 12, + average_current_milli_ampere: 50, + temperature_tenth_kelvin: 1337, + average_time_to_empty_minute: 123, + average_time_to_full_minute: 123, + average_discharge_power_cycle_milli_watt: 123, + cycle_count: 123, + state_health_percent: 90, + } + } +} + pub struct FileSystemSizeInfo { pub total_size: usize, pub used_size: usize, pub free_size: usize, } - #[derive(strum::Display)] pub enum ClearConfigType { WifiConfig, Config, - None + None, } #[derive(Debug)] @@ -98,8 +122,8 @@ pub trait PlantCtrlBoardInteraction { fn time(&mut self) -> Result>; fn wifi(&mut self, ssid: &str, password: Option<&str>, max_wait: u32) -> Result<()>; fn sntp(&mut self, max_wait: u32) -> Result>; - fn mountFileSystem(&mut self) -> Result<()>; - fn fileSystemSize(&mut self) -> Result; + fn mount_file_system(&mut self) -> Result<()>; + fn file_system_size(&mut self) -> Result; fn battery_state(&mut self) -> Result; @@ -137,6 +161,8 @@ pub trait PlantCtrlBoardInteraction { fn wifi_ap(&mut self) -> Result<()>; fn wifi_scan(&mut self) -> Result>; fn test(&mut self) -> Result<()>; + fn is_wifi_config_file_existant(&mut self) -> bool; + fn mqtt(&mut self, config: &Config) -> Result<()>; } pub trait CreatePlantHal<'a> { @@ -145,13 +171,504 @@ pub trait CreatePlantHal<'a> { pub struct PlantHal {} +pub struct PlantCtrlBoard<'a> { + shift_register: ShiftRegister40< + PinDriver<'a, esp_idf_hal::gpio::Gpio21, esp_idf_hal::gpio::Output>, + PinDriver<'a, esp_idf_hal::gpio::Gpio22, esp_idf_hal::gpio::Output>, + PinDriver<'a, esp_idf_hal::gpio::Gpio19, esp_idf_hal::gpio::Output>, + >, + consecutive_watering_plant: Mutex<[u32; PLANT_COUNT]>, + last_watering_timestamp: Mutex<[i64; PLANT_COUNT]>, + low_voltage_detected: Mutex, + tank_driver: AdcDriver<'a, esp_idf_hal::adc::ADC1>, + tank_channel: esp_idf_hal::adc::AdcChannelDriver<'a, { attenuation::DB_11 }, Gpio39>, + solar_is_day: PinDriver<'a, esp_idf_hal::gpio::Gpio25, esp_idf_hal::gpio::Input>, + boot_button: PinDriver<'a, esp_idf_hal::gpio::Gpio0, esp_idf_hal::gpio::Input>, + signal_counter: PcntDriver<'a>, + light: PinDriver<'a, esp_idf_hal::gpio::Gpio26, esp_idf_hal::gpio::Output>, + main_pump: PinDriver<'a, esp_idf_hal::gpio::Gpio23, esp_idf_hal::gpio::Output>, + tank_power: PinDriver<'a, esp_idf_hal::gpio::Gpio27, esp_idf_hal::gpio::Output>, + general_fault: PinDriver<'a, esp_idf_hal::gpio::Gpio13, esp_idf_hal::gpio::Output>, + pub wifi_driver: EspWifi<'a>, + one_wire_bus: OneWire>, + mqtt_client: Option>, +} + +impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> { + fn battery_state(&mut self) -> Result { + return Ok(BatteryState::default()); + } + + fn is_day(&self) -> bool { + self.solar_is_day.get_level().into() + } + + fn water_temperature_c(&mut self) -> Result { + let mut delay = Delay::new_default(); + + self.one_wire_bus + .reset(&mut delay) + .map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?; + let first = self.one_wire_bus.devices(false, &mut delay).next(); + if first.is_none() { + bail!("Not found any one wire Ds18b20"); + } + let device_address = first + .unwrap() + .map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?; + + let water_temp_sensor = Ds18b20::new::(device_address) + .map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?; + + water_temp_sensor + .start_temp_measurement(&mut self.one_wire_bus, &mut delay) + .map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?; + ds18b20::Resolution::Bits12.delay_for_measurement_time(&mut delay); + let sensor_data = water_temp_sensor + .read_data(&mut self.one_wire_bus, &mut delay) + .map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?; + if sensor_data.temperature == 85_f32 { + bail!("Ds18b20 dummy temperature returned"); + } + Ok(sensor_data.temperature) + } + + fn tank_sensor_mv(&mut self) -> Result { + let delay = Delay::new_default(); + self.tank_power.set_high()?; + //let stabilize + delay.delay_ms(100); + let value = self.tank_driver.read(&mut self.tank_channel)?; + self.tank_power.set_low()?; + Ok(value) + } + + fn set_low_voltage_in_cycle(&mut self) { + *self.low_voltage_detected.get_mut().unwrap() = true; + } + + fn clear_low_voltage_in_cycle(&mut self) { + *self.low_voltage_detected.get_mut().unwrap() = false; + } + + fn light(&mut self, enable: bool) -> Result<()> { + self.light.set_state(enable.into())?; + Ok(()) + } + + fn pump(&self, plant: usize, enable: bool) -> Result<()> { + let index = plant * PINS_PER_PLANT + PLANT_PUMP_OFFSET; + //currently infailable error, keep for future as result anyway + self.shift_register.decompose()[index] + .set_state(enable.into()) + .unwrap(); + Ok(()) + } + + fn last_pump_time(&self, plant: usize) -> Result> { + let ts = unsafe { LAST_WATERING_TIMESTAMP }[plant]; + let timestamp = NaiveDateTime::from_timestamp_millis(ts) + .ok_or(anyhow!("could not convert timestamp"))?; + Ok(DateTime::::from_naive_utc_and_offset(timestamp, Utc)) + } + + fn store_last_pump_time(&mut self, plant: usize, time: chrono::DateTime) { + self.last_watering_timestamp.get_mut().unwrap()[plant] = time.timestamp_millis(); + } + + fn store_consecutive_pump_count(&mut self, plant: usize, count: u32) { + self.consecutive_watering_plant.get_mut().unwrap()[plant] = count; + } + + fn consecutive_pump_count(&mut self, plant: usize) -> u32 { + return self.consecutive_watering_plant.get_mut().unwrap()[plant]; + } + + fn fault(&self, plant: usize, enable: bool) { + let index = plant * PINS_PER_PLANT + PLANT_FAULT_OFFSET; + self.shift_register.decompose()[index] + .set_state(enable.into()) + .unwrap() + } + + fn low_voltage_in_cycle(&mut self) -> bool { + return *self.low_voltage_detected.get_mut().unwrap(); + } + + fn any_pump(&mut self, enable: bool) -> Result<()> { + { + self.main_pump.set_state(enable.into()).unwrap(); + Ok(()) + } + } + + fn time(&mut self) -> Result> { + let time = EspSystemTime {}.now().as_millis(); + let smaller_time = time as i64; + let local_time = NaiveDateTime::from_timestamp_millis(smaller_time) + .ok_or(anyhow!("could not convert timestamp"))?; + Ok(local_time.and_utc()) + } + + fn sntp(&mut self, max_wait_ms: u32) -> Result> { + let sntp = sntp::EspSntp::new_default()?; + let mut counter = 0; + while sntp.get_sync_status() != SyncStatus::Completed { + let delay = Delay::new_default(); + delay.delay_ms(100); + counter += 100; + if counter > max_wait_ms { + bail!("Reached sntp timeout, aborting") + } + } + + self.time() + } + + fn measure_moisture_hz(&self, plant: usize, sensor: Sensor) -> Result { + self.signal_counter.counter_pause()?; + self.signal_counter.counter_clear()?; + // + let offset = match sensor { + Sensor::A => PLANT_MOIST_A_OFFSET, + Sensor::B => PLANT_MOIST_B_OFFSET, + Sensor::PUMP => PLANT_MOIST_PUMP_OFFSET, + }; + let index = plant * PINS_PER_PLANT + offset; + + let delay = Delay::new_default(); + let measurement = 100; + let factor = 1000 / 100; + + self.shift_register.decompose()[index].set_high().unwrap(); + //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()[index].set_low().unwrap(); + let unscaled = self.signal_counter.get_counter_value()? as i32; + let hz = unscaled * factor; + println!("Measuring {:?} @ {} with {}", sensor, plant, hz); + Ok(hz) + } + + fn general_fault(&mut self, enable: bool) { + self.general_fault.set_state(enable.into()).unwrap(); + } + + fn wifi_ap(&mut self) -> Result<()> { + let apconfig = AccessPointConfiguration { + ssid: "PlantCtrl".into(), + auth_method: AuthMethod::None, + ssid_hidden: false, + ..Default::default() + }; + let clientconfig = ClientConfiguration::default(); + self.wifi_driver + .set_configuration(&Configuration::Mixed(clientconfig, apconfig))?; + self.wifi_driver.start()?; + Ok(()) + } + + fn wifi(&mut self, ssid: &str, password: Option<&str>, max_wait: u32) -> Result<()> { + match password { + Some(pw) => { + //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.into(), + password: pw.into(), + ..Default::default() + }, + ))?; + } + None => { + self.wifi_driver + .set_configuration(&Configuration::Client(ClientConfiguration { + ssid: ssid.into(), + auth_method: AuthMethod::None, + ..Default::default() + })) + .unwrap(); + } + } + + self.wifi_driver.start()?; + self.wifi_driver.connect()?; + + let delay = Delay::new_default(); + let mut counter = 0_u32; + while !self.wifi_driver.is_connected()? { + println!("Waiting for station connection"); + //TODO blink status? + delay.delay_ms(250); + counter += 250; + if counter > max_wait { + //ignore these errors, wifi 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"); + } + } + println!("Should be connected now"); + + while !self.wifi_driver.is_up().unwrap() { + println!("Waiting for network being up"); + delay.delay_ms(250); + counter += 250; + if counter > max_wait { + //ignore these errors, wifi 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"); + } + } + //update freertos registers ;) + let address = self.wifi_driver.sta_netif().get_ip_info().unwrap(); + println!("IP info: {:?}", address); + Ok(()) + } + + fn mount_file_system(&mut self) -> Result<()> { + let base_path = CString::new("/spiffs")?; + let storage = CString::new(SPIFFS_PARTITION_NAME)?; + let conf = esp_idf_sys::esp_vfs_spiffs_conf_t { + base_path: base_path.as_ptr(), + partition_label: storage.as_ptr(), + max_files: 2, + format_if_mount_failed: true, + }; + + unsafe { + esp_idf_sys::esp!(esp_idf_sys::esp_vfs_spiffs_register(&conf))?; + Ok(()) + } + } + + fn file_system_size(&mut self) -> Result { + let storage = CString::new(SPIFFS_PARTITION_NAME)?; + let mut total_size = 0; + let mut used_size = 0; + unsafe { + esp_idf_sys::esp!(esp_idf_sys::esp_spiffs_info( + storage.as_ptr(), + &mut total_size, + &mut used_size + ))?; + } + Ok(FileSystemSizeInfo { + total_size, + used_size, + free_size: total_size - used_size, + }) + } + + fn is_config_reset(&mut self) -> bool { + self.boot_button.get_level() == Level::Low + } + + fn remove_configs(&mut self) -> Result { + let config = Path::new(CONFIG_FILE); + if config.exists() { + println!("Removing config"); + std::fs::remove_file(config)?; + return Ok(ClearConfigType::Config); + } + + let wifi_config = Path::new(WIFI_CONFIG_FILE); + if wifi_config.exists() { + println!("Removing wifi config"); + std::fs::remove_file(wifi_config)?; + return Ok(ClearConfigType::WifiConfig); + } + + Ok(ClearConfigType::None) + } + + fn get_wifi(&mut self) -> Result { + let cfg = File::open(WIFI_CONFIG_FILE)?; + let config: WifiConfig = serde_json::from_reader(cfg)?; + Ok(config) + } + + fn set_wifi(&mut self, wifi: &WifiConfig) -> Result<()> { + let mut cfg = File::create(WIFI_CONFIG_FILE)?; + serde_json::to_writer(&mut cfg, &wifi)?; + println!("Wrote wifi config {}", wifi); + Ok(()) + } + + fn get_config(&mut self) -> Result { + let cfg = File::open(CONFIG_FILE)?; + let config: Config = serde_json::from_reader(cfg)?; + Ok(config) + } + + fn set_config(&mut self, config: &Config) -> Result<()> { + let mut cfg = File::create(CONFIG_FILE)?; + serde_json::to_writer(&mut cfg, &config)?; + println!("Wrote config config {:?}", config); + Ok(()) + } + + fn wifi_scan(&mut self) -> Result> { + //remove this parts + for i in 1..11 { + println!("Scanning channel {}", i); + self.wifi_driver.start_scan( + &ScanConfig { + scan_type: ScanType::Passive(Duration::from_secs(1)), + show_hidden: false, + channel: Some(i), + ..Default::default() + }, + true, + )?; + let sr = self.wifi_driver.get_scan_result()?; + for r in sr.iter() { + println!("Found wifi {}", r.ssid); + } + } + + self.wifi_driver.start_scan( + &ScanConfig { + scan_type: ScanType::Passive(Duration::from_secs(1)), + show_hidden: false, + ..Default::default() + }, + true, + )?; + Ok(self.wifi_driver.get_scan_result()?) + } + + fn test(&mut self) -> Result<()> { + self.general_fault(true); + unsafe { vTaskDelay(100) }; + self.general_fault(false); + unsafe { vTaskDelay(100) }; + self.any_pump(true)?; + unsafe { vTaskDelay(500) }; + self.any_pump(false)?; + unsafe { vTaskDelay(500) }; + self.light(true)?; + unsafe { vTaskDelay(500) }; + self.light(false)?; + unsafe { vTaskDelay(500) }; + for i in 0..8 { + self.fault(i, true); + unsafe { vTaskDelay(500) }; + self.fault(i, false); + unsafe { vTaskDelay(500) }; + } + for i in 0..8 { + self.pump(i, true)?; + unsafe { vTaskDelay(500) }; + self.pump(i, false)?; + unsafe { vTaskDelay(500) }; + } + for i in 0..8 { + self.measure_moisture_hz(i, Sensor::A)?; + unsafe { vTaskDelay(500) }; + } + for i in 0..8 { + self.measure_moisture_hz(i, Sensor::B)?; + unsafe { vTaskDelay(500) }; + } + for i in 0..8 { + self.measure_moisture_hz(i, Sensor::PUMP)?; + unsafe { vTaskDelay(500) }; + } + + Ok(()) + } + + fn is_wifi_config_file_existant(&mut self) -> bool { + let config = Path::new(CONFIG_FILE); + config.exists() + } + + fn mqtt(&mut self, config: &Config) -> Result<()> { + //FIXME testament + let mqtt_client_config = MqttClientConfiguration { + //room for improvement + ..Default::default() + }; + + let round_trip_ok = Arc::new(AtomicBool::new(false)); + let round_trip_topic = format!("{}/internal/roundtrip", config.base_topic); + let stay_alive_topic = format!("{}/stay_alive", config.base_topic); + println!("Round trip topic is {}", round_trip_topic); + println!("Stay alive topic is {}", stay_alive_topic); + + let stay_alive_topic_copy = stay_alive_topic.clone(); + let round_trip_topic_copy = round_trip_topic.clone(); + let round_trip_ok_copy = round_trip_ok.clone(); + let mut client = + EspMqttClient::new(&config.mqtt_url, &mqtt_client_config, move |handler| { + match handler { + Err(err) => println!("Ignoring damaged message {}", err), + core::result::Result::Ok(event) => { + match event { + embedded_svc::mqtt::client::Event::Received(msg) => { + let data = String::from_utf8_lossy(msg.data()); + if let Some(topic) = msg.topic() { + //todo use enums + if topic.eq(round_trip_topic_copy.as_str()) { + round_trip_ok_copy + .store(true, std::sync::atomic::Ordering::Relaxed); + } else if topic.eq(stay_alive_topic_copy.as_str()) { + let value = data.eq_ignore_ascii_case("true") + || data.eq_ignore_ascii_case("1"); + println!("Received stay alive with value {}", value); + STAY_ALIVE + .store(value, std::sync::atomic::Ordering::Relaxed); + } else { + println!("Unknown topic recieved {}", topic); + } + } + } + _ => {} + } + } + } + })?; + //subscribe to roundtrip + + client.subscribe(round_trip_topic.as_str(), ExactlyOnce)?; + client.subscribe(stay_alive_topic.as_str(), ExactlyOnce)?; + //publish to roundtrip + client.publish( + round_trip_topic.as_str(), + ExactlyOnce, + false, + "online_test".as_bytes(), + )?; + + let wait_for_roundtrip = 0; + while wait_for_roundtrip < 100 { + match round_trip_ok.load(std::sync::atomic::Ordering::Relaxed) { + true => { + println!("Round trip registered, proceeding"); + self.mqtt_client = Some(client); + return Ok(()); + } + false => { + unsafe { vTaskDelay(10) }; + } + } + } + bail!("Mqtt did not complete roundtrip in time"); + } +} + impl CreatePlantHal<'_> for PlantHal { fn create() -> Result>> { let peripherals = Peripherals::take()?; - let mut clock = PinDriver::output(peripherals.pins.gpio21)?; - let mut latch = PinDriver::output(peripherals.pins.gpio22)?; - let mut data = PinDriver::output(peripherals.pins.gpio19)?; + let clock = PinDriver::output(peripherals.pins.gpio21)?; + let latch = PinDriver::output(peripherals.pins.gpio22)?; + let data = PinDriver::output(peripherals.pins.gpio19)?; let one_wire_pin = PinDriver::input_output_od(peripherals.pins.gpio4)?; //TODO make to none if not possible to init @@ -222,7 +739,8 @@ impl CreatePlantHal<'_> for PlantHal { let last_watering_timestamp = Mutex::new(unsafe { LAST_WATERING_TIMESTAMP }); let consecutive_watering_plant = Mutex::new(unsafe { CONSECUTIVE_WATERING_PLANT }); let low_voltage_detected = Mutex::new(unsafe { LOW_VOLTAGE_DETECTED }); - let tank_driver = AdcDriver::new(peripherals.adc1, &esp_idf_hal::adc::config::Config::new())?; + let tank_driver = + AdcDriver::new(peripherals.adc1, &esp_idf_hal::adc::config::Config::new())?; let tank_channel: AdcChannelDriver<'_, { attenuation::DB_11 }, Gpio39> = AdcChannelDriver::new(peripherals.pins.gpio39)?; let solar_is_day = PinDriver::input(peripherals.pins.gpio25)?; @@ -236,421 +754,24 @@ impl CreatePlantHal<'_> for PlantHal { println!("After stuff"); - let rv = Mutex::new(PlantCtrlBoard { - shift_register : shift_register, - last_watering_timestamp: last_watering_timestamp, - consecutive_watering_plant: consecutive_watering_plant, - low_voltage_detected: low_voltage_detected, - tank_driver: tank_driver, - tank_channel: tank_channel, - solar_is_day: solar_is_day, - boot_button: boot_button, - light: light, - main_pump: main_pump, - tank_power: tank_power, - general_fault: general_fault, - one_wire_bus: one_wire_bus, + shift_register, + last_watering_timestamp, + consecutive_watering_plant, + low_voltage_detected, + tank_driver, + tank_channel, + solar_is_day, + boot_button, + light, + main_pump, + tank_power, + general_fault, + one_wire_bus, signal_counter: counter_unit1, - wifi_driver: wifi_driver, + wifi_driver, + mqtt_client: None, }); - return Ok(rv); - } -} - -pub struct PlantCtrlBoard<'a> { - shift_register: ShiftRegister40, PinDriver<'a, esp_idf_hal::gpio::Gpio22, esp_idf_hal::gpio::Output>, PinDriver<'a, esp_idf_hal::gpio::Gpio19, esp_idf_hal::gpio::Output>>, - consecutive_watering_plant: Mutex<[u32; PLANT_COUNT]>, - last_watering_timestamp: Mutex<[i64; PLANT_COUNT]>, - low_voltage_detected: Mutex, - tank_driver: AdcDriver<'a, esp_idf_hal::adc::ADC1>, - tank_channel: esp_idf_hal::adc::AdcChannelDriver<'a, { attenuation::DB_11 }, Gpio39>, - solar_is_day: PinDriver<'a, esp_idf_hal::gpio::Gpio25, esp_idf_hal::gpio::Input>, - boot_button: PinDriver<'a, esp_idf_hal::gpio::Gpio0, esp_idf_hal::gpio::Input>, - signal_counter: PcntDriver<'a>, - light: PinDriver<'a, esp_idf_hal::gpio::Gpio26, esp_idf_hal::gpio::Output>, - main_pump: PinDriver<'a, esp_idf_hal::gpio::Gpio23, esp_idf_hal::gpio::Output>, - tank_power: PinDriver<'a, esp_idf_hal::gpio::Gpio27, esp_idf_hal::gpio::Output>, - general_fault: PinDriver<'a, esp_idf_hal::gpio::Gpio13, esp_idf_hal::gpio::Output>, - pub wifi_driver: EspWifi<'a>, - one_wire_bus: OneWire>, -} - -impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> { - fn battery_state(&mut self) -> Result { - todo!() - } - - fn is_day(&self) -> bool { - return self.solar_is_day.get_level().into(); - } - - fn water_temperature_c(&mut self) -> Result { - let mut delay = Delay::new_default(); - - self.one_wire_bus - .reset(&mut delay) - .map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?; - let first = self.one_wire_bus.devices(false, &mut delay).next(); - if first.is_none() { - bail!("Not found any one wire Ds18b20"); - } - let device_address = first - .unwrap() - .map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?; - - let water_temp_sensor = Ds18b20::new::(device_address) - .map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?; - - water_temp_sensor - .start_temp_measurement(&mut self.one_wire_bus, &mut delay) - .map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?; - ds18b20::Resolution::Bits12.delay_for_measurement_time(&mut delay); - let sensor_data = water_temp_sensor - .read_data(&mut self.one_wire_bus, &mut delay) - .map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?; - if sensor_data.temperature == 85_f32 { - bail!("Ds18b20 dummy temperature returned"); - } - return Ok(sensor_data.temperature); - } - - fn tank_sensor_mv(&mut self) -> Result { - let delay = Delay::new_default(); - self.tank_power.set_high()?; - //let stabilize - delay.delay_ms(100); - let value = self.tank_driver.read(&mut self.tank_channel)?; - self.tank_power.set_low()?; - return Ok(value); - } - - fn set_low_voltage_in_cycle(&mut self) { - *self.low_voltage_detected.get_mut().unwrap() = true; - } - - fn clear_low_voltage_in_cycle(&mut self) { - *self.low_voltage_detected.get_mut().unwrap() = false; - } - - fn light(&mut self, enable: bool) -> Result<()> { - self.light.set_state(enable.into())?; - Ok(()) - } - - fn pump(&self, plant: usize, enable: bool) -> Result<()> { - let index = plant * PINS_PER_PLANT + PLANT_PUMP_OFFSET; - //currently infailable error, keep for future as result anyway - self.shift_register.decompose()[index].set_state(enable.into()).unwrap(); - Ok(()) - } - - fn last_pump_time(&self, plant: usize) -> Result> { - let ts = unsafe { LAST_WATERING_TIMESTAMP }[plant]; - let timestamp = NaiveDateTime::from_timestamp_millis(ts) - .ok_or(anyhow!("could not convert timestamp"))?; - return Ok(DateTime::::from_naive_utc_and_offset(timestamp, Utc)); - } - - fn store_last_pump_time(&mut self, plant: usize, time: chrono::DateTime) { - self.last_watering_timestamp.get_mut().unwrap()[plant] = time.timestamp_millis(); - } - - fn store_consecutive_pump_count(&mut self, plant: usize, count: u32) { - self.consecutive_watering_plant.get_mut().unwrap()[plant] = count; - } - - fn consecutive_pump_count(&mut self, plant: usize) -> u32 { - return self.consecutive_watering_plant.get_mut().unwrap()[plant]; - } - - fn fault(&self, plant: usize, enable: bool) { - let index = plant * PINS_PER_PLANT + PLANT_FAULT_OFFSET; - self.shift_register.decompose()[index].set_state(enable.into()).unwrap() - } - - fn low_voltage_in_cycle(&mut self) -> bool { - return *self.low_voltage_detected.get_mut().unwrap(); - } - - fn any_pump(&mut self, enable: bool) -> Result<()> { - return Ok(self.main_pump.set_state(enable.into()).unwrap()); - } - - fn time(&mut self) -> Result> { - let time = EspSystemTime {}.now().as_millis(); - let smaller_time = time as i64; - let local_time = NaiveDateTime::from_timestamp_millis(smaller_time) - .ok_or(anyhow!("could not convert timestamp"))?; - return Ok(local_time.and_utc()); - } - - fn sntp(&mut self, max_wait_ms: u32) -> Result> { - let sntp = sntp::EspSntp::new_default()?; - let mut counter = 0; - while sntp.get_sync_status() != SyncStatus::Completed { - let delay = Delay::new_default(); - delay.delay_ms(100); - counter += 100; - if counter > max_wait_ms { - bail!("Reached sntp timeout, aborting") - } - } - - return self.time(); - } - - fn measure_moisture_hz(&self, plant: usize, sensor: Sensor) -> Result { - self.signal_counter.counter_pause()?; - self.signal_counter.counter_clear()?; - // - let offset = match sensor { - Sensor::A => PLANT_MOIST_A_OFFSET, - Sensor::B => PLANT_MOIST_B_OFFSET, - Sensor::PUMP => PLANT_MOIST_PUMP_OFFSET, - }; - let index = plant * PINS_PER_PLANT + offset; - - let delay = Delay::new_default(); - let measurement = 100; - let factor = 1000 / 100; - - self.shift_register.decompose()[index].set_high().unwrap(); - //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()[index].set_low().unwrap(); - let unscaled = self.signal_counter.get_counter_value()? as i32; - let hz = unscaled * factor; - println!("Measuring {:?} @ {} with {}", sensor, plant, hz); - return Ok(hz); - } - - fn general_fault(&mut self, enable: bool) { - self.general_fault.set_state(enable.into()).unwrap(); - } - - fn wifi_ap(&mut self) -> Result<()> { - - let apconfig = AccessPointConfiguration { - ssid: "PlantCtrl".into(), - auth_method: AuthMethod::None, - ssid_hidden: false, - ..Default::default() - }; - let clientconfig = ClientConfiguration::default(); - self.wifi_driver.set_configuration(&Configuration::Mixed(clientconfig, apconfig))?; - self.wifi_driver.start()?; - Ok(()) - } - - fn wifi(&mut self, ssid: &str, password: Option<&str>, max_wait: u32) -> Result<()> { - match password { - Some(pw) => { - //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.into(), - password: pw.into(), - ..Default::default() - }, - ))?; - } - None => { - self.wifi_driver - .set_configuration(&Configuration::Client(ClientConfiguration { - ssid: ssid.into(), - auth_method: AuthMethod::None, - ..Default::default() - })) - .unwrap(); - } - } - - self.wifi_driver.start()?; - self.wifi_driver.connect()?; - - let delay = Delay::new_default(); - let mut counter = 0_u32; - while !self.wifi_driver.is_connected()? { - println!("Waiting for station connection"); - //TODO blink status? - delay.delay_ms(250); - counter += 250; - if counter > max_wait { - //ignore these errors, wifi 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"); - } - } - println!("Should be connected now"); - - while self.wifi_driver.is_up().unwrap() == false { - println!("Waiting for network being up"); - delay.delay_ms(250); - counter += 250; - if counter > max_wait { - //ignore these errors, wifi 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"); - } - } - //update freertos registers ;) - let address = self.wifi_driver.sta_netif().get_ip_info().unwrap(); - println!("IP info: {:?}", address); - return Ok(()); - } - - fn mountFileSystem(&mut self) -> Result<()> { - let base_path = CString::new("/spiffs")?; - let storage = CString::new(SPIFFS_PARTITION_NAME)?; - let conf = esp_idf_sys::esp_vfs_spiffs_conf_t { - base_path: base_path.as_ptr(), - partition_label: storage.as_ptr(), - max_files: 2, - format_if_mount_failed: true, - }; - - unsafe { - esp_idf_sys::esp!(esp_idf_sys::esp_vfs_spiffs_register(&conf))?; - Ok(()) - } - } - - fn fileSystemSize(&mut self) -> Result { - let storage = CString::new(SPIFFS_PARTITION_NAME)?; - let mut total_size = 0; - let mut used_size = 0; - unsafe { - esp_idf_sys::esp!(esp_idf_sys::esp_spiffs_info( - storage.as_ptr(), - &mut total_size, - &mut used_size - ))?; - } - return Ok(FileSystemSizeInfo { - total_size, - used_size, - free_size: total_size - used_size, - }); - } - - fn is_config_reset(&mut self) -> bool { - return self.boot_button.get_level() == Level::Low; - } - - - - fn remove_configs(&mut self) -> Result { - let config = Path::new(CONFIG_FILE); - if config.exists() { - println!("Removing config"); - std::fs::remove_file(config)?; - return Ok(ClearConfigType::Config); - } - - let wifi_config = Path::new(WIFI_CONFIG_FILE); - if wifi_config.exists() { - println!("Removing wifi config"); - std::fs::remove_file(wifi_config)?; - return Ok(ClearConfigType::WifiConfig); - } - - return Ok((ClearConfigType::None)); - } - - fn get_wifi(&mut self) -> Result { - let cfg = File::open(WIFI_CONFIG_FILE)?; - let config: WifiConfig = serde_json::from_reader(cfg)?; - return Ok(config); - } - - fn set_wifi(&mut self, wifi: &WifiConfig) -> Result<()> { - let mut cfg = File::create(WIFI_CONFIG_FILE)?; - serde_json::to_writer(&mut cfg, &wifi)?; - println!("Wrote wifi config {}", wifi); - return Ok(()); - } - - fn get_config(&mut self) -> Result { - let cfg = File::open(CONFIG_FILE)?; - let config: Config = serde_json::from_reader(cfg)?; - return Ok(config); - } - - fn set_config(&mut self, config: &Config) -> Result<()> { - let mut cfg = File::create(CONFIG_FILE)?; - serde_json::to_writer(&mut cfg, &config)?; - println!("Wrote config config {:?}", config); - return Ok(()); - } - - fn wifi_scan(&mut self) -> Result> { - //remove this parts - for i in 1..11 { - println!("Scanning channel {}", i); - self.wifi_driver.start_scan(&ScanConfig{ - scan_type : ScanType::Passive(Duration::from_secs(1)), - show_hidden: false, - channel: Some(i), - ..Default::default() - }, true)?; - let sr = self.wifi_driver.get_scan_result()?; - for r in sr.iter(){ - println!("Found wifi {}", r.ssid); - } - } - - self.wifi_driver.start_scan(&ScanConfig{ - scan_type : ScanType::Passive(Duration::from_secs(1)), - show_hidden: false, - ..Default::default() - }, true)?; - return Ok(self.wifi_driver.get_scan_result()?); - } - - fn test(&mut self) -> Result<()> { - self.general_fault(true); - unsafe { vTaskDelay(100) }; - self.general_fault(false); - unsafe { vTaskDelay(100) }; - self.any_pump(true)?; - unsafe { vTaskDelay(500) }; - self.any_pump(false)?; - unsafe { vTaskDelay(500) }; - self.light(true)?; - unsafe { vTaskDelay(500) }; - self.light(false)?; - unsafe { vTaskDelay(500) }; - for i in 0 .. 8{ - self.fault(i, true); - unsafe { vTaskDelay(500) }; - self.fault(i, false); - unsafe { vTaskDelay(500) }; - } - for i in 0 .. 8{ - self.pump(i, true)?; - unsafe { vTaskDelay(500) }; - self.pump(i, false)?; - unsafe { vTaskDelay(500) }; - } - for i in 0 .. 8{ - self.measure_moisture_hz(i, Sensor::A)?; - unsafe { vTaskDelay(500) }; - } - for i in 0 .. 8{ - self.measure_moisture_hz(i, Sensor::B)?; - unsafe { vTaskDelay(500) }; - } - for i in 0 .. 8{ - self.measure_moisture_hz(i, Sensor::PUMP)?; - unsafe { vTaskDelay(500) }; - } - - return Ok(()); + Ok(rv) } } diff --git a/rust/src/sipo.rs b/rust/src/sipo.rs index b5fb849..a4686fe 100644 --- a/rust/src/sipo.rs +++ b/rust/src/sipo.rs @@ -10,21 +10,21 @@ trait ShiftRegisterInternal { } /// Output pin of the shift register -pub struct ShiftRegisterPin<'a> -{ +pub struct ShiftRegisterPin<'a> { shift_register: &'a dyn ShiftRegisterInternal, index: usize, } -impl<'a> ShiftRegisterPin<'a> -{ +impl<'a> ShiftRegisterPin<'a> { fn new(shift_register: &'a dyn ShiftRegisterInternal, index: usize) -> Self { - ShiftRegisterPin { shift_register, index } + ShiftRegisterPin { + shift_register, + index, + } } } -impl OutputPin for ShiftRegisterPin<'_> -{ +impl OutputPin for ShiftRegisterPin<'_> { type Error = (); fn set_low(&mut self) -> Result<(), Self::Error> { @@ -42,9 +42,10 @@ macro_rules! ShiftRegisterBuilder { ($name: ident, $size: expr) => { /// Serial-in parallel-out shift register pub struct $name - where Pin1: OutputPin, - Pin2: OutputPin, - Pin3: OutputPin + where + Pin1: OutputPin, + Pin2: OutputPin, + Pin3: OutputPin, { clock: RefCell, latch: RefCell, @@ -53,12 +54,13 @@ macro_rules! ShiftRegisterBuilder { } impl ShiftRegisterInternal for $name - where Pin1: OutputPin, - Pin2: OutputPin, - Pin3: OutputPin + where + Pin1: OutputPin, + Pin2: OutputPin, + Pin3: OutputPin, { /// Sets the value of the shift register output at `index` to value `command` - fn update(&self, index: usize, command: bool) -> Result<(), ()>{ + fn update(&self, index: usize, command: bool) -> Result<(), ()> { self.output_state.borrow_mut()[index] = command; let output_state = self.output_state.borrow(); self.latch.borrow_mut().set_low().map_err(|_e| ())?; @@ -78,11 +80,11 @@ macro_rules! ShiftRegisterBuilder { } } - impl $name - where Pin1: OutputPin, - Pin2: OutputPin, - Pin3: OutputPin + where + Pin1: OutputPin, + Pin2: OutputPin, + Pin3: OutputPin, { /// Creates a new SIPO shift register from clock, latch, and data output pins pub fn new(clock: Pin1, latch: Pin2, data: Pin3) -> Self { @@ -95,14 +97,12 @@ macro_rules! ShiftRegisterBuilder { } /// Get embedded-hal output pins to control the shift register outputs - pub fn decompose(&self) -> [ShiftRegisterPin; $size] { - + pub fn decompose(&self) -> [ShiftRegisterPin; $size] { // Create an uninitialized array of `MaybeUninit`. The `assume_init` is // safe because the type we are claiming to have initialized here is a // bunch of `MaybeUninit`s, which do not require initialization. - let mut pins: [MaybeUninit; $size] = unsafe { - MaybeUninit::uninit().assume_init() - }; + let mut pins: [MaybeUninit; $size] = + unsafe { MaybeUninit::uninit().assume_init() }; // Dropping a `MaybeUninit` does nothing, so if there is a panic during this loop, // we have a memory leak, but there is no memory safety issue. @@ -117,12 +117,16 @@ macro_rules! ShiftRegisterBuilder { /// Consume the shift register and return the original clock, latch, and data output pins pub fn release(self) -> (Pin1, Pin2, Pin3) { - let Self{clock, latch, data, output_state: _} = self; + let Self { + clock, + latch, + data, + output_state: _, + } = self; (clock.into_inner(), latch.into_inner(), data.into_inner()) } } - - } + }; } ShiftRegisterBuilder!(ShiftRegister8, 8); @@ -143,4 +147,4 @@ ShiftRegisterBuilder!(ShiftRegister120, 120); ShiftRegisterBuilder!(ShiftRegister128, 128); /// 8 output serial-in parallel-out shift register -pub type ShiftRegister = ShiftRegister8; \ No newline at end of file +pub type ShiftRegister = ShiftRegister8; diff --git a/rust/src/webserver/config.html b/rust/src/webserver/config.html index b360708..fd6af1d 100644 --- a/rust/src/webserver/config.html +++ b/rust/src/webserver/config.html @@ -11,23 +11,45 @@

config

- -
+

Mqtt:

+
+ + MQTT Url +
+
+ + Base Topic +
+ +

Tank:

Enable Tank Sensor
-
- + + Allow Pumping if Sensor Error +
+ + +
+ Tank Size mL
Tank Warn below mL
+
+ + Tank Empty Voltage (mv) +
+
+ + Tank Full Voltage (mv) +

Light:

@@ -44,9 +66,17 @@

Plants:

+ + +
+ + Max consecutive pump count: +
+
+

diff --git a/rust/src/webserver/webserver.rs b/rust/src/webserver/webserver.rs index 93fbe57..0f9f553 100644 --- a/rust/src/webserver/webserver.rs +++ b/rust/src/webserver/webserver.rs @@ -1,192 +1,222 @@ //offer ota and config mode -use std::{sync::{Mutex, Arc, atomic::AtomicBool}, str::from_utf8}; +use std::{ + str::from_utf8, + sync::{atomic::AtomicBool, Arc}, +}; -use embedded_svc::http::{Method, Headers}; -use esp_idf_svc::http::server::EspHttpServer; +use crate::BOARD_ACCESS; +use embedded_svc::http::Method; +use esp_idf_svc::http::server::{Configuration, EspHttpServer}; use esp_ota::OtaUpdate; use heapless::String; use serde::Serialize; -use crate::BOARD_ACCESS; -use crate::{plant_hal::{PlantCtrlBoard, PlantCtrlBoardInteraction, PLANT_COUNT}, config::{WifiConfig, Config, Plant}}; +use crate::{ + config::{Config, WifiConfig}, + plant_hal::PlantCtrlBoardInteraction, +}; -#[derive(Serialize)] -#[derive(Debug)] +#[derive(Serialize, Debug)] struct SSIDList<'a> { - ssids: Vec<&'a String<32>> + ssids: Vec<&'a String<32>>, } pub fn httpd_initial(reboot_now: Arc) -> Box> { let mut server = shared(); - server.fn_handler("/",Method::Get, move |request| { - let mut response = request.into_ok_response()?; - response.write(include_bytes!("initial_config.html"))?; - return Ok(()) - }).unwrap(); + server + .fn_handler("/", Method::Get, move |request| { + let mut response = request.into_ok_response()?; + response.write(include_bytes!("initial_config.html"))?; + Ok(()) + }) + .unwrap(); - server.fn_handler("/wifiscan",Method::Post, move |request| { - let mut response = request.into_ok_response()?; - let mut board = BOARD_ACCESS.lock().unwrap(); - match board.wifi_scan() { - Err(error) => { - response.write(format!("Error scanning wifi: {}", error).as_bytes())?; - }, - Ok(scan_result) => { - let mut ssids: Vec<&String<32>> = Vec::new(); - scan_result.iter().for_each(|s| - ssids.push(&s.ssid) - ); - let ssid_json = serde_json::to_string( &SSIDList{ssids})?; - println!("Sending ssid list {}", &ssid_json); - response.write( &ssid_json.as_bytes())?; - }, - } - return Ok(()) - }).unwrap(); + server + .fn_handler("/wifiscan", Method::Post, move |request| { + let mut response = request.into_ok_response()?; + let mut board = BOARD_ACCESS.lock().unwrap(); + match board.wifi_scan() { + Err(error) => { + response.write(format!("Error scanning wifi: {}", error).as_bytes())?; + } + Ok(scan_result) => { + let mut ssids: Vec<&String<32>> = Vec::new(); + scan_result.iter().for_each(|s| ssids.push(&s.ssid)); + let ssid_json = serde_json::to_string(&SSIDList { ssids })?; + println!("Sending ssid list {}", &ssid_json); + response.write(ssid_json.as_bytes())?; + } + } + Ok(()) + }) + .unwrap(); - - server.fn_handler("/wifisave",Method::Post, move |mut request| { + server + .fn_handler("/wifisave", Method::Post, move |mut request| { + let mut buf = [0_u8; 2048]; + let read = request.read(&mut buf); + if read.is_err() { + let error_text = read.unwrap_err().to_string(); + println!("Could not parse wificonfig {}", error_text); + request + .into_status_response(500)? + .write(error_text.as_bytes())?; + return Ok(()); + } + let actual_data = &buf[0..read.unwrap()]; + println!("raw {:?}", actual_data); + println!("Raw data {}", from_utf8(actual_data).unwrap()); + let wifi_config: Result = + serde_json::from_slice(actual_data); + if wifi_config.is_err() { + let error_text = wifi_config.unwrap_err().to_string(); + println!("Could not parse wificonfig {}", error_text); + request + .into_status_response(500)? + .write(error_text.as_bytes())?; + return Ok(()); + } + let mut board = BOARD_ACCESS.lock().unwrap(); + board.set_wifi(&wifi_config.unwrap())?; + let mut response = request.into_status_response(202)?; + response.write("saved".as_bytes())?; + reboot_now.store(true, std::sync::atomic::Ordering::Relaxed); + Ok(()) + }) + .unwrap(); - let mut buf = [0_u8;2048]; - let read = request.read(&mut buf); - if read.is_err(){ - let error_text = read.unwrap_err().to_string(); - println!("Could not parse wificonfig {}", error_text); - request.into_status_response(500)?.write(error_text.as_bytes())?; - return Ok(()); - } - let actual_data = &buf[0..read.unwrap()]; - println!("raw {:?}", actual_data); - println!("Raw data {}", from_utf8(actual_data).unwrap()); - let wifi_config: Result = serde_json::from_slice(actual_data); - if wifi_config.is_err(){ - let error_text = wifi_config.unwrap_err().to_string(); - println!("Could not parse wificonfig {}", error_text); - request.into_status_response(500)?.write(error_text.as_bytes())?; - return Ok(()); - } - let mut board = BOARD_ACCESS.lock().unwrap(); - board.set_wifi(&wifi_config.unwrap())?; - let mut response = request.into_status_response(202)?; - response.write("saved".as_bytes())?; - reboot_now.store(true, std::sync::atomic::Ordering::Relaxed); - return Ok(()) - }).unwrap(); + server + .fn_handler("/boardtest", Method::Post, move |_| { + let mut board = BOARD_ACCESS.lock().unwrap(); + board.test()?; + Ok(()) + }) + .unwrap(); - server.fn_handler("/boardtest",Method::Post, move |request| { - let mut board = BOARD_ACCESS.lock().unwrap(); - board.test(); - return Ok(()) - }).unwrap(); - - return server + server } pub fn httpd(reboot_now: Arc) -> Box> { let mut server = shared(); server - .fn_handler("/",Method::Get, move |request| { - let mut response = request.into_ok_response()?; - response.write(include_bytes!("config.html"))?; - return Ok(()) - }).unwrap(); + .fn_handler("/", Method::Get, move |request| { + let mut response = request.into_ok_response()?; + response.write(include_bytes!("config.html"))?; + Ok(()) + }) + .unwrap(); server - .fn_handler("/get_config",Method::Get, move |request| { - let mut response = request.into_ok_response()?; - let mut board = BOARD_ACCESS.lock()?; - match board.get_config() { - Ok(config) => { - let config_json = serde_json::to_string(&config)?; - response.write(config_json.as_bytes())?; - }, - Err(_) => { - let config_json = serde_json::to_string(&Config::default())?; - response.write(config_json.as_bytes())?; - }, - } - return Ok(()) - }).unwrap(); - - server.fn_handler("/set_config",Method::Post, move |mut request| { - let mut buf = [0_u8;2048]; - let read = request.read(&mut buf); - if read.is_err(){ - let error_text = read.unwrap_err().to_string(); - println!("Could not parse wificonfig {}", error_text); - request.into_status_response(500)?.write(error_text.as_bytes())?; - return Ok(()); - } - let actual_data = &buf[0..read.unwrap()]; - println!("raw {:?}", actual_data); - println!("Raw data {}", from_utf8(actual_data).unwrap()); - let config: Result = serde_json::from_slice(actual_data); - if config.is_err(){ - let error_text = config.unwrap_err().to_string(); - println!("Could not parse wificonfig {}", error_text); - request.into_status_response(500)?.write(error_text.as_bytes())?; - return Ok(()); - } - let mut board = BOARD_ACCESS.lock().unwrap(); - board.set_config(&config.unwrap())?; - let mut response = request.into_status_response(202)?; - response.write("saved".as_bytes())?; - reboot_now.store(true, std::sync::atomic::Ordering::Relaxed); - return Ok(()) - }).unwrap(); - return server; + .fn_handler("/get_config", Method::Get, move |request| { + let mut response = request.into_ok_response()?; + let mut board = BOARD_ACCESS.lock()?; + match board.get_config() { + Ok(config) => { + let config_json = serde_json::to_string(&config)?; + response.write(config_json.as_bytes())?; + } + Err(_) => { + let config_json = serde_json::to_string(&Config::default())?; + response.write(config_json.as_bytes())?; + } + } + Ok(()) + }) + .unwrap(); + server + .fn_handler("/set_config", Method::Post, move |mut request| { + let mut buf = [0_u8; 3072]; + let read = request.read(&mut buf); + if read.is_err() { + let error_text = read.unwrap_err().to_string(); + println!("Could not parse config {}", error_text); + request + .into_status_response(500)? + .write(error_text.as_bytes())?; + return Ok(()); + } + let actual_data = &buf[0..read.unwrap()]; + println!("Raw data {}", from_utf8(actual_data).unwrap()); + let config: Result = serde_json::from_slice(actual_data); + if config.is_err() { + let error_text = config.unwrap_err().to_string(); + println!("Could not parse config {}", error_text); + request + .into_status_response(500)? + .write(error_text.as_bytes())?; + return Ok(()); + } + let mut board = BOARD_ACCESS.lock().unwrap(); + board.set_config(&config.unwrap())?; + let mut response = request.into_status_response(202)?; + response.write("saved".as_bytes())?; + reboot_now.store(true, std::sync::atomic::Ordering::Relaxed); + Ok(()) + }) + .unwrap(); + server } pub fn shared() -> Box> { - let mut server: Box> = Box::new(EspHttpServer::new(&Default::default()).unwrap()); - + let server_config = Configuration { + stack_size: 8192, + ..Default::default() + }; + let mut server: Box> = + Box::new(EspHttpServer::new(&server_config).unwrap()); server - .fn_handler("/version",Method::Get, |request| { + .fn_handler("/version", Method::Get, |request| { let mut response = request.into_ok_response()?; response.write(env!("VERGEN_GIT_DESCRIBE").as_bytes())?; - return Ok(()) - }).unwrap(); + Ok(()) + }) + .unwrap(); server - .fn_handler("/bundle.js",Method::Get, |request| { + .fn_handler("/bundle.js", Method::Get, |request| { let mut response = request.into_ok_response()?; response.write(include_bytes!("bundle.js"))?; - return Ok(()) - }).unwrap(); + Ok(()) + }) + .unwrap(); server - .fn_handler("/favicon.ico",Method::Get, |request| { - let mut response = request.into_ok_response()?; - response.write(include_bytes!("favicon.ico"))?; - return Ok(()) - }).unwrap(); + .fn_handler("/favicon.ico", Method::Get, |request| { + let mut response = request.into_ok_response()?; + response.write(include_bytes!("favicon.ico"))?; + Ok(()) + }) + .unwrap(); server - .fn_handler("/ota", Method::Post, |mut request| { + .fn_handler("/ota", Method::Post, |mut request| { let ota = OtaUpdate::begin(); - if ota.is_err(){ + if ota.is_err() { let error_text = ota.unwrap_err().to_string(); - request.into_status_response(500)?.write(error_text.as_bytes())?; + request + .into_status_response(500)? + .write(error_text.as_bytes())?; return Ok(()); } let mut ota = ota.unwrap(); println!("start ota"); - + //having a larger buffer is not really faster, requires more stack and prevents the progress bar from working ;) - const BUFFER_SIZE:usize = 512; - let mut buffer :[u8;BUFFER_SIZE] = [0;BUFFER_SIZE]; + const BUFFER_SIZE: usize = 512; + let mut buffer: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; let mut total_read: usize = 0; loop { let read = request.read(&mut buffer).unwrap(); total_read += read; println!("received {read} bytes ota {total_read}"); - let to_write = & buffer[0 .. read]; + let to_write = &buffer[0..read]; - let write_result = ota.write(to_write); - if write_result.is_err(){ + if write_result.is_err() { let error_text = write_result.unwrap_err().to_string(); - request.into_status_response(500)?.write(error_text.as_bytes())?; + request + .into_status_response(500)? + .write(error_text.as_bytes())?; return Ok(()); } println!("wrote {read} bytes ota {total_read}"); @@ -199,16 +229,19 @@ pub fn shared() -> Box> { println!("finalizing and changing boot partition to {partition:?}"); let finalizer = ota.finalize(); - if finalizer.is_err(){ + if finalizer.is_err() { let error_text = finalizer.err().unwrap().to_string(); - request.into_status_response(500)?.write(error_text.as_bytes())?; + request + .into_status_response(500)? + .write(error_text.as_bytes())?; return Ok(()); } let mut finalizer = finalizer.unwrap(); - + println!("changing boot partition"); finalizer.set_as_boot_partition().unwrap(); finalizer.restart(); - }).unwrap(); - return server; + }) + .unwrap(); + server } diff --git a/rust/src_webpack/src/form.ts b/rust/src_webpack/src/form.ts index 15e924d..90ffa4c 100644 --- a/rust/src_webpack/src/form.ts +++ b/rust/src_webpack/src/form.ts @@ -1,11 +1,19 @@ interface PlantConfig { + mqtt_url: string, + base_topic: string, tank_sensor_enabled: boolean, - tank_full_ml: number, + tank_allow_pumping_if_sensor_error: boolean, + tank_useable_ml: number, tank_warn_percent: number, + tank_empty_mv: number, + tank_full_mv: number, night_lamp_hour_start: number, night_lamp_hour_end: number, night_lamp_only_when_dark: boolean, + max_consecutive_pump_count: number, + plants: { + mode: string, target_moisture: number, pump_time_s: number, pump_cooldown_min: number, @@ -28,12 +36,26 @@ let fromWrapper = (() => { } } - let tank_full_ml = document.getElementById("tank_full_ml") as HTMLInputElement; - tank_full_ml.onchange = updateJson + + let mqtt_url = document.getElementById("mqtt_url") as HTMLInputElement; + mqtt_url.onchange = updateJson + let base_topic = document.getElementById("base_topic") as HTMLInputElement; + base_topic.onchange = updateJson + let max_consecutive_pump_count = document.getElementById("max_consecutive_pump_count") as HTMLInputElement; + max_consecutive_pump_count.onchange = updateJson + + let tank_useable_ml = document.getElementById("tank_useable_ml") as HTMLInputElement; + tank_useable_ml.onchange = updateJson + let tank_empty_mv = document.getElementById("tank_empty_mv") as HTMLInputElement; + tank_empty_mv.onchange = updateJson + let tank_full_mv = document.getElementById("tank_full_mv") as HTMLInputElement; + tank_full_mv.onchange = updateJson let tank_warn_percent = document.getElementById("tank_warn_percent") as HTMLInputElement; tank_warn_percent.onchange = updateJson let tank_sensor_enabled = document.getElementById("tank_sensor_enabled") as HTMLInputElement; tank_sensor_enabled.onchange = updateJson + let tank_allow_pumping_if_sensor_error = document.getElementById("tank_allow_pumping_if_sensor_error") as HTMLInputElement; + tank_allow_pumping_if_sensor_error.onchange = updateJson let night_lamp_only_when_dark = document.getElementById("night_lamp_only_when_dark") as HTMLInputElement; night_lamp_only_when_dark.onchange = updateJson let night_lamp_time_start = document.getElementById("night_lamp_time_start") as HTMLSelectElement; @@ -53,6 +75,33 @@ let fromWrapper = (() => { header.textContent = "Plant " + (i + 1); plant.appendChild(header); + { + let holder = document.createElement("div"); + plant.appendChild(holder); + let inputf = document.createElement("select"); + inputf.id = "plant_" + i + "_mode"; + inputf.onchange = updateJson; + holder.appendChild(inputf) + + let optionOff = document.createElement("option"); + optionOff.value = "OFF"; + optionOff.innerText = "Off"; + inputf.appendChild(optionOff); + + let optionTargetMoisture = document.createElement("option"); + optionTargetMoisture.value = "TargetMoisture"; + optionTargetMoisture.innerText = "Target Moisture"; + inputf.appendChild(optionTargetMoisture); + + let optionTimerOnly = document.createElement("option"); + optionTimerOnly.value = "TimerOnly"; + optionTimerOnly.innerText = "Timer"; + inputf.appendChild(optionTimerOnly); + + let text = document.createElement("span"); + holder.appendChild(text) + text.innerHTML += "Mode" + } { let holder = document.createElement("div"); plant.appendChild(holder); @@ -130,16 +179,25 @@ let fromWrapper = (() => { function sync(current: PlantConfig) { plantcount = current.plants.length - tank_full_ml.disabled = !current.tank_sensor_enabled; + mqtt_url.value = current.mqtt_url; + base_topic.value = current.base_topic; + max_consecutive_pump_count.value = current.max_consecutive_pump_count.toString(); + + tank_useable_ml.disabled = !current.tank_sensor_enabled; tank_warn_percent.disabled = !current.tank_sensor_enabled; - tank_sensor_enabled.checked = current.tank_sensor_enabled; - tank_full_ml.value = current.tank_full_ml.toString(); + tank_allow_pumping_if_sensor_error.checked = current.tank_allow_pumping_if_sensor_error; + tank_useable_ml.value = current.tank_useable_ml.toString(); tank_warn_percent.value = current.tank_warn_percent.toString(); + tank_empty_mv.value = current.tank_empty_mv.toString(); + tank_full_mv.value = current.tank_full_mv.toString(); + night_lamp_time_start.value = current.night_lamp_hour_start.toString(); night_lamp_time_end.value = current.night_lamp_hour_end.toString(); for (let i = 0; i < current.plants.length; i++) { + let plant_mode = document.getElementById("plant_" + i + "_mode") as HTMLSelectElement; + plant_mode.value = current.plants[i].mode; let plant_target_moisture = document.getElementById("plant_" + i + "_target_moisture") as HTMLInputElement; plant_target_moisture.value = current.plants[i].target_moisture.toString(); let plant_pump_time_s = document.getElementById("plant_" + i + "_pump_time_s") as HTMLInputElement; @@ -155,9 +213,15 @@ let fromWrapper = (() => { function updateJson() { var current: PlantConfig = { + max_consecutive_pump_count: +max_consecutive_pump_count.value, + mqtt_url: mqtt_url.value, + base_topic: base_topic.value, + tank_allow_pumping_if_sensor_error: tank_allow_pumping_if_sensor_error.checked, tank_sensor_enabled: tank_sensor_enabled.checked, - tank_full_ml: +tank_full_ml.value, + tank_useable_ml: +tank_useable_ml.value, tank_warn_percent: +tank_warn_percent.value, + tank_empty_mv: +tank_empty_mv.value, + tank_full_mv: +tank_full_mv.value, night_lamp_hour_start: +night_lamp_time_start.value, night_lamp_hour_end: +night_lamp_time_end.value, night_lamp_only_when_dark: night_lamp_only_when_dark.checked, @@ -166,6 +230,7 @@ let fromWrapper = (() => { for (let i = 0; i < plantcount; i++) { console.log("Adding plant " + i) + let plant_mode = document.getElementById("plant_" + i + "_mode") as HTMLSelectElement; let plant_target_moisture = document.getElementById("plant_" + i + "_target_moisture") as HTMLInputElement; let plant_pump_time_s = document.getElementById("plant_" + i + "_pump_time_s") as HTMLInputElement; let plant_pump_cooldown_min = document.getElementById("plant_" + i + "_pump_cooldown_min") as HTMLInputElement; @@ -173,6 +238,7 @@ let fromWrapper = (() => { let plant_pump_hour_end = document.getElementById("plant_" + i + "_pump_hour_end") as HTMLInputElement; current.plants[i] = { + mode: plant_mode.value, target_moisture: +plant_target_moisture.value, pump_time_s: +plant_pump_time_s.value, pump_cooldown_min: +plant_pump_cooldown_min.value, @@ -184,18 +250,24 @@ let fromWrapper = (() => { sync(current); console.log(current); - var pretty = JSON.stringify(current, undefined, 4); + var pretty = JSON.stringify(current, undefined, 1); json.value = pretty; } let submitFormBtn = document.getElementById("submit") as HTMLButtonElement + let submit_status = document.getElementById("submit_status") + if (submitFormBtn) { + submitFormBtn.onclick = function (){ updateJson() fetch("/set_config", { method :"POST", body: json.value }) + .then(response => response.text()) + .then(text => submit_status.innerText = text) + }; }