diff --git a/rust/.cargo/config.toml b/rust/.cargo/config.toml index d87148b..c7e8e2b 100644 --- a/rust/.cargo/config.toml +++ b/rust/.cargo/config.toml @@ -5,8 +5,8 @@ target = "riscv32imac-esp-espidf" [target.riscv32imac-esp-espidf] linker = "ldproxy" #runner = "espflash flash --monitor --baud 921600 --partition-table partitions.csv -b no-reset" # Select this runner in case of usb ttl -#runner = "espflash flash --monitor --baud 921600 --flash-size 16mb --partition-table partitions.csv" -runner = "cargo runner" +runner = "espflash flash --monitor --baud 921600 --flash-size 16mb --partition-table partitions.csv" +#runner = "cargo runner" #runner = "espflash flash --monitor --partition-table partitions.csv -b no-reset" # create upgrade image file for webupload diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 593cc2c..cbfa9d1 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -88,6 +88,7 @@ text-template = "0.1.0" strum_macros = "0.27.0" esp-ota = { version = "0.2.2", features = ["log"] } unit-enum = "1.4.1" +pca9535 = { version = "2.0.0", features = ["std"] } [patch.crates-io] diff --git a/rust/src/config.rs b/rust/src/config.rs index f70fd25..910ce67 100644 --- a/rust/src/config.rs +++ b/rust/src/config.rs @@ -1,6 +1,5 @@ -use std::str::FromStr; - use serde::{Deserialize, Serialize}; +use std::str::FromStr; use crate::plant_state::PlantWateringMode; use crate::PLANT_COUNT; @@ -13,6 +12,7 @@ pub struct NetworkConfig { pub password: Option>, pub mqtt_url: Option>, pub base_topic: Option>, + pub max_wait: u32 } impl Default for NetworkConfig { fn default() -> Self { @@ -22,6 +22,7 @@ impl Default for NetworkConfig { password: None, mqtt_url: None, base_topic: None, + max_wait: 10000, } } } @@ -72,9 +73,31 @@ impl Default for TankConfig { } } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)] +pub enum BatteryBoardVersion{ + #[default] + Disabled, + BQ34Z100G1, + WchI2cSlave +} +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)] +pub enum BoardVersion{ + #[default] + INITIAL, + V3, + V4 +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)] +pub struct BoardHardware { + pub board: BoardVersion, + pub battery: BatteryBoardVersion, +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)] #[serde(default)] pub struct PlantControllerConfig { + pub hardware: BoardHardware, pub network: NetworkConfig, pub tank: TankConfig, pub night_lamp: NightLampConfig, diff --git a/rust/src/main.rs b/rust/src/main.rs index 8a0a643..b7f72f0 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -1,8 +1,4 @@ -use std::{ - fmt::Display, - sync::{atomic::AtomicBool, Arc, Mutex}, -}; -use std::sync::MutexGuard; +use crate::webserver::webserver::httpd; use anyhow::bail; use chrono::{DateTime, Datelike, Timelike, Utc}; use chrono_tz::Tz; @@ -18,19 +14,24 @@ use esp_idf_sys::{ use esp_ota::{mark_app_valid, rollback_and_reboot}; use log::{log, LogMessage}; use once_cell::sync::Lazy; -use plant_hal::{PlantCtrlBoard, PlantHal, PLANT_COUNT}; +use plant_hal::{PlantHal, PLANT_COUNT}; use serde::{Deserialize, Serialize}; -use crate::{config::PlantControllerConfig, webserver::webserver::httpd}; +use std::sync::MutexGuard; +use std::{ + fmt::Display, + sync::{atomic::AtomicBool, Arc, Mutex}, +}; mod config; mod log; pub mod plant_hal; mod plant_state; mod tank; +use crate::plant_hal::{BatteryInteraction, BoardHal, BoardInteraction, HAL}; use plant_state::PlantState; use tank::*; -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)); mod webserver { @@ -112,7 +113,6 @@ fn safe_main() -> anyhow::Result<()> { esp_idf_sys::CONFIG_MAIN_TASK_STACK_SIZE ) } - println!("Startup Rust"); let mut to_config = false; @@ -150,20 +150,9 @@ fn safe_main() -> anyhow::Result<()> { }; log(LogMessage::PartitionState, 0, 0, "", ota_state_string); - let mut board: std::sync::MutexGuard<'_, PlantCtrlBoard<'_>> = BOARD_ACCESS.lock().unwrap(); + let mut board = BOARD_ACCESS.lock().expect("Could not lock board no other lock should be able to exist during startup!"); board.general_fault(false); - log(LogMessage::MountingFilesystem, 0, 0, "", ""); - board.mount_file_system()?; - let free_space = board.file_system_size()?; - log( - LogMessage::FilesystemMount, - free_space.free_size as u32, - free_space.total_size as u32, - &free_space.used_size.to_string(), - "", - ); - let cur = board .get_rtc_time() .or_else(|err| { @@ -183,7 +172,12 @@ fn safe_main() -> anyhow::Result<()> { } println!("cur is {}", cur); - board.update_charge_indicator(); + match board.battery_monitor.average_current_milli_ampere() { + Ok(charging) => { + board.set_charge_indicator(charging > 20) + } + Err(_) => {} + } if board.get_restart_to_conf() { log(LogMessage::ConfigModeSoftwareOverride, 0, 0, "", ""); @@ -214,28 +208,22 @@ fn safe_main() -> anyhow::Result<()> { } } - let config: PlantControllerConfig = match board.get_config() { - Ok(valid) => valid, - Err(err) => { - log( - LogMessage::ConfigModeMissingConfig, - 0, - 0, - "", - &err.to_string(), - ); - //config upload will trigger reboot! - let _ = board.wifi_ap(None); + match board.board_hal { + BoardHal::Initial { .. } => { + //config upload will trigger reboot and then switch to selected board_hal + let _ = board.wifi_ap(); drop(board); let reboot_now = Arc::new(AtomicBool::new(false)); let _webserver = httpd(reboot_now.clone()); wait_infinity(WaitType::MissingConfig, reboot_now.clone()); } - }; + _ => {} + } + println!("attempting to connect wifi"); - let network_mode = if config.network.ssid.is_some() { - try_connect_wifi_sntp_mqtt(&mut board, &config) + let network_mode = if board.config.network.ssid.is_some() { + try_connect_wifi_sntp_mqtt(&mut board) } else { println!("No wifi configured"); NetworkMode::OFFLINE @@ -243,7 +231,7 @@ fn safe_main() -> anyhow::Result<()> { if matches!(network_mode, NetworkMode::OFFLINE) && to_config { println!("Could not connect to station and config mode forced, switching to ap mode!"); - match board.wifi_ap(Some(config.network.ap_ssid.clone())) { + match board.wifi_ap() { Ok(_) => { println!("Started ap, continuing") } @@ -251,7 +239,7 @@ fn safe_main() -> anyhow::Result<()> { } } - let timezone = match &config.timezone { + let timezone = match & board.config.timezone { Some(tz_str) => tz_str.parse::().unwrap_or_else(|_| { println!("Invalid timezone '{}', falling back to UTC", tz_str); UTC @@ -268,8 +256,8 @@ fn safe_main() -> anyhow::Result<()> { ); if let NetworkMode::WIFI { ref ip_address, .. } = network_mode { - publish_firmware_info(version, address, ota_state_string, &mut board, &config, &ip_address, timezone_time); - publish_battery_state(&mut board, &config); + publish_firmware_info(version, address, ota_state_string, &mut board, &ip_address, timezone_time); + publish_battery_state(&mut board); } @@ -295,10 +283,10 @@ fn safe_main() -> anyhow::Result<()> { let dry_run = false; - let tank_state = determine_tank_state(&mut board, &config); + let tank_state = determine_tank_state(&mut board); if tank_state.is_enabled() { - if let Some(err) = tank_state.got_error(&config.tank) { + if let Some(err) = tank_state.got_error(&board.config.tank) { match err { TankError::SensorDisabled => { /* unreachable */ } TankError::SensorMissing(raw_value_mv) => log( @@ -321,7 +309,7 @@ fn safe_main() -> anyhow::Result<()> { } // disabled cannot trigger this because of wrapping if is_enabled board.general_fault(true); - } else if tank_state.warn_level(&config.tank).is_ok_and(|warn| warn) { + } else if tank_state.warn_level(&board.config.tank).is_ok_and(|warn| warn) { log(LogMessage::TankWaterLevelLow, 0, 0, "", ""); board.general_fault(true); } @@ -336,15 +324,15 @@ fn safe_main() -> anyhow::Result<()> { } } - publish_tank_state(&mut board, &config, &tank_state, &water_temp); + publish_tank_state(&mut board, &tank_state, &water_temp); let plantstate: [PlantState; PLANT_COUNT] = - core::array::from_fn(|i| PlantState::read_hardware_state(i, &mut board, &config.plants[i])); - publish_plant_states(&mut board, &config, &timezone_time, &plantstate); + core::array::from_fn(|i| PlantState::read_hardware_state(i, &mut board)); + publish_plant_states(&mut board, &timezone_time, &plantstate); let pump_required = plantstate .iter() - .zip(&config.plants) + .zip(&board.config.plants) .any(|(it, conf)| it.needs_to_be_watered(conf, &timezone_time)) && !water_frozen; if pump_required { @@ -352,7 +340,7 @@ fn safe_main() -> anyhow::Result<()> { if !dry_run { board.any_pump(true)?; // enables main power output, eg for a central pump with valve setup or a main water valve for the risk affine } - for (plant_id, (state, plant_config)) in plantstate.iter().zip(&config.plants).enumerate() { + for (plant_id, (state, plant_config)) in plantstate.iter().zip(&board.config.plants.clone()).enumerate() { if state.needs_to_be_watered(plant_config, &timezone_time) { let pump_count = board.consecutive_pump_count(plant_id) + 1; board.store_consecutive_pump_count(plant_id, pump_count); @@ -361,12 +349,12 @@ fn safe_main() -> anyhow::Result<()> { if pump_ineffective { log( LogMessage::ConsecutivePumpCountLimit, - pump_count as u32, + pump_count, plant_config.max_consecutive_pump_count as u32, &(plant_id+1).to_string(), "", ); - board.fault(plant_id, true); + board.fault(plant_id, true)?; } log( LogMessage::PumpPlant, @@ -379,14 +367,14 @@ fn safe_main() -> anyhow::Result<()> { board.last_pump_time(plant_id); //state.active = true; - pump_info(&mut board, &config, plant_id, true, pump_ineffective); + pump_info(&mut board, plant_id, true, pump_ineffective); if !dry_run { board.pump(plant_id, true)?; Delay::new_default().delay_ms(1000 * plant_config.pump_time_s as u32); board.pump(plant_id, false)?; } - pump_info(&mut board, &config, plant_id, false, pump_ineffective); + pump_info(&mut board, plant_id, false, pump_ineffective); } else if !state.pump_in_timeout(plant_config, &timezone_time) { // plant does not need to be watered and is not in timeout @@ -400,29 +388,29 @@ fn safe_main() -> anyhow::Result<()> { } let is_day = board.is_day(); - let state_of_charge = board.state_charge_percent().unwrap_or(0); + let state_of_charge = board.battery_monitor.state_charge_percent().unwrap_or(0); let mut light_state = LightState { - enabled: config.night_lamp.enabled, + enabled: board.config.night_lamp.enabled, ..Default::default() }; if light_state.enabled { light_state.is_day = is_day; light_state.out_of_work_hour = !in_time_range( &timezone_time, - config.night_lamp.night_lamp_hour_start, - config.night_lamp.night_lamp_hour_end, + board.config.night_lamp.night_lamp_hour_start, + board.config.night_lamp.night_lamp_hour_end, ); - if state_of_charge < config.night_lamp.low_soc_cutoff { + if state_of_charge < board.config.night_lamp.low_soc_cutoff { board.set_low_voltage_in_cycle(); - } else if state_of_charge > config.night_lamp.low_soc_restore { + } else if state_of_charge > board.config.night_lamp.low_soc_restore { board.clear_low_voltage_in_cycle(); } light_state.battery_low = board.low_voltage_in_cycle(); if !light_state.out_of_work_hour { - if config.night_lamp.night_lamp_only_when_dark { + if board.config.night_lamp.night_lamp_only_when_dark { if !light_state.is_day { if light_state.battery_low { board.light(false)?; @@ -447,7 +435,7 @@ fn safe_main() -> anyhow::Result<()> { match serde_json::to_string(&light_state) { Ok(state) => { - let _ = board.mqtt_publish(&config, "/light", state.as_bytes()); + let _ = board.mqtt_publish( "/light", state.as_bytes()); } Err(err) => { println!("Error publishing lightstate {}", err); @@ -455,16 +443,16 @@ fn safe_main() -> anyhow::Result<()> { }; let deep_sleep_duration_minutes: u32 = if state_of_charge < 10 { - let _ = board.mqtt_publish(&config, "/deepsleep", "low Volt 12h".as_bytes()); + let _ = board.mqtt_publish( "/deepsleep", "low Volt 12h".as_bytes()); 12 * 60 } else if is_day { - let _ = board.mqtt_publish(&config, "/deepsleep", "normal 20m".as_bytes()); + let _ = board.mqtt_publish( "/deepsleep", "normal 20m".as_bytes()); 20 } else { - let _ = board.mqtt_publish(&config, "/deepsleep", "night 1h".as_bytes()); + let _ = board.mqtt_publish( "/deepsleep", "night 1h".as_bytes()); 60 }; - let _ = board.mqtt_publish(&config, "/state", "sleep".as_bytes()); + let _ = board.mqtt_publish( "/state", "sleep".as_bytes()); mark_app_valid(); @@ -484,7 +472,7 @@ fn safe_main() -> anyhow::Result<()> { board.deep_sleep(1000 * 1000 * 60 * deep_sleep_duration_minutes as u64); } -fn obtain_tank_temperature(board: &mut MutexGuard) -> anyhow::Result { +fn obtain_tank_temperature(board: &mut MutexGuard) -> anyhow::Result { //multisample should be moved to water_temperature_c let mut attempt = 1; let water_temp: Result = loop { @@ -506,10 +494,10 @@ fn obtain_tank_temperature(board: &mut MutexGuard) -> anyhow::Re water_temp } -fn publish_tank_state(board: &mut MutexGuard, config: &PlantControllerConfig, tank_state: &TankState, water_temp: &anyhow::Result) { - match serde_json::to_string(&tank_state.as_mqtt_info(&config.tank, water_temp)) { +fn publish_tank_state(board: &mut MutexGuard, tank_state: &TankState, water_temp: &anyhow::Result) { + match serde_json::to_string(&tank_state.as_mqtt_info(&board.config.tank, water_temp)) { Ok(state) => { - let _ = board.mqtt_publish(&config, "/water", state.as_bytes()); + let _ = board.mqtt_publish("/water", state.as_bytes()); } Err(err) => { println!("Error publishing tankstate {}", err); @@ -517,12 +505,12 @@ fn publish_tank_state(board: &mut MutexGuard, config: &PlantCont }; } -fn publish_plant_states(board: &mut MutexGuard, config: &PlantControllerConfig, timezone_time: &DateTime, plantstate: &[PlantState; 8]) { - for (plant_id, (plant_state, plant_conf)) in plantstate.iter().zip(&config.plants).enumerate() { +fn publish_plant_states(board: &mut MutexGuard, timezone_time: &DateTime, plantstate: &[PlantState; 8]) { + for (plant_id, (plant_state, plant_conf)) in plantstate.iter().zip(& board.config.plants.clone()).enumerate() { match serde_json::to_string(&plant_state.to_mqtt_info(plant_conf, &timezone_time)) { Ok(state) => { let plant_topic = format!("/plant{}", plant_id + 1); - let _ = board.mqtt_publish(&config, &plant_topic, state.as_bytes()); + let _ = board.mqtt_publish(&plant_topic, state.as_bytes()); //reduce speed as else messages will be dropped Delay::new_default().delay_ms(200); } @@ -533,34 +521,27 @@ fn publish_plant_states(board: &mut MutexGuard, config: &PlantCo } } -fn publish_firmware_info(version: VersionInfo, address: u32, ota_state_string: &str, board: &mut MutexGuard, config: &PlantControllerConfig, ip_address: &String, timezone_time: DateTime) { - let _ = board.mqtt_publish(&config, "/firmware/address", ip_address.as_bytes()); - let _ = board.mqtt_publish(&config, "/firmware/githash", version.git_hash.as_bytes()); +fn publish_firmware_info(version: VersionInfo, address: u32, ota_state_string: &str, board: &mut MutexGuard, ip_address: &String, timezone_time: DateTime) { + let _ = board.mqtt_publish("/firmware/address", ip_address.as_bytes()); + let _ = board.mqtt_publish( "/firmware/githash", version.git_hash.as_bytes()); let _ = board.mqtt_publish( - &config, "/firmware/buildtime", version.build_time.as_bytes(), ); let _ = board.mqtt_publish( - &config, "/firmware/last_online", timezone_time.to_rfc3339().as_bytes(), ); - let _ = board.mqtt_publish(&config, "/firmware/ota_state", ota_state_string.as_bytes()); + let _ = board.mqtt_publish( "/firmware/ota_state", ota_state_string.as_bytes()); let _ = board.mqtt_publish( - &config, "/firmware/partition_address", format!("{:#06x}", address).as_bytes(), ); - let _ = board.mqtt_publish(&config, "/state", "online".as_bytes()); + let _ = board.mqtt_publish( "/state", "online".as_bytes()); } -fn try_connect_wifi_sntp_mqtt(board: &mut MutexGuard, config: &PlantControllerConfig) -> NetworkMode{ - match board.wifi( - config.network.ssid.clone().unwrap(), - config.network.password.clone(), - 10000, - ) { +fn try_connect_wifi_sntp_mqtt(board: &mut MutexGuard) -> NetworkMode{ + match board.wifi() { Ok(ip_info) => { let sntp_mode: SntpMode = match board.sntp(1000 * 10) { Ok(new_time) => { @@ -574,8 +555,8 @@ fn try_connect_wifi_sntp_mqtt(board: &mut MutexGuard, config: &P SntpMode::OFFLINE } }; - let mqtt_connected = if let Some(_) = config.network.mqtt_url { - match board.mqtt(&config) { + let mqtt_connected = if let Some(_) = board.config.network.mqtt_url { + match board.mqtt() { Ok(_) => { println!("Mqtt connection ready"); true @@ -603,7 +584,7 @@ fn try_connect_wifi_sntp_mqtt(board: &mut MutexGuard, config: &P } //TODO clean this up? better state -fn pump_info(board: &mut MutexGuard, config: &PlantControllerConfig, plant_id: usize, pump_active: bool, pump_ineffective: bool) { +fn pump_info(board: &mut MutexGuard, plant_id: usize, pump_active: bool, pump_ineffective: bool) { let pump_info = PumpInfo { enabled: pump_active, pump_ineffective @@ -611,7 +592,7 @@ fn pump_info(board: &mut MutexGuard, config: &PlantControllerCon let pump_topic = format!("/pump{}", plant_id + 1); match serde_json::to_string(&pump_info) { Ok(state) => { - let _ = board.mqtt_publish(config, &pump_topic, state.as_bytes()); + let _ = board.mqtt_publish(&pump_topic, state.as_bytes()); //reduce speed as else messages will be dropped Delay::new_default().delay_ms(200); } @@ -622,18 +603,10 @@ fn pump_info(board: &mut MutexGuard, config: &PlantControllerCon } fn publish_battery_state( - board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, - config: &PlantControllerConfig, + board: &mut MutexGuard<'_, HAL<'_>> ) { - let bat = board.get_battery_state(); - match serde_json::to_string(&bat) { - Ok(state) => { - let _ = board.mqtt_publish(config, "/battery", state.as_bytes()); - } - Err(err) => { - println!("Error publishing battery_state {}", err); - } - }; + let state = board.get_battery_state(); + let _ = board.mqtt_publish( "/battery", state.as_bytes()); } fn wait_infinity(wait_type: WaitType, reboot_now: Arc) -> ! { @@ -644,28 +617,30 @@ fn wait_infinity(wait_type: WaitType, reboot_now: Arc) -> ! { loop { unsafe { let mut lock = BOARD_ACCESS.lock().unwrap(); - lock.update_charge_indicator(); + if let Ok(charging) = lock.battery_monitor.average_current_milli_ampere() { + lock.set_charge_indicator(charging > 20) + } match wait_type { WaitType::MissingConfig => { // Keep existing behavior: circular filling pattern led_count %= 8; led_count += 1; for i in 0..8 { - lock.fault(i, i < led_count); + let _ = lock.fault(i, i < led_count); } } WaitType::ConfigButton => { // Alternating pattern: 1010 1010 -> 0101 0101 pattern_step = (pattern_step + 1) % 2; for i in 0..8 { - lock.fault(i, (i + pattern_step) % 2 == 0); + let _ = lock.fault(i, (i + pattern_step) % 2 == 0); } } WaitType::MqttConfig => { // Moving dot pattern pattern_step = (pattern_step + 1) % 8; for i in 0..8 { - lock.fault(i, i == pattern_step); + let _ = lock.fault(i, i == pattern_step); } } } @@ -678,7 +653,7 @@ fn wait_infinity(wait_type: WaitType, reboot_now: Arc) -> ! { // Clear all LEDs for i in 0..8 { - lock.fault(i, false); + let _ = lock.fault(i, false); } drop(lock); vTaskDelay(delay); diff --git a/rust/src/plant_hal.rs b/rust/src/plant_hal.rs index 96fef55..c1201c5 100644 --- a/rust/src/plant_hal.rs +++ b/rust/src/plant_hal.rs @@ -12,7 +12,7 @@ use embedded_svc::wifi::{ use esp_idf_hal::adc::oneshot::config::AdcChannelConfig; use esp_idf_hal::adc::oneshot::{AdcChannelDriver, AdcDriver}; -use esp_idf_hal::adc::{attenuation, Resolution}; +use esp_idf_hal::adc::{attenuation, Resolution, ADC1}; use esp_idf_hal::i2c::{APBTickType, I2cConfig, I2cDriver, I2cError}; use esp_idf_hal::units::FromValueType; use esp_idf_svc::eventloop::EspSystemEventLoop; @@ -49,29 +49,24 @@ use std::time::Duration; use embedded_hal::digital::OutputPin; use esp_idf_hal::delay::Delay; -use esp_idf_hal::gpio::{AnyInputPin, Gpio18, Gpio5, IOPin, InputOutput, Level, PinDriver, Pull}; -use esp_idf_hal::pcnt::{ - PcntChannel, PcntChannelConfig, PcntControlMode, PcntCountMode, PcntDriver, PinIndex, -}; +use esp_idf_hal::gpio::{AnyInputPin, Gpio0, Gpio1, Gpio10, Gpio11, Gpio12, Gpio13, Gpio14, Gpio15, Gpio16, Gpio17, Gpio18, Gpio19, Gpio2, Gpio20, Gpio21, Gpio22, Gpio23, Gpio24, Gpio25, Gpio26, Gpio27, Gpio28, Gpio29, Gpio3, Gpio30, Gpio4, Gpio5, Gpio6, Gpio7, Gpio8, IOPin, InputOutput, Level, PinDriver, Pull}; +use esp_idf_hal::pcnt::{PcntChannel, PcntChannelConfig, PcntControlMode, PcntCountMode, PcntDriver, PinIndex, PCNT0}; 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::{gpio_hold_dis, gpio_hold_en, vTaskDelay, EspError}; use one_wire_bus::OneWire; - -use crate::config::PlantControllerConfig; +use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface}; +use crate::config::{BatteryBoardVersion, BoardVersion, PlantControllerConfig}; use crate::log::log; -use crate::{to_string, STAY_ALIVE}; +use crate::plant_hal::BoardHal::{Initial, V3, V4}; +use crate::{plant_hal, to_string, STAY_ALIVE}; //Only support for 8 right now! pub const PLANT_COUNT: usize = 8; const REPEAT_MOIST_MEASURE: usize = 1; -const SPIFFS_PARTITION_NAME: &str = "storage"; -const CONFIG_FILE: &str = "/spiffs/config.cfg"; -const BASE_PATH: &str = "/spiffs"; - const TANK_MULTI_SAMPLE: usize = 11; const PUMP8_BIT: usize = 0; @@ -83,12 +78,39 @@ const PUMP5_BIT: usize = 5; const PUMP6_BIT: usize = 6; const PUMP7_BIT: usize = 7; -const MS_0: usize = 8; -const MS_4: usize = 9; -const MS_2: usize = 10; -const MS_3: usize = 11; -const SENSOR_ON: usize = 12; -const MS_1: usize = 13; +#[non_exhaustive] +struct V3Constants; + +impl V3Constants { + const MS_0: usize = 8; + const MS_4: usize = 9; + const MS_2: usize = 10; + const MS_3: usize = 11; + const MS_1: usize = 13; + const SENSOR_ON: usize = 12; + + const SENSOR_A_1: u8 = 7; + const SENSOR_A_2: u8 = 6; + const SENSOR_A_3: u8 = 5; + const SENSOR_A_4: u8 = 4; + const SENSOR_A_5: u8 = 3; + const SENSOR_A_6: u8 = 2; + const SENSOR_A_7: u8 = 1; + const SENSOR_A_8: u8 = 0; + + const SENSOR_B_1: u8 = 8; + const SENSOR_B_2: u8 = 9; + const SENSOR_B_3: u8 = 10; + const SENSOR_B_4: u8 = 11; + const SENSOR_B_5: u8 = 12; + const SENSOR_B_6: u8 = 13; + const SENSOR_B_7: u8 = 14; + const SENSOR_B_8: u8 = 15; +} + + + + const CHARGING: usize = 14; const AWAKE: usize = 15; @@ -101,23 +123,7 @@ const FAULT_4: usize = 21; const FAULT_1: usize = 22; const FAULT_2: usize = 23; -const SENSOR_A_1: u8 = 7; -const SENSOR_A_2: u8 = 6; -const SENSOR_A_3: u8 = 5; -const SENSOR_A_4: u8 = 4; -const SENSOR_A_5: u8 = 3; -const SENSOR_A_6: u8 = 2; -const SENSOR_A_7: u8 = 1; -const SENSOR_A_8: u8 = 0; -const SENSOR_B_1: u8 = 8; -const SENSOR_B_2: u8 = 9; -const SENSOR_B_3: u8 = 10; -const SENSOR_B_4: u8 = 11; -const SENSOR_B_5: u8 = 12; -const SENSOR_B_6: u8 = 13; -const SENSOR_B_7: u8 = 14; -const SENSOR_B_8: u8 = 15; const X25: crc::Crc = crc::Crc::::new(&crc::CRC_16_IBM_SDLC); @@ -150,232 +156,88 @@ pub enum Sensor { } pub struct PlantHal {} - -pub struct PlantCtrlBoard<'a> { - shift_register: ShiftRegister40< - PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, - PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, - PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, - >, - shift_register_enable_invert: - PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, esp_idf_hal::gpio::Output>, - tank_channel: AdcChannelDriver<'a, Gpio5, AdcDriver<'a, esp_idf_hal::adc::ADC1>>, - solar_is_day: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, esp_idf_hal::gpio::Input>, - boot_button: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, esp_idf_hal::gpio::Input>, - signal_counter: PcntDriver<'a>, - light: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, - main_pump: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, - 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>, +pub struct ESP<'a> { mqtt_client: Option>, - battery_driver: Bq34z100g1Driver>, Delay>, - rtc: - Ds323x>>, ds323x::ic::DS3231>, - eeprom: Eeprom24x< - MutexDevice<'a, I2cDriver<'a>>, - eeprom24x::page_size::B32, - eeprom24x::addr_size::TwoBytes, - eeprom24x::unique_serial::No, - >, + wifi_driver: EspWifi<'a>, + boot_button: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, esp_idf_hal::gpio::Input> } -#[derive(Serialize, Debug)] -pub struct FileInfo { - filename: String, - size: usize, -} - -#[derive(Serialize, Debug)] -pub struct FileList { - total: usize, - used: usize, - files: Vec, - file_system_corrupt: Option, - iter_error: Option, -} - -#[derive(Serialize)] -pub struct BatteryState { - voltage_milli_volt: String, - current_milli_ampere: String, - cycle_count: String, - design_milli_ampere: String, - remaining_milli_ampere: String, - state_of_charge: String, - state_of_health: String, - temperature: String, -} - -#[derive(Serialize, Deserialize, PartialEq, Debug)] -pub struct BackupHeader { - pub timestamp: i64, - crc16: u16, - pub size: usize, -} - -impl PlantCtrlBoard<'_> { - pub fn update_charge_indicator(&mut self) { - let is_charging = match self.battery_driver.average_current() { - OkStd(current) => current < 20, - Err(_) => false, - }; - self.shift_register.decompose()[CHARGING] - .set_state(is_charging.into()) - .unwrap(); +impl ESP<'_> { + const SPIFFS_PARTITION_NAME: &'static str = "storage"; + const CONFIG_FILE: &'static str = "/spiffs/config.cfg"; + const BASE_PATH: &'static str = "/spiffs"; + fn get_config(&mut self) -> Result { + let cfg = File::open(Self::CONFIG_FILE)?; + let config: PlantControllerConfig = serde_json::from_reader(cfg)?; + Ok(config) } - - pub fn deep_sleep(&mut self, duration_in_ms: u64) -> ! { - self.shift_register.decompose()[AWAKE].set_low().unwrap(); - unsafe { - //if we don't do this here, we might just revert newly flashed firmware - mark_app_valid(); - //allow early wakeup by pressing the boot button - if duration_in_ms == 0 { - esp_restart(); - } else { - //configure gpio 1 to wakeup on low, reused boot button for this - esp_sleep_enable_ext1_wakeup( - 0b10u64, - esp_sleep_ext1_wakeup_mode_t_ESP_EXT1_WAKEUP_ANY_LOW, - ); - esp_deep_sleep(duration_in_ms); - } - }; + pub(crate) fn set_config(&mut self, config: &PlantControllerConfig) -> Result<()> { + let mut cfg = File::create(Self::CONFIG_FILE)?; + serde_json::to_writer(&mut cfg, &config)?; + println!("Wrote config config {:?}", config); + Ok(()) } - - pub fn get_backup_info(&mut self) -> Result { - let dummy = BackupHeader { - timestamp: 0, - crc16: 0, - size: 0, - }; - let store = bincode::serialize(&dummy)?.len(); - let mut header_page_buffer = vec![0_u8; store]; - - match self.eeprom.read_data(0, &mut header_page_buffer) { - OkStd(_) => {} - Err(err) => bail!("Error reading eeprom header {:?}", err), - }; - println!("Raw header is {:?} with size {}", header_page_buffer, store); - let header: BackupHeader = bincode::deserialize(&header_page_buffer)?; - Ok(header) - } - - pub fn get_backup_config(&mut self) -> Result> { - let dummy = BackupHeader { - timestamp: 0, - crc16: 0, - size: 0, - }; - let store = bincode::serialize(&dummy)?.len(); - let mut header_page_buffer = vec![0_u8; store]; - - match self.eeprom.read_data(0, &mut header_page_buffer) { - OkStd(_) => {} - Err(err) => bail!("Error reading eeprom header {:?}", err), - }; - let header: BackupHeader = bincode::deserialize(&header_page_buffer)?; - - //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(_) => {} - Err(err) => bail!("Error reading eeprom data {:?}", err), - }; - - let checksum = X25.checksum(&data_buffer); - if checksum != header.crc16 { - bail!( - "Invalid checksum, got {} but expected {}", - checksum, - header.crc16 - ); - } - - Ok(data_buffer) - } - - pub fn backup_config(&mut self, bytes: &[u8]) -> Result<()> { - let delay = Delay::new_default(); - - let checksum = X25.checksum(bytes); - let page_size = self.eeprom.page_size(); - - let time = self.get_rtc_time()?.timestamp_millis(); - - let header = BackupHeader { - crc16: checksum, - timestamp: time, - size: bytes.len(), - }; - - let encoded = bincode::serialize(&header)?; - if encoded.len() > page_size { - bail!( - "Size limit reached header is {}, but firest page is only {}", - encoded.len(), - page_size - ) - } - let as_u8: &[u8] = &encoded; - - match self.eeprom.write_page(0, as_u8) { - OkStd(_) => {} - Err(err) => bail!("Error writing eeprom {:?}", err), - }; - delay.delay_ms(5); - - let to_write = bytes.chunks(page_size); - - let mut lastiter = 0; - let mut current_page = 1; - for chunk in to_write { - let address = current_page * page_size as u32; - match self.eeprom.write_page(address, chunk) { - OkStd(_) => {} - Err(err) => bail!("Error writing eeprom {:?}", err), - }; - current_page += 1; - - let iter = (current_page % 8) as usize; - if iter != lastiter { - for i in 0..PLANT_COUNT { - self.fault(i, iter == i); - } - lastiter = iter; - } - - //update led here? - delay.delay_ms(5); + fn delete_config(&self) -> Result<()>{ + let config = Path::new(Self::CONFIG_FILE); + if config.exists() { + println!("Removing config"); + fs::remove_file(config)? } Ok(()) } + fn mount_file_system(&mut self) -> Result<()> { + log(LogMessage::MountingFilesystem, 0, 0, "", ""); + let base_path = CString::new("/spiffs")?; + let storage = CString::new(Self::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: 5, + format_if_mount_failed: true, + }; - pub fn get_battery_state(&mut self) -> 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()), - design_milli_ampere: to_string(self.design_milli_ampere_hour()), - remaining_milli_ampere: to_string(self.remaining_milli_ampere_hour()), - state_of_charge: to_string(self.state_charge_percent()), - state_of_health: to_string(self.state_health_percent()), - temperature: to_string(self.bat_temperature()), + unsafe { + esp_idf_sys::esp!(esp_idf_sys::esp_vfs_spiffs_register(&conf))?; } + + let free_space = self.file_system_size()?; + log( + LogMessage::FilesystemMount, + free_space.free_size as u32, + free_space.total_size as u32, + &free_space.used_size.to_string(), + "", + ); + Ok(()) + } + fn file_system_size(&mut self) -> Result { + let storage = CString::new(Self::SPIFFS_PARTITION_NAME)?; + let mut total_size = 0; + let mut used_size = 0; + unsafe { + esp_idf_sys::esp!(esp_spiffs_info( + storage.as_ptr(), + &mut total_size, + &mut used_size + ))?; + } + Ok(FileSystemSizeInfo { + total_size, + used_size, + free_size: total_size - used_size, + }) } - pub fn list_files(&self) -> FileList { - let storage = CString::new(SPIFFS_PARTITION_NAME).unwrap(); + + pub(crate) fn list_files(&self) -> FileList { + let storage = CString::new(Self::SPIFFS_PARTITION_NAME).unwrap(); let mut file_system_corrupt = None; let mut iter_error = None; let mut result = Vec::new(); - let filepath = Path::new(BASE_PATH); + let filepath = Path::new(Self::BASE_PATH); let read_dir = fs::read_dir(filepath); match read_dir { OkStd(read_dir) => { @@ -417,9 +279,8 @@ impl PlantCtrlBoard<'_> { iter_error, } } - - pub fn delete_file(&self, filename: &str) -> Result<()> { - let filepath = Path::new(BASE_PATH).join(Path::new(filename)); + pub(crate) fn delete_file(&self, filename: &str) -> Result<()> { + let filepath = Path::new(Self::BASE_PATH).join(Path::new(filename)); match fs::remove_file(filepath) { OkStd(_) => Ok(()), Err(err) => { @@ -427,9 +288,8 @@ impl PlantCtrlBoard<'_> { } } } - - pub fn get_file_handle(&self, filename: &str, write: bool) -> Result { - let filepath = Path::new(BASE_PATH).join(Path::new(filename)); + pub(crate) fn get_file_handle(&self, filename: &str, write: bool) -> Result { + let filepath = Path::new(Self::BASE_PATH).join(Path::new(filename)); Ok(if write { File::create(filepath)? } else { @@ -437,18 +297,528 @@ impl PlantCtrlBoard<'_> { }) } - pub fn is_day(&self) -> bool { - self.solar_is_day.get_level().into() + fn init_rtc_deepsleep_memory(&self, init_rtc_store: bool, to_config_mode: bool){ + if init_rtc_store { + unsafe { + LAST_WATERING_TIMESTAMP = [0; PLANT_COUNT]; + CONSECUTIVE_WATERING_PLANT = [0; PLANT_COUNT]; + LOW_VOLTAGE_DETECTED = false; + crate::log::init(); + RESTART_TO_CONF = to_config_mode; + }; + } else { + unsafe { + if to_config_mode { + RESTART_TO_CONF = true; + } + log( + LogMessage::RestartToConfig, + RESTART_TO_CONF as u32, + 0, + "", + "", + ); + log( + LogMessage::LowVoltage, + LOW_VOLTAGE_DETECTED as u32, + 0, + "", + "", + ); + for i in 0..PLANT_COUNT { + println!( + "LAST_WATERING_TIMESTAMP[{}] = UTC {}", + i, LAST_WATERING_TIMESTAMP[i] + ); + } + for i in 0..PLANT_COUNT { + println!( + "CONSECUTIVE_WATERING_PLANT[{}] = {}", + i, CONSECUTIVE_WATERING_PLANT[i] + ); + } + } + } + } +} + +pub struct HAL<'a>{ + pub config: PlantControllerConfig, + pub board_hal: BoardHal<'a>, + pub esp: ESP<'a>, + pub battery_monitor: BatteryMonitor<'a> +} + +pub enum BatteryMonitor<'a> { + Disabled { + + }, + BQ34Z100G1 { + battery_driver: Bq34z100g1Driver>, Delay> + }, + WchI2cSlave { + + } +} + +pub enum BoardHal<'a>{ + Initial { + general_fault: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput> + }, + V3 { + shift_register: ShiftRegister40< + PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, + PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, + PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, + >, + shift_register_enable_invert: + PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, esp_idf_hal::gpio::Output>, + tank_channel: AdcChannelDriver<'a, Gpio5, AdcDriver<'a, esp_idf_hal::adc::ADC1>>, + solar_is_day: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, esp_idf_hal::gpio::Input>, + light: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, + main_pump: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, + tank_power: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, + general_fault: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, + signal_counter: PcntDriver<'a>, + one_wire_bus: OneWire>, + rtc: + Ds323x>>, ds323x::ic::DS3231>, + eeprom: Eeprom24x< + MutexDevice<'a, I2cDriver<'a>>, + eeprom24x::page_size::B32, + eeprom24x::addr_size::TwoBytes, + eeprom24x::unique_serial::No, + >, + }, + V4 { + tank_channel: AdcChannelDriver<'a, Gpio5, AdcDriver<'a, esp_idf_hal::adc::ADC1>>, + solar_is_day: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, esp_idf_hal::gpio::Input>, + signal_counter: PcntDriver<'a>, + light: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, + tank_power: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, + one_wire_bus: OneWire>, + rtc: + Ds323x>>, ds323x::ic::DS3231>, + eeprom: Eeprom24x< + MutexDevice<'a, I2cDriver<'a>>, + eeprom24x::page_size::B32, + eeprom24x::addr_size::TwoBytes, + eeprom24x::unique_serial::No, + >, + general_fault: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, + pump_expander: Pca9535Immediate>>, + sensor_expander: Pca9535Immediate>>, + } +} + +#[derive(Serialize, Debug)] +pub struct FileInfo { + filename: String, + size: usize, +} + +#[derive(Serialize, Debug)] +pub struct FileList { + total: usize, + used: usize, + files: Vec, + file_system_corrupt: Option, + iter_error: Option, +} + +#[derive(Serialize)] +pub struct BatteryState { + voltage_milli_volt: String, + current_milli_ampere: String, + cycle_count: String, + design_milli_ampere: String, + remaining_milli_ampere: String, + state_of_charge: String, + state_of_health: String, + temperature: String, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct BackupHeader { + pub timestamp: i64, + crc16: u16, + pub size: usize, +} + +impl BatteryInteraction for BatteryMonitor<'_> { + fn state_charge_percent(&mut self) -> Result { + match self { + BatteryMonitor::BQ34Z100G1 { battery_driver} => { + match battery_driver.state_of_charge() { + OkStd(r) => Ok(r), + Err(err) => bail!("Error reading SoC {:?}", err), + } + }, + BatteryMonitor::WchI2cSlave { .. } => { + bail!("Not implemented") + } + BatteryMonitor::Disabled {} => { + bail!("Battery monitor is disabled") + } + } } + fn remaining_milli_ampere_hour(&mut self) -> Result { + match self { + BatteryMonitor::BQ34Z100G1 { battery_driver} => { + match battery_driver.remaining_capacity(){ + OkStd(r) => Ok(r), + Err(err) => bail!("Error reading remaining_milli_ampere_hour {:?}", err), + } + }, + BatteryMonitor::WchI2cSlave { .. } => { + bail!("Not implemented") + }, + &mut BatteryMonitor::Disabled { } => { + bail!("Battery monitor is disabled") + } + } + } + fn max_milli_ampere_hour(&mut self) -> Result { + match self { + BatteryMonitor::BQ34Z100G1 { battery_driver} => { + match battery_driver.full_charge_capacity() { + OkStd(r) => Ok(r), + Err(err) => bail!("Error reading max_milli_ampere_hour {:?}", err), + } + }, + BatteryMonitor::WchI2cSlave { .. } => { + bail!("Not implemented") + }, + &mut BatteryMonitor::Disabled { } => { + bail!("Battery monitor is disabled") + } + } + } + fn design_milli_ampere_hour(&mut self) -> Result { + match self { + BatteryMonitor::BQ34Z100G1 { battery_driver} => { + match battery_driver.design_capacity() { + OkStd(r) => Ok(r), + Err(err) => bail!("Error reading design_milli_ampere_hour {:?}", err), + } + }, + BatteryMonitor::WchI2cSlave { .. } => { + bail!("Not implemented") + }, + &mut BatteryMonitor::Disabled { } => { + bail!("Battery monitor is disabled") + } + } + } + fn voltage_milli_volt(&mut self) -> Result { + match self { + BatteryMonitor::BQ34Z100G1 { battery_driver} => { + match battery_driver.voltage() { + OkStd(r) => Ok(r), + Err(err) => bail!("Error reading voltage_milli_volt {:?}", err), + } + }, + BatteryMonitor::WchI2cSlave { .. } => { + bail!("Not implemented") + }, + &mut BatteryMonitor::Disabled { } => { + bail!("Battery monitor is disabled") + } + } + } + fn average_current_milli_ampere(&mut self) -> Result { + match self { + BatteryMonitor::BQ34Z100G1 { battery_driver} => { + match battery_driver.average_current() { + OkStd(r) => Ok(r), + Err(err) => bail!("Error reading average_current_milli_ampere {:?}", err), + } + }, + BatteryMonitor::WchI2cSlave { .. } => { + bail!("Not implemented") + }, + &mut BatteryMonitor::Disabled { } => { + bail!("Battery monitor is disabled") + } + } + } + fn cycle_count(&mut self) -> Result { + match self { + BatteryMonitor::BQ34Z100G1 { battery_driver} => { + match battery_driver.cycle_count() { + OkStd(r) => Ok(r), + Err(err) => bail!("Error reading cycle_count {:?}", err), + } + }, + BatteryMonitor::WchI2cSlave { .. } => { + bail!("Not implemented") + }, + &mut BatteryMonitor::Disabled { } => { + bail!("Battery monitor is disabled") + } + } + } + fn state_health_percent(&mut self) -> Result { + match self { + BatteryMonitor::BQ34Z100G1 { battery_driver} => { + match battery_driver.state_of_health() { + OkStd(r) => Ok(r as u8), + Err(err) => bail!("Error reading state_health_percent {:?}", err), + } + }, + BatteryMonitor::WchI2cSlave { .. } => { + bail!("Not implemented") + }, + &mut BatteryMonitor::Disabled { } => { + bail!("Battery monitor is disabled") + } + } + } + fn bat_temperature(&mut self) -> Result { + match self { + BatteryMonitor::BQ34Z100G1 { battery_driver} => { + match battery_driver.temperature() { + OkStd(r) => Ok(r), + Err(err) => bail!("Error reading bat_temperature {:?}", err), + } + }, + BatteryMonitor::WchI2cSlave { .. } => { + bail!("Not implemented") + }, + &mut BatteryMonitor::Disabled { } => { + bail!("Battery monitor is disabled") + } + } + } +} + +impl BoardInteraction for HAL<'_> { + fn set_charge_indicator(&mut self, charging: bool) { + match &mut self.board_hal { + V3 { shift_register, .. } => { + shift_register.decompose()[CHARGING] + .set_state(charging.into()) + .unwrap(); + } + V4 { .. } => {}, + Initial { .. } => { + + } + } + } + fn deep_sleep(&mut self, duration_in_ms: u64) -> ! { + match &mut self.board_hal { + V3 { shift_register, .. } => { + shift_register.decompose()[AWAKE].set_low().unwrap(); + } + V4 { .. } => {}, + Initial { .. } => { + + } + } + + unsafe { + //if we don't do this here, we might just revert newly flashed firmware + mark_app_valid(); + //allow early wakeup by pressing the boot button + if duration_in_ms == 0 { + esp_restart(); + } else { + //configure gpio 1 to wakeup on low, reused boot button for this + esp_sleep_enable_ext1_wakeup( + 0b10u64, + esp_sleep_ext1_wakeup_mode_t_ESP_EXT1_WAKEUP_ANY_LOW, + ); + esp_deep_sleep(duration_in_ms); + } + }; + } + fn get_backup_info(&mut self) -> Result { + let eeprom = match &mut self.board_hal { + BoardHal::V3 { eeprom, .. } => {eeprom} + BoardHal::V4 { eeprom, .. } => {eeprom }, + &mut plant_hal::BoardHal::Initial { .. } => { + bail!("Board not configured yet") + } + }; + let dummy = BackupHeader { + timestamp: 0, + crc16: 0, + size: 0, + }; + let store = bincode::serialize(&dummy)?.len(); + let mut header_page_buffer = vec![0_u8; store]; + + match eeprom.read_data(0, &mut header_page_buffer) { + OkStd(_) => {} + Err(err) => bail!("Error reading eeprom header {:?}", err), + }; + println!("Raw header is {:?} with size {}", header_page_buffer, store); + let header: BackupHeader = bincode::deserialize(&header_page_buffer)?; + Ok(header) + } + fn get_backup_config(&mut self) -> Result> { + let eeprom = match &mut self.board_hal { + BoardHal::V3 { eeprom, .. } => {eeprom} + BoardHal::V4 { eeprom, .. } => {eeprom } + &mut plant_hal::BoardHal::Initial { .. } => { + bail!("Board not configured yet") + } + }; + + let dummy = BackupHeader { + timestamp: 0, + crc16: 0, + size: 0, + }; + let store = bincode::serialize(&dummy)?.len(); + let mut header_page_buffer = vec![0_u8; store]; + + match eeprom.read_data(0, &mut header_page_buffer) { + OkStd(_) => {} + Err(err) => bail!("Error reading eeprom header {:?}", err), + }; + let header: BackupHeader = bincode::deserialize(&header_page_buffer)?; + + //skip page 0, used by the header + let data_start_address = eeprom.page_size() as u32; + let mut data_buffer = vec![0_u8; header.size]; + match eeprom.read_data(data_start_address, &mut data_buffer) { + OkStd(_) => {} + Err(err) => bail!("Error reading eeprom data {:?}", err), + }; + + let checksum = X25.checksum(&data_buffer); + if checksum != header.crc16 { + bail!( + "Invalid checksum, got {} but expected {}", + checksum, + header.crc16 + ); + } + + Ok(data_buffer) + } + fn backup_config(&mut self, bytes: &[u8]) -> Result<()> { + let time = self.get_rtc_time()?.timestamp_millis(); + let eeprom = match &mut self.board_hal { + V3 { eeprom, .. } => { + eeprom + } + BoardHal::V4 { eeprom, .. } => { eeprom }, + &mut plant_hal::BoardHal::Initial { .. } => { + bail!("Board not configured yet") + } + }; + + let delay = Delay::new_default(); + + let checksum = X25.checksum(bytes); + let page_size = eeprom.page_size(); + + let header = BackupHeader { + crc16: checksum, + timestamp: time, + size: bytes.len(), + }; + + let encoded = bincode::serialize(&header)?; + if encoded.len() > page_size { + bail!( + "Size limit reached header is {}, but firest page is only {}", + encoded.len(), + page_size + ) + } + let as_u8: &[u8] = &encoded; + + match eeprom.write_page(0, as_u8) { + OkStd(_) => {} + Err(err) => bail!("Error writing eeprom {:?}", err), + }; + delay.delay_ms(5); + + let to_write = bytes.chunks(page_size); + + let mut lastiter = 0; + let mut current_page = 1; + for chunk in to_write { + let address = current_page * page_size as u32; + let eeprom = match &mut self.board_hal { + V3 { eeprom, .. } => { + eeprom + } + BoardHal::V4 { eeprom, .. } => { eeprom } + &mut plant_hal::BoardHal::Initial { .. } => { + bail!("Board not configured yet") + } + }; + match eeprom.write_page(address, chunk) { + OkStd(_) => {} + Err(err) => bail!("Error writing eeprom {:?}", err), + }; + current_page += 1; + + let iter = (current_page % 8) as usize; + if iter != lastiter { + for i in 0..PLANT_COUNT { + let _ = self.fault(i, iter == i); + } + lastiter = iter; + } + + //update led here? + delay.delay_ms(5); + } + Ok(()) + } + fn get_battery_state(&mut self) -> String { + let bat = BatteryState { + voltage_milli_volt: to_string(self.battery_monitor.voltage_milli_volt()), + current_milli_ampere: to_string(self.battery_monitor.average_current_milli_ampere()), + cycle_count: to_string(self.battery_monitor.cycle_count()), + design_milli_ampere: to_string(self.battery_monitor.design_milli_ampere_hour()), + remaining_milli_ampere: to_string(self.battery_monitor.remaining_milli_ampere_hour()), + state_of_charge: to_string(self.battery_monitor.state_charge_percent()), + state_of_health: to_string(self.battery_monitor.state_health_percent()), + temperature: to_string(self.battery_monitor.bat_temperature()), + }; + + match serde_json::to_string(&bat) { + std::prelude::rust_2015::Ok(state) => { + state + } + Err(err) => { + format!("{:?}", err).to_owned() + } + } + } + fn is_day(& self) -> bool { + match & self.board_hal { + BoardHal::V3 { solar_is_day, .. } => {solar_is_day.get_level().into()} + BoardHal::V4 { solar_is_day, .. } => {solar_is_day.get_level().into() } + plant_hal::BoardHal::Initial { .. } => { + false + } + } + } //should be multsampled - pub fn water_temperature_c(&mut self) -> Result { + fn water_temperature_c(&mut self) -> Result { let mut delay = Delay::new_default(); - self.one_wire_bus + let one_wire_bus = match &mut self.board_hal { + BoardHal::V3 { one_wire_bus, .. } => one_wire_bus, + BoardHal::V4 { one_wire_bus, .. } => one_wire_bus, + &mut plant_hal::BoardHal::Initial { .. } => { + bail!("Board not configured yet") + } + }; + + 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(); + let first = one_wire_bus.devices(false, &mut delay).next(); if first.is_none() { bail!("Not found any one wire Ds18b20"); } @@ -460,131 +830,173 @@ impl PlantCtrlBoard<'_> { .map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?; water_temp_sensor - .start_temp_measurement(&mut self.one_wire_bus, &mut delay) + .start_temp_measurement(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) + .read_data(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 / 10_f32) } - /// return median tank sensor value in milli volt - pub fn tank_sensor_voltage(&mut self) -> Result { + fn tank_sensor_voltage(&mut self) -> Result { + let (tank_power, tank_channel) = match &mut self.board_hal { + BoardHal::V3 { tank_power, tank_channel, .. } => {(tank_power,tank_channel)} + BoardHal::V4 { tank_power, tank_channel, .. } => {(tank_power,tank_channel) } + &mut plant_hal::BoardHal::Initial { .. } => { + bail!("Board not configured yet") + } + }; + + let delay = Delay::new_default(); - self.tank_power.set_high()?; + tank_power.set_high()?; //let stabilize delay.delay_ms(100); let mut store = [0_u16; TANK_MULTI_SAMPLE]; for multisample in 0..TANK_MULTI_SAMPLE { - let value = self.tank_channel.read()?; + let value = tank_channel.read()?; store[multisample] = value; } - self.tank_power.set_low()?; + tank_power.set_low()?; store.sort(); let median_mv = store[6] as f32 / 1000_f32; Ok(median_mv) } - - pub fn set_low_voltage_in_cycle(&mut self) { + fn set_low_voltage_in_cycle(&mut self) { unsafe { LOW_VOLTAGE_DETECTED = true; } } - - pub fn clear_low_voltage_in_cycle(&mut self) { + fn clear_low_voltage_in_cycle(&mut self) { unsafe { LOW_VOLTAGE_DETECTED = false; } } - - pub fn light(&mut self, enable: bool) -> Result<()> { - unsafe { gpio_hold_dis(self.light.pin()) }; - self.light.set_state(enable.into())?; - unsafe { gpio_hold_en(self.light.pin()) }; - Ok(()) - } - - pub fn pump(&self, plant: usize, enable: bool) -> Result<()> { - let index = match plant { - 0 => PUMP1_BIT, - 1 => PUMP2_BIT, - 2 => PUMP3_BIT, - 3 => PUMP4_BIT, - 4 => PUMP5_BIT, - 5 => PUMP6_BIT, - 6 => PUMP7_BIT, - 7 => PUMP8_BIT, - _ => bail!("Invalid pump {plant}",), + fn light(&mut self, enable: bool) -> Result<()> { + let light = match &mut self.board_hal { + BoardHal::V3 { light, .. } => light, + BoardHal::V4 { light, .. } => light, + &mut plant_hal::BoardHal::Initial { .. } => { + bail!("Board not configured yet") + } }; - //currently infallible error, keep for future as result anyway - self.shift_register.decompose()[index].set_state(enable.into())?; + + unsafe { gpio_hold_dis(light.pin()) }; + light.set_state(enable.into())?; + unsafe { gpio_hold_en(light.pin()) }; Ok(()) } + fn pump(&mut self, plant: usize, enable: bool) -> Result<()> { + match &mut self.board_hal { + V3 { shift_register, .. } => { + let index = match plant { + 0 => PUMP1_BIT, + 1 => PUMP2_BIT, + 2 => PUMP3_BIT, + 3 => PUMP4_BIT, + 4 => PUMP5_BIT, + 5 => PUMP6_BIT, + 6 => PUMP7_BIT, + 7 => PUMP8_BIT, + _ => bail!("Invalid pump {plant}",), + }; + //currently infallible error, keep for future as result anyway + shift_register.decompose()[index].set_state(enable.into())?; + } + V4 { pump_expander, .. } => { + if enable { + pump_expander.pin_set_high(GPIOBank::Bank0, plant.try_into()?)?; + } else { + pump_expander.pin_set_low(GPIOBank::Bank0, plant.try_into()?)?; + } + }, + &mut Initial { .. } => { + bail!("Board not configured yet") + } + } - pub fn last_pump_time(&self, plant: usize) -> Option> { + Ok(()) + } + fn last_pump_time(&self, plant: usize) -> Option> { let ts = unsafe { LAST_WATERING_TIMESTAMP }[plant]; DateTime::from_timestamp_millis(ts) } - - pub fn store_last_pump_time(&mut self, plant: usize, time: DateTime) { + fn store_last_pump_time(&mut self, plant: usize, time: DateTime) { unsafe { LAST_WATERING_TIMESTAMP[plant] = time.timestamp_millis(); } } - - pub fn store_consecutive_pump_count(&mut self, plant: usize, count: u32) { + fn store_consecutive_pump_count(&mut self, plant: usize, count: u32) { unsafe { CONSECUTIVE_WATERING_PLANT[plant] = count; } } - - pub fn consecutive_pump_count(&mut self, plant: usize) -> u32 { + fn consecutive_pump_count(&mut self, plant: usize) -> u32 { unsafe { CONSECUTIVE_WATERING_PLANT[plant] } } + fn fault(&mut self, plant: usize, enable: bool) -> Result<()>{ + match &mut self.board_hal { + V3 { shift_register, .. } => { + let index = match plant { + 0 => FAULT_1, + 1 => FAULT_2, + 2 => FAULT_3, + 3 => FAULT_4, + 4 => FAULT_5, + 5 => FAULT_6, + 6 => FAULT_7, + 7 => FAULT_8, + _ => panic!("Invalid plant id {}", plant), + }; + shift_register.decompose()[index] + .set_state(enable.into())?; + } + V4 { pump_expander, .. } => { + if enable { + pump_expander.pin_set_high(GPIOBank::Bank1, plant.try_into()?)?; + } else { + pump_expander.pin_set_low(GPIOBank::Bank1, plant.try_into()?)?; + } - pub fn fault(&self, plant: usize, enable: bool) { - let index = match plant { - 0 => FAULT_1, - 1 => FAULT_2, - 2 => FAULT_3, - 3 => FAULT_4, - 4 => FAULT_5, - 5 => FAULT_6, - 6 => FAULT_7, - 7 => FAULT_8, - _ => panic!("Invalid plant id {}", plant), - }; - self.shift_register.decompose()[index] - .set_state(enable.into()) - .unwrap() + } + &mut Initial { .. } => { + bail!("Board not configured yet") + } + } + Ok(()) } - - pub fn low_voltage_in_cycle(&mut self) -> bool { + fn low_voltage_in_cycle(&mut self) -> bool { unsafe { LOW_VOLTAGE_DETECTED } } - - pub fn any_pump(&mut self, enable: bool) -> Result<()> { - { - self.main_pump.set_state(enable.into())?; - Ok(()) + fn any_pump(&mut self, enable: bool) -> Result<()> { + match &mut self.board_hal { + V3 { main_pump, .. } => { + main_pump.set_state(enable.into())?; + } + V4 { .. } => { + //does not exist in v4, ignore it + } + &mut plant_hal::BoardHal::Initial { .. } => { + bail!("Board not configured yet") + } } - } + Ok(()) - pub fn time(&mut self) -> Result> { + } + fn time(&mut self) -> Result> { let time = EspSystemTime {}.now().as_millis(); let smaller_time = time as i64; let local_time = DateTime::from_timestamp_millis(smaller_time) .ok_or(anyhow!("could not convert timestamp"))?; Ok(local_time) } - - pub fn sntp(&mut self, max_wait_ms: u32) -> Result> { + 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 { @@ -598,106 +1010,233 @@ impl PlantCtrlBoard<'_> { self.time() } + fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result { + match &mut self.board_hal { + V3{ signal_counter, shift_register, .. } => { + let mut results = [0_f32; REPEAT_MOIST_MEASURE]; + for repeat in 0..REPEAT_MOIST_MEASURE { + signal_counter.counter_pause()?; + signal_counter.counter_clear()?; + //Disable all + shift_register.decompose()[V3Constants::MS_4].set_high()?; - pub fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result { - let sensor_channel = match sensor { - Sensor::A => match plant { - 0 => SENSOR_A_1, - 1 => SENSOR_A_2, - 2 => SENSOR_A_3, - 3 => SENSOR_A_4, - 4 => SENSOR_A_5, - 5 => SENSOR_A_6, - 6 => SENSOR_A_7, - 7 => SENSOR_A_8, - _ => bail!("Invalid plant id {}", plant), + let sensor_channel = match sensor { + Sensor::A => match plant { + 0 => V3Constants::SENSOR_A_1, + 1 => V3Constants::SENSOR_A_2, + 2 => V3Constants::SENSOR_A_3, + 3 => V3Constants::SENSOR_A_4, + 4 => V3Constants::SENSOR_A_5, + 5 => V3Constants::SENSOR_A_6, + 6 => V3Constants::SENSOR_A_7, + 7 => V3Constants::SENSOR_A_8, + _ => bail!("Invalid plant id {}", plant), + }, + Sensor::B => match plant { + 0 => V3Constants::SENSOR_B_1, + 1 => V3Constants::SENSOR_B_2, + 2 => V3Constants::SENSOR_B_3, + 3 => V3Constants::SENSOR_B_4, + 4 => V3Constants::SENSOR_B_5, + 5 => V3Constants::SENSOR_B_6, + 6 => V3Constants::SENSOR_B_7, + 7 => V3Constants::SENSOR_B_8, + _ => bail!("Invalid plant id {}", plant), + }, + }; + + + let is_bit_set = |b: u8| -> bool { sensor_channel & (1 << b) != 0 }; + let pin_0 = &mut shift_register.decompose()[V3Constants::MS_0]; + let pin_1 = &mut shift_register.decompose()[V3Constants::MS_1]; + let pin_2 = &mut shift_register.decompose()[V3Constants::MS_2]; + let pin_3 = &mut shift_register.decompose()[V3Constants::MS_3]; + if is_bit_set(0) { + pin_0.set_high()?; + } else { + pin_0.set_low()?; + } + if is_bit_set(1) { + pin_1.set_high()?; + } else { + pin_1.set_low()?; + } + if is_bit_set(2) { + pin_2.set_high()?; + } else { + pin_2.set_low()?; + } + if is_bit_set(3) { + pin_3.set_high()?; + } else { + pin_3.set_low()?; + } + + shift_register.decompose()[V3Constants::MS_4].set_low()?; + shift_register.decompose()[V3Constants::SENSOR_ON].set_high()?; + + let delay = Delay::new_default(); + 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); + signal_counter.counter_resume()?; + delay.delay_ms(measurement); + signal_counter.counter_pause()?; + shift_register.decompose()[V3Constants::MS_4].set_high()?; + shift_register.decompose()[V3Constants::SENSOR_ON].set_low()?; + delay.delay_ms(10); + let unscaled = signal_counter.get_counter_value()? as i32; + let hz = unscaled as f32 * factor; + log( + LogMessage::RawMeasure, + unscaled as u32, + hz as u32, + &plant.to_string(), + &format!("{sensor:?}"), + ); + results[repeat] = hz; + } + results.sort_by(|a, b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord + + let mid = results.len() / 2; + let median = results[mid]; + Ok(median) }, - Sensor::B => match plant { - 0 => SENSOR_B_1, - 1 => SENSOR_B_2, - 2 => SENSOR_B_3, - 3 => SENSOR_B_4, - 4 => SENSOR_B_5, - 5 => SENSOR_B_6, - 6 => SENSOR_B_7, - 7 => SENSOR_B_8, - _ => bail!("Invalid plant id {}", plant), - }, - }; + V4 {sensor_expander, signal_counter, ..} => { + let mut results = [0_f32; REPEAT_MOIST_MEASURE]; + for repeat in 0..REPEAT_MOIST_MEASURE { + signal_counter.counter_pause()?; + signal_counter.counter_clear()?; - 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()?; - self.sensor_multiplexer(sensor_channel)?; + const MS0: u8 = 1_u8; + const MS1: u8 = 0_u8; + const MS2: u8 = 3_u8; + const MS3: u8 = 4_u8; + const MS4: u8 = 2_u8; + const SENSOR_ON: u8 = 5_u8; + //Disable all + sensor_expander.pin_set_high(GPIOBank::Bank0, MS4)?; - self.shift_register.decompose()[MS_4].set_low()?; - self.shift_register.decompose()[SENSOR_ON].set_high()?; + let sensor_channel = match sensor { + Sensor::A =>{ + plant as u32 + }, + Sensor::B => { + (15 - plant) as u32 + }, + }; - let delay = Delay::new_default(); - 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()?; - 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; - log( - LogMessage::RawMeasure, - unscaled as u32, - hz as u32, - &plant.to_string(), - &format!("{sensor:?}"), - ); - results[repeat] = hz; + + let is_bit_set = |b: u8| -> bool { sensor_channel & (1 << b) != 0 }; + if is_bit_set(0) { + sensor_expander.pin_set_high(GPIOBank::Bank0, MS0)?; + } else { + sensor_expander.pin_set_low(GPIOBank::Bank0, MS0)?; + } + if is_bit_set(1) { + sensor_expander.pin_set_high(GPIOBank::Bank0, MS1)?; + } else { + sensor_expander.pin_set_low(GPIOBank::Bank0, MS1)?; + } + if is_bit_set(2) { + sensor_expander.pin_set_high(GPIOBank::Bank0, MS2)?; + } else { + sensor_expander.pin_set_low(GPIOBank::Bank0, MS2)?; + } + if is_bit_set(3) { + sensor_expander.pin_set_high(GPIOBank::Bank0, MS3)?; + } else { + sensor_expander.pin_set_low(GPIOBank::Bank0, MS3)?; + } + + sensor_expander.pin_set_low(GPIOBank::Bank0, MS4)?; + sensor_expander.pin_set_high(GPIOBank::Bank0, SENSOR_ON)?; + + let delay = Delay::new_default(); + 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); + signal_counter.counter_resume()?; + delay.delay_ms(measurement); + signal_counter.counter_pause()?; + sensor_expander.pin_set_high(GPIOBank::Bank0, MS4)?; + sensor_expander.pin_set_low(GPIOBank::Bank0, SENSOR_ON)?; + sensor_expander.pin_set_low(GPIOBank::Bank0, MS0); + sensor_expander.pin_set_low(GPIOBank::Bank0, MS1); + sensor_expander.pin_set_low(GPIOBank::Bank0, MS2); + sensor_expander.pin_set_low(GPIOBank::Bank0, MS3); + delay.delay_ms(10); + let unscaled = signal_counter.get_counter_value()? as i32; + let hz = unscaled as f32 * factor; + log( + LogMessage::RawMeasure, + unscaled as u32, + hz as u32, + &plant.to_string(), + &format!("{sensor:?}"), + ); + results[repeat] = hz; + } + results.sort_by(|a, b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord + + let mid = results.len() / 2; + let median = results[mid]; + Ok(median) + } + _ => { + bail!("Not implemented for this board") + } } - results.sort_by(|a, b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord - let mid = results.len() / 2; - - Ok(results[mid]) } - - pub fn general_fault(&mut self, enable: bool) { - unsafe { gpio_hold_dis(self.general_fault.pin()) }; - self.general_fault.set_state(enable.into()).unwrap(); - unsafe { gpio_hold_en(self.general_fault.pin()) }; + fn general_fault(&mut self, enable: bool) { + let general_fault = match &mut self.board_hal { + V3 { general_fault, .. } => {general_fault} + V4 { general_fault, .. } => {general_fault}, + Initial { general_fault, .. } => {general_fault} + }; + unsafe { gpio_hold_dis(general_fault.pin()) }; + general_fault.set_state(enable.into()).unwrap(); + unsafe { gpio_hold_en(general_fault.pin()) }; } - - pub fn wifi_ap(&mut self, ap_ssid: Option>) -> Result<()> { - let ssid = - ap_ssid.unwrap_or(heapless::String::from_str("PlantCtrl Emergency Mode").unwrap()); + fn wifi_ap(&mut self) -> Result<()> { + let ssid = match self.board_hal { + Initial { .. } => { + //this mode is only used if no config file is found, or it is unparseable + heapless::String::from_str("PlantCtrl Emergency Mode").unwrap() + }, + _ => { + self.config.network.ap_ssid.clone() + } + }; let apconfig = AccessPointConfiguration { ssid, auth_method: AuthMethod::None, ssid_hidden: false, ..Default::default() }; - self.wifi_driver + self.esp.wifi_driver .set_configuration(&Configuration::AccessPoint(apconfig))?; - self.wifi_driver.start()?; + self.esp.wifi_driver.start()?; Ok(()) } - - pub fn wifi( - &mut self, - ssid: heapless::String<32>, - password: Option>, - max_wait: u32, + fn wifi( + &mut self ) -> Result { + let ssid = self.config.network.ssid.clone().ok_or(anyhow!("No ssid configured"))?; + let password = self.config.network.password.clone(); + let max_wait = self.config.network.max_wait; + 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( + self.esp.wifi_driver.set_configuration(&Configuration::Client( ClientConfiguration { ssid, password: pw, @@ -706,7 +1245,7 @@ impl PlantCtrlBoard<'_> { ))?; } None => { - self.wifi_driver.set_configuration(&Configuration::Client( + self.esp.wifi_driver.set_configuration(&Configuration::Client( ClientConfiguration { ssid, auth_method: AuthMethod::None, @@ -716,128 +1255,83 @@ impl PlantCtrlBoard<'_> { } } - self.wifi_driver.start()?; - self.wifi_driver.connect()?; + self.esp.wifi_driver.start()?; + self.esp.wifi_driver.connect()?; let delay = Delay::new_default(); let mut counter = 0_u32; - while !self.wifi_driver.is_connected()? { + while !self.esp.wifi_driver.is_connected()? { delay.delay_ms(250); counter += 250; if counter > max_wait { //ignore these errors, Wi-Fi will not be used this - self.wifi_driver.disconnect().unwrap_or(()); - self.wifi_driver.stop().unwrap_or(()); + self.esp.wifi_driver.disconnect().unwrap_or(()); + self.esp.wifi_driver.stop().unwrap_or(()); bail!("Did not manage wifi connection within timeout"); } } - println!("Should be connected now"); + println!("Should be connected now, waiting for link to be up"); - while !self.wifi_driver.is_up()? { + while !self.esp.wifi_driver.is_up()? { delay.delay_ms(250); counter += 250; if counter > max_wait { //ignore these errors, Wi-Fi will not be used this - self.wifi_driver.disconnect().unwrap_or(()); - self.wifi_driver.stop().unwrap_or(()); + self.esp.wifi_driver.disconnect().unwrap_or(()); + self.esp.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()?; + let address = self.esp.wifi_driver.sta_netif().get_ip_info()?; log(LogMessage::WifiInfo, 0, 0, "", &format!("{address:?}")); Ok(address) } - - pub 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: 5, - format_if_mount_failed: true, - }; - - //TODO check fielsystem esp_spiffs_check - - unsafe { - esp_idf_sys::esp!(esp_idf_sys::esp_vfs_spiffs_register(&conf))?; - Ok(()) - } + fn mode_override_pressed(&mut self) -> bool { + self.esp.boot_button.get_level() == Level::Low } - - pub 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_spiffs_info( - storage.as_ptr(), - &mut total_size, - &mut used_size - ))?; - } - Ok(FileSystemSizeInfo { - total_size, - used_size, - free_size: total_size - used_size, - }) - } - - pub fn mode_override_pressed(&mut self) -> bool { - self.boot_button.get_level() == Level::Low - } - - pub fn factory_reset(&mut self) -> Result<()> { + fn factory_reset(&mut self) -> Result<()> { println!("factory resetting"); - let config = Path::new(CONFIG_FILE); - if config.exists() { - println!("Removing config"); - fs::remove_file(config)?; - } - + self.esp.delete_config()?; //destroy backup header let dummy: [u8; 0] = []; self.backup_config(&dummy)?; Ok(()) } - - pub fn get_rtc_time(&mut self) -> Result> { - match self.rtc.datetime() { + fn get_rtc_time(&mut self) -> Result> { + let rtc = match &mut self.board_hal { + V3 { rtc, .. } => {rtc} + V4 { rtc, .. } => {rtc}, + Initial { .. } => { + bail!("Board not configured yet") + } + }; + match rtc.datetime() { OkStd(rtc_time) => Ok(rtc_time.and_utc()), Err(err) => { bail!("Error getting rtc time {:?}", err) } } } - - pub fn set_rtc_time(&mut self, time: &DateTime) -> Result<()> { + fn set_rtc_time(&mut self, time: &DateTime) -> Result<()> { + let rtc = match &mut self.board_hal { + V3 { rtc, .. } => {rtc} + V4 { rtc, .. } => {rtc} + Initial { .. } => { + bail!("Board not configured yet") + } + }; let naive_time = time.naive_utc(); - match self.rtc.set_datetime(&naive_time) { + match rtc.set_datetime(&naive_time) { OkStd(_) => Ok(()), Err(err) => { bail!("Error getting rtc time {:?}", err) } } } - - pub fn get_config(&mut self) -> Result { - let cfg = File::open(CONFIG_FILE)?; - let config: PlantControllerConfig = serde_json::from_reader(cfg)?; - Ok(config) - } - - pub fn set_config(&mut self, config: &PlantControllerConfig) -> Result<()> { - let mut cfg = File::create(CONFIG_FILE)?; - serde_json::to_writer(&mut cfg, &config)?; - println!("Wrote config config {:?}", config); - Ok(()) - } - - pub fn wifi_scan(&mut self) -> Result> { - self.wifi_driver.start_scan( + fn wifi_scan(&mut self) -> Result> { + self.esp.wifi_driver.start_scan( &ScanConfig { scan_type: ScanType::Passive(Duration::from_secs(5)), show_hidden: false, @@ -845,10 +1339,9 @@ impl PlantCtrlBoard<'_> { }, true, )?; - Ok(self.wifi_driver.get_scan_result()?) + Ok(self.esp.wifi_driver.get_scan_result()?) } - - pub fn test_pump(&mut self, plant: usize) -> Result<()> { + fn test_pump(&mut self, plant: usize) -> Result<()> { self.any_pump(true)?; self.pump(plant, true)?; unsafe { vTaskDelay(30000) }; @@ -856,8 +1349,7 @@ impl PlantCtrlBoard<'_> { self.any_pump(false)?; Ok(()) } - - pub fn test(&mut self) -> Result<()> { + fn test(&mut self) -> Result<()> { self.general_fault(true); unsafe { vTaskDelay(100) }; self.general_fault(false); @@ -871,9 +1363,9 @@ impl PlantCtrlBoard<'_> { self.light(false)?; unsafe { vTaskDelay(500) }; for i in 0..PLANT_COUNT { - self.fault(i, true); + self.fault(i, true)?; unsafe { vTaskDelay(500) }; - self.fault(i, false); + self.fault(i, false)?; unsafe { vTaskDelay(500) }; } for i in 0..PLANT_COUNT { @@ -898,9 +1390,8 @@ impl PlantCtrlBoard<'_> { Delay::new_default().delay_ms(10); Ok(()) } - - pub fn mqtt(&mut self, config: &PlantControllerConfig) -> Result<()> { - let base_topic = config + fn mqtt(&mut self) -> Result<()> { + let base_topic = self.config .network .base_topic .as_ref() @@ -908,7 +1399,7 @@ impl PlantCtrlBoard<'_> { if base_topic.is_empty() { bail!("Mqtt base_topic was empty") } - let mqtt_url = config + let mqtt_url = self.config .network .mqtt_url .as_ref() @@ -1033,7 +1524,7 @@ impl PlantCtrlBoard<'_> { match round_trip_ok.load(std::sync::atomic::Ordering::Relaxed) { true => { println!("Round trip registered, proceeding"); - self.mqtt_client = Some(client); + self.esp.mqtt_client = Some(client); return Ok(()); } false => { @@ -1055,14 +1546,12 @@ impl PlantCtrlBoard<'_> { } bail!("Mqtt did not fire connection callback in time"); } - - pub fn mqtt_publish( + fn mqtt_publish( &mut self, - config: &PlantControllerConfig, subtopic: &str, message: &[u8], ) -> Result<()> { - if self.mqtt_client.is_none() { + if self.esp.mqtt_client.is_none() { return Ok(()); } if !subtopic.starts_with("/") { @@ -1073,10 +1562,10 @@ impl PlantCtrlBoard<'_> { println!("Subtopic exceeds 192 chars {}", subtopic); bail!("Subtopic exceeds 192 chars {}", subtopic); } - let client = self.mqtt_client.as_mut().unwrap(); + let client = self.esp.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(self.config.network.base_topic.as_ref().unwrap()) .is_err() { println!("Some error assembling full_topic 1"); @@ -1109,120 +1598,78 @@ impl PlantCtrlBoard<'_> { } } } - - pub fn get_restart_to_conf(&mut self) -> bool { + fn get_restart_to_conf(&mut self) -> bool { unsafe { RESTART_TO_CONF } } - - pub fn set_restart_to_conf(&mut self, to_conf: bool) { + fn set_restart_to_conf(&mut self, to_conf: bool) { unsafe { RESTART_TO_CONF = to_conf; } } - - pub fn state_charge_percent(&mut self) -> Result { - match self.battery_driver.state_of_charge() { - OkStd(r) => Ok(r), - Err(err) => bail!("Error reading SoC {:?}", err), - } - } - - pub fn remaining_milli_ampere_hour(&mut self) -> Result { - match self.battery_driver.remaining_capacity() { - OkStd(r) => Ok(r), - Err(err) => bail!("Error reading Remaining Capacity {:?}", err), - } - } - - pub fn max_milli_ampere_hour(&mut self) -> Result { - match self.battery_driver.full_charge_capacity() { - OkStd(r) => Ok(r), - Err(err) => bail!("Error reading Full Charge Capacity {:?}", err), - } - } - - pub fn design_milli_ampere_hour(&mut self) -> Result { - match self.battery_driver.design_capacity() { - OkStd(r) => Ok(r), - Err(err) => bail!("Error reading Design Capacity {:?}", err), - } - } - - pub fn voltage_milli_volt(&mut self) -> Result { - match self.battery_driver.voltage() { - OkStd(r) => Ok(r), - Err(err) => bail!("Error reading voltage {:?}", err), - } - } - - pub fn average_current_milli_ampere(&mut self) -> Result { - match self.battery_driver.average_current() { - OkStd(r) => Ok(r), - Err(err) => bail!("Error reading Average Current {:?}", err), - } - } - - pub fn cycle_count(&mut self) -> Result { - match self.battery_driver.cycle_count() { - OkStd(r) => Ok(r), - Err(err) => bail!("Error reading Cycle Count {:?}", err), - } - } - - pub fn state_health_percent(&mut self) -> Result { - match self.battery_driver.state_of_health() { - OkStd(r) => Ok(r as u8), - Err(err) => bail!("Error reading State of Health {:?}", err), - } - } - - pub fn bat_temperature(&mut self) -> Result { - match self.battery_driver.temperature() { - OkStd(r) => Ok(r), - Err(err) => bail!("Error reading Temperature {:?}", err), - } - } - - pub fn flash_bq34_z100(&mut self, line: &str, dryrun: bool) -> Result<()> { - match self.battery_driver.write_flash_stream_i2c(line, dryrun) { - OkStd(r) => Ok(r), - Err(err) => bail!("Error reading SoC {:?}", err), - } - } - - pub fn sensor_multiplexer(&mut self, n: u8) -> Result<()> { - assert!(n < 16); - let is_bit_set = |b: u8| -> bool { n & (1 << b) != 0 }; - - let pin_0 = &mut self.shift_register.decompose()[MS_0]; - let pin_1 = &mut self.shift_register.decompose()[MS_1]; - let pin_2 = &mut self.shift_register.decompose()[MS_2]; - let pin_3 = &mut self.shift_register.decompose()[MS_3]; - if is_bit_set(0) { - pin_0.set_high()?; - } else { - pin_0.set_low()?; - } - if is_bit_set(1) { - pin_1.set_high()?; - } else { - pin_1.set_low()?; - } - if is_bit_set(2) { - pin_2.set_high()?; - } else { - pin_2.set_low()?; - } - if is_bit_set(3) { - pin_3.set_high()?; - } else { - pin_3.set_low()?; - } - Ok(()) - } } -fn print_battery( +pub trait BatteryInteraction { + fn state_charge_percent(&mut self) -> Result; + fn remaining_milli_ampere_hour(&mut self) -> Result; + fn max_milli_ampere_hour(&mut self) -> Result; + fn design_milli_ampere_hour(&mut self) -> Result; + fn voltage_milli_volt(&mut self) -> Result; + fn average_current_milli_ampere(&mut self) -> Result; + fn cycle_count(&mut self) -> Result; + fn state_health_percent(&mut self) -> Result; + fn bat_temperature(&mut self) -> Result; +} + +pub trait BoardInteraction { + fn set_charge_indicator(&mut self, charging: bool); + fn deep_sleep(&mut self, duration_in_ms: u64) -> !; + fn get_backup_info(&mut self) -> Result; + fn get_backup_config(&mut self) -> Result>; + fn backup_config(&mut self, bytes: &[u8]) -> Result<()>; + fn get_battery_state(&mut self) -> String; + fn is_day(&self) -> bool; + //should be multsampled + fn water_temperature_c(&mut self) -> Result; + /// return median tank sensor value in milli volt + fn tank_sensor_voltage(&mut self) -> Result; + fn set_low_voltage_in_cycle(&mut self); + fn clear_low_voltage_in_cycle(&mut self); + fn light(&mut self, enable: bool) -> Result<()>; + fn pump(&mut self, plant: usize, enable: bool) -> Result<()>; + fn last_pump_time(&self, plant: usize) -> Option>; + fn store_last_pump_time(&mut self, plant: usize, time: DateTime); + fn store_consecutive_pump_count(&mut self, plant: usize, count: u32); + fn consecutive_pump_count(&mut self, plant: usize) -> u32; + fn fault(&mut self, plant: usize, enable: bool) -> Result<()>; + fn low_voltage_in_cycle(&mut self) -> bool; + fn any_pump(&mut self, enable: bool) -> Result<()>; + fn time(&mut self) -> Result>; + fn sntp(&mut self, max_wait_ms: u32) -> Result>; + fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result; + fn general_fault(&mut self, enable: bool); + fn wifi_ap(&mut self) -> Result<()>; + fn wifi( + &mut self, + ) -> Result; + fn mode_override_pressed(&mut self) -> bool; + fn factory_reset(&mut self) -> Result<()>; + fn get_rtc_time(&mut self) -> Result>; + fn set_rtc_time(&mut self, time: &DateTime) -> Result<()>; + fn wifi_scan(&mut self) -> Result>; + fn test_pump(&mut self, plant: usize) -> Result<()>; + fn test(&mut self) -> Result<()>; + fn mqtt(&mut self) -> Result<()>; + fn mqtt_publish( + &mut self, + subtopic: &str, + message: &[u8], + ) -> Result<()>; + fn get_restart_to_conf(&mut self) -> bool; + fn set_restart_to_conf(&mut self, to_conf: bool); + +} + +fn print_battery_bq34z100( battery_driver: &mut Bq34z100g1Driver>, Delay>, ) -> Result<(), Bq34Z100Error> { println!("Try communicating with battery"); @@ -1280,6 +1727,42 @@ fn print_battery( Result::Ok(()) } + +pub struct FreePeripherals { + pub gpio0: Gpio0, + pub gpio1: Gpio1, + pub gpio2: Gpio2, + pub gpio3: Gpio3, + pub gpio4: Gpio4, + pub gpio5: Gpio5, + pub gpio6: Gpio6, + pub gpio7: Gpio7, + pub gpio8: Gpio8, + pub gpio10: Gpio10, + pub gpio11: Gpio11, + pub gpio12: Gpio12, + pub gpio13: Gpio13, + pub gpio14: Gpio14, + pub gpio15: Gpio15, + pub gpio16: Gpio16, + pub gpio17: Gpio17, + pub gpio18: Gpio18, + pub gpio19: Gpio19, + pub gpio20: Gpio20, + pub gpio21: Gpio21, + pub gpio22: Gpio22, + pub gpio23: Gpio23, + pub gpio24: Gpio24, + pub gpio25: Gpio25, + pub gpio26: Gpio26, + pub gpio27: Gpio27, + pub gpio28: Gpio28, + pub gpio29: Gpio29, + pub gpio30: Gpio30, + pub pcnt0: PCNT0, + pub adc1: ADC1, +} + pub static I2C_DRIVER: Lazy>> = Lazy::new(PlantHal::create_i2c); impl PlantHal { fn create_i2c() -> Mutex> { @@ -1297,77 +1780,59 @@ impl PlantHal { Mutex::new(I2cDriver::new(i2c, sda, scl, &config).unwrap()) } - pub fn create() -> Result>> { + + + pub fn create() -> Result>> { let peripherals = Peripherals::take()?; + let sys_loop = EspSystemEventLoop::take()?; + let nvs = EspDefaultNvsPartition::take()?; + let wifi_driver = EspWifi::new(peripherals.modem, sys_loop, Some(nvs))?; - let mut clock = PinDriver::input_output(peripherals.pins.gpio15.downgrade())?; - clock.set_pull(Pull::Floating)?; - let mut latch = PinDriver::input_output(peripherals.pins.gpio3.downgrade())?; - latch.set_pull(Pull::Floating)?; - let mut data = PinDriver::input_output(peripherals.pins.gpio23.downgrade())?; - 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()?; - } - let awake = &mut shift_register.decompose()[AWAKE]; - awake.set_high()?; + let mut boot_button = PinDriver::input(peripherals.pins.gpio9.downgrade())?; + boot_button.set_pull(Pull::Floating)?; - let charging = &mut shift_register.decompose()[CHARGING]; - charging.set_high()?; - - let ms0 = &mut shift_register.decompose()[MS_0]; - ms0.set_low()?; - let ms1 = &mut shift_register.decompose()[MS_1]; - ms1.set_low()?; - let ms2 = &mut shift_register.decompose()[MS_2]; - ms2.set_low()?; - let ms3 = &mut shift_register.decompose()[MS_3]; - ms3.set_low()?; - - let ms4 = &mut shift_register.decompose()[MS_4]; - ms4.set_high()?; - - println!("Init battery driver"); - let mut battery_driver = Bq34z100g1Driver { - i2c: MutexDevice::new(&I2C_DRIVER), - delay: Delay::new(0), - flash_block_data: [0; 32], + let free_pins = FreePeripherals { + adc1: peripherals.adc1, + pcnt0: peripherals.pcnt0, + gpio0: peripherals.pins.gpio0, + gpio1: peripherals.pins.gpio1, + gpio2: peripherals.pins.gpio2, + gpio3: peripherals.pins.gpio3, + gpio4: peripherals.pins.gpio4, + gpio5: peripherals.pins.gpio5, + gpio6: peripherals.pins.gpio6, + gpio7: peripherals.pins.gpio7, + gpio8: peripherals.pins.gpio8, + gpio10: peripherals.pins.gpio10, + gpio11: peripherals.pins.gpio11, + gpio12: peripherals.pins.gpio12, + gpio13: peripherals.pins.gpio13, + gpio14: peripherals.pins.gpio14, + gpio15: peripherals.pins.gpio15, + gpio16: peripherals.pins.gpio16, + gpio17: peripherals.pins.gpio17, + gpio18: peripherals.pins.gpio18, + gpio19: peripherals.pins.gpio19, + gpio20: peripherals.pins.gpio20, + gpio21: peripherals.pins.gpio21, + gpio22: peripherals.pins.gpio22, + gpio23: peripherals.pins.gpio23, + gpio24: peripherals.pins.gpio24, + gpio25: peripherals.pins.gpio25, + gpio26: peripherals.pins.gpio26, + gpio27: peripherals.pins.gpio27, + gpio28: peripherals.pins.gpio28, + gpio29: peripherals.pins.gpio29, + gpio30: peripherals.pins.gpio30, }; - println!("Init rtc driver"); - let mut rtc = Ds323x::new_ds3231(MutexDevice::new(&I2C_DRIVER)); - - println!("Init rtc eeprom driver"); - let mut eeprom = { - Eeprom24x::new_24x32( - MutexDevice::new(&I2C_DRIVER), - SlaveAddr::Alternative(true, true, true), - ) + let mut esp = ESP { + mqtt_client: None, + wifi_driver, + boot_button }; - let mut one_wire_pin = PinDriver::input_output_od(peripherals.pins.gpio18)?; - one_wire_pin.set_pull(Pull::Floating)?; - - let rtc_time = rtc.datetime(); - match rtc_time { - OkStd(tt) => { - println!("Rtc Module reports time at UTC {}", tt); - } - Err(err) => { - println!("Rtc Module could not be read {:?}", err); - } - } - match eeprom.read_byte(0) { - OkStd(byte) => { - println!("Read first byte with status {}", byte); - } - Err(err) => { - println!("Eeprom could not read first byte {:?}", err); - } - } //init,reset rtc memory depending on cause let mut init_rtc_store: bool = false; @@ -1400,57 +1865,145 @@ impl PlantHal { "", &format!("{reasons:?}"), ); - if init_rtc_store { - unsafe { - LAST_WATERING_TIMESTAMP = [0; PLANT_COUNT]; - CONSECUTIVE_WATERING_PLANT = [0; PLANT_COUNT]; - LOW_VOLTAGE_DETECTED = false; - crate::log::init(); - RESTART_TO_CONF = to_config_mode; - }; - } else { - unsafe { - if to_config_mode { - RESTART_TO_CONF = true; + + esp.init_rtc_deepsleep_memory(init_rtc_store, to_config_mode); + let fs_mount_error = esp.mount_file_system().is_err(); + + let config = esp.get_config(); + let hal = match config { + Result::Ok(config) => { + let board_hal : BoardHal = match config.hardware.board { + BoardVersion::INITIAL => { + let mut general_fault = PinDriver::input_output(free_pins.gpio6.downgrade())?; + general_fault.set_pull(Pull::Floating)?; + general_fault.set_low()?; + + if fs_mount_error { + general_fault.set_high()? + } + + Initial { general_fault}}, + BoardVersion::V3 => {PlantHal::create_v3(free_pins)?}, + BoardVersion::V4 => {PlantHal::create_v4(free_pins)?} + }; + + let battery_monitor : BatteryMonitor = match config.hardware.battery { + BatteryBoardVersion::Disabled => { BatteryMonitor::Disabled {}} + BatteryBoardVersion::BQ34Z100G1 => { + let mut battery_driver = Bq34z100g1Driver { + i2c: MutexDevice::new(&I2C_DRIVER), + delay: Delay::new(0), + flash_block_data: [0; 32], + }; + let status = print_battery_bq34z100(&mut battery_driver); + match status { + OkStd(_) => {} + Err(err) => { + log( + LogMessage::BatteryCommunicationError, + 0u32, + 0, + "", + &format!("{err:?})"), + ); + } + } + BatteryMonitor::BQ34Z100G1 { battery_driver } + } + BatteryBoardVersion::WchI2cSlave => { + BatteryMonitor::WchI2cSlave {} + } + }; + + + HAL{ + config, + board_hal, + esp, + battery_monitor } + } + Err(err) => { log( - LogMessage::RestartToConfig, - RESTART_TO_CONF as u32, + LogMessage::ConfigModeMissingConfig, + 0, 0, "", - "", + &err.to_string(), ); - log( - LogMessage::LowVoltage, - LOW_VOLTAGE_DETECTED as u32, - 0, - "", - "", - ); - for i in 0..PLANT_COUNT { - println!( - "LAST_WATERING_TIMESTAMP[{}] = UTC {}", - i, LAST_WATERING_TIMESTAMP[i] - ); - } - for i in 0..PLANT_COUNT { - println!( - "CONSECUTIVE_WATERING_PLANT[{}] = {}", - i, CONSECUTIVE_WATERING_PLANT[i] - ); + + let mut general_fault = PinDriver::input_output(free_pins.gpio6.downgrade())?; + general_fault.set_pull(Pull::Floating)?; + general_fault.set_low()?; + + HAL{ + config: Default::default(), + board_hal: Initial { general_fault}, + esp, + battery_monitor: BatteryMonitor::Disabled {}, } } + }; + + Ok(Mutex::new(hal)) + } + + fn create_v4(peripherals: FreePeripherals) -> Result> { + let mut awake = PinDriver::output(peripherals.gpio15.downgrade())?; + awake.set_high()?; + + let mut general_fault = PinDriver::input_output(peripherals.gpio6.downgrade())?; + general_fault.set_pull(Pull::Floating)?; + general_fault.set_low()?; + + + + println!("Init rtc driver"); + let mut rtc = Ds323x::new_ds3231(MutexDevice::new(&I2C_DRIVER)); + + println!("Init rtc eeprom driver"); + let mut eeprom = { + Eeprom24x::new_24x32( + MutexDevice::new(&I2C_DRIVER), + SlaveAddr::Alternative(true, true, true), + ) + }; + + let mut one_wire_pin = PinDriver::input_output_od(peripherals.gpio18.downgrade())?; + one_wire_pin.set_pull(Pull::Floating)?; + + let one_wire_bus = OneWire::new(one_wire_pin) + .map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?; + + + let rtc_time = rtc.datetime(); + match rtc_time { + OkStd(tt) => { + println!("Rtc Module reports time at UTC {}", tt); + } + Err(err) => { + println!("Rtc Module could not be read {:?}", err); + } + } + match eeprom.read_byte(0) { + OkStd(byte) => { + println!("Read first byte with status {}", byte); + } + Err(err) => { + println!("Eeprom could not read first byte {:?}", err); + } } - let mut counter_unit1 = PcntDriver::new( + + let mut signal_counter = PcntDriver::new( peripherals.pcnt0, - Some(peripherals.pins.gpio22), + Some(peripherals.gpio22), Option::::None, Option::::None, Option::::None, )?; - counter_unit1.channel_config( + signal_counter.channel_config( PcntChannel::Channel0, PinIndex::Pin0, PinIndex::Pin1, @@ -1464,9 +2017,148 @@ impl PlantHal { }, )?; - let sys_loop = EspSystemEventLoop::take()?; - let nvs = EspDefaultNvsPartition::take()?; - let wifi_driver = EspWifi::new(peripherals.modem, sys_loop, Some(nvs))?; + let adc_config = AdcChannelConfig { + attenuation: attenuation::DB_11, + resolution: Resolution::Resolution12Bit, + calibration: esp_idf_hal::adc::oneshot::config::Calibration::Curve, + }; + let tank_driver = AdcDriver::new(peripherals.adc1)?; + let tank_channel: AdcChannelDriver> = + AdcChannelDriver::new(tank_driver, peripherals.gpio5, &adc_config)?; + + let mut solar_is_day = PinDriver::input(peripherals.gpio7.downgrade())?; + solar_is_day.set_pull(Pull::Floating)?; + + let mut light = PinDriver::input_output(peripherals.gpio10.downgrade())?; + light.set_pull(Pull::Floating)?; + + let mut tank_power = PinDriver::input_output(peripherals.gpio11.downgrade())?; + tank_power.set_pull(Pull::Floating)?; + + + let mut pump_expander = Pca9535Immediate::new(MutexDevice::new(&I2C_DRIVER), 32); + + //todo error handing if init error + for pin in 0..8{ + let _ = pump_expander.pin_into_output(GPIOBank::Bank0, pin); + let _ = pump_expander.pin_into_output(GPIOBank::Bank1, pin); + let _ = pump_expander.pin_set_low(GPIOBank::Bank0, pin); + let _ = pump_expander.pin_set_low(GPIOBank::Bank1, pin); + } + + let mut sensor_expander = Pca9535Immediate::new(MutexDevice::new(&I2C_DRIVER), 34); + for pin in 0..8{ + let _ = sensor_expander.pin_into_output(GPIOBank::Bank0, pin); + let _ = sensor_expander.pin_into_output(GPIOBank::Bank1, pin); + let _ = sensor_expander.pin_set_low(GPIOBank::Bank0, pin); + let _ = sensor_expander.pin_set_low(GPIOBank::Bank1, pin); + } + + Ok(V4 { + tank_channel, + solar_is_day, + signal_counter, + light, + tank_power, + one_wire_bus, + rtc, + eeprom, + general_fault, + pump_expander, + sensor_expander + }) + } + + fn create_v3(peripherals: FreePeripherals) -> Result> { + let mut clock = PinDriver::input_output(peripherals.gpio15.downgrade())?; + clock.set_pull(Pull::Floating)?; + let mut latch = PinDriver::input_output(peripherals.gpio3.downgrade())?; + latch.set_pull(Pull::Floating)?; + let mut data = PinDriver::input_output(peripherals.gpio23.downgrade())?; + 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()?; + } + + let awake = &mut shift_register.decompose()[AWAKE]; + awake.set_high()?; + + let charging = &mut shift_register.decompose()[CHARGING]; + charging.set_high()?; + + let ms0 = &mut shift_register.decompose()[V3Constants::MS_0]; + ms0.set_low()?; + let ms1 = &mut shift_register.decompose()[V3Constants::MS_1]; + ms1.set_low()?; + let ms2 = &mut shift_register.decompose()[V3Constants::MS_2]; + ms2.set_low()?; + let ms3 = &mut shift_register.decompose()[V3Constants::MS_3]; + ms3.set_low()?; + + let ms4 = &mut shift_register.decompose()[V3Constants::MS_4]; + ms4.set_high()?; + + println!("Init battery driver"); + + + println!("Init rtc driver"); + let mut rtc = Ds323x::new_ds3231(MutexDevice::new(&I2C_DRIVER)); + + println!("Init rtc eeprom driver"); + let mut eeprom = { + Eeprom24x::new_24x32( + MutexDevice::new(&I2C_DRIVER), + SlaveAddr::Alternative(true, true, true), + ) + }; + + let mut one_wire_pin = PinDriver::input_output_od(peripherals.gpio18.downgrade())?; + one_wire_pin.set_pull(Pull::Floating)?; + + let rtc_time = rtc.datetime(); + match rtc_time { + OkStd(tt) => { + println!("Rtc Module reports time at UTC {}", tt); + } + Err(err) => { + println!("Rtc Module could not be read {:?}", err); + } + } + match eeprom.read_byte(0) { + OkStd(byte) => { + println!("Read first byte with status {}", byte); + } + Err(err) => { + println!("Eeprom could not read first byte {:?}", err); + } + } + + + let mut signal_counter = PcntDriver::new( + peripherals.pcnt0, + Some(peripherals.gpio22), + Option::::None, + Option::::None, + Option::::None, + )?; + + signal_counter.channel_config( + PcntChannel::Channel0, + PinIndex::Pin0, + PinIndex::Pin1, + &PcntChannelConfig { + lctrl_mode: PcntControlMode::Keep, + hctrl_mode: PcntControlMode::Keep, + pos_mode: PcntCountMode::Increment, + neg_mode: PcntCountMode::Hold, + counter_h_lim: i16::MAX, + counter_l_lim: 0, + }, + )?; + + let adc_config = AdcChannelConfig { attenuation: attenuation::DB_11, @@ -1475,70 +2167,50 @@ impl PlantHal { }; let tank_driver = AdcDriver::new(peripherals.adc1)?; let tank_channel: AdcChannelDriver> = - AdcChannelDriver::new(tank_driver, peripherals.pins.gpio5, &adc_config)?; + AdcChannelDriver::new(tank_driver, peripherals.gpio5, &adc_config)?; - let mut solar_is_day = PinDriver::input(peripherals.pins.gpio7.downgrade())?; + let mut solar_is_day = PinDriver::input(peripherals.gpio7.downgrade())?; solar_is_day.set_pull(Pull::Floating)?; - let mut boot_button = PinDriver::input(peripherals.pins.gpio9.downgrade())?; - boot_button.set_pull(Pull::Floating)?; - let mut light = PinDriver::input_output(peripherals.pins.gpio10.downgrade())?; + + let mut light = PinDriver::input_output(peripherals.gpio10.downgrade())?; light.set_pull(Pull::Floating)?; - let mut main_pump = PinDriver::input_output(peripherals.pins.gpio2.downgrade())?; + let mut main_pump = PinDriver::input_output(peripherals.gpio2.downgrade())?; main_pump.set_pull(Pull::Floating)?; main_pump.set_low()?; - let mut tank_power = PinDriver::input_output(peripherals.pins.gpio11.downgrade())?; + let mut tank_power = PinDriver::input_output(peripherals.gpio11.downgrade())?; tank_power.set_pull(Pull::Floating)?; - let mut general_fault = PinDriver::input_output(peripherals.pins.gpio6.downgrade())?; + let mut general_fault = PinDriver::input_output(peripherals.gpio6.downgrade())?; general_fault.set_pull(Pull::Floating)?; general_fault.set_low()?; let one_wire_bus = OneWire::new(one_wire_pin) .map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?; - let status = print_battery(&mut battery_driver); - match status { - OkStd(_) => {} - Err(err) => { - log( - LogMessage::BatteryCommunicationError, - 0u32, - 0, - "", - &format!("{err:?})"), - ); - } - } - let shift_register_enable_invert = PinDriver::output(peripherals.pins.gpio21.downgrade())?; - let rv = Mutex::new(PlantCtrlBoard { + let mut shift_register_enable_invert = PinDriver::output(peripherals.gpio21.downgrade())?; + + + unsafe { gpio_hold_dis(shift_register_enable_invert.pin()) }; + shift_register_enable_invert.set_low()?; + unsafe { gpio_hold_en(shift_register_enable_invert.pin()) }; + + + Ok(BoardHal::V3 { shift_register, shift_register_enable_invert, tank_channel, solar_is_day, - boot_button, light, main_pump, tank_power, general_fault, + signal_counter, one_wire_bus, - signal_counter: counter_unit1, - wifi_driver, - mqtt_client: None, - 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()) }; - true - }); - - Ok(rv) + }) } } diff --git a/rust/src/plant_state.rs b/rust/src/plant_state.rs index 6bae845..e0e67ef 100644 --- a/rust/src/plant_state.rs +++ b/rust/src/plant_state.rs @@ -3,6 +3,7 @@ use chrono_tz::Tz; use serde::{Deserialize, Serialize}; use crate::{config::PlantConfig, in_time_range, plant_hal}; +use crate::plant_hal::BoardInteraction; 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 @@ -113,15 +114,14 @@ fn map_range_moisture( impl PlantState { pub fn read_hardware_state( plant_id: usize, - board: &mut plant_hal::PlantCtrlBoard, - config: &PlantConfig, + board: &mut plant_hal::HAL ) -> Self { - let sensor_a = if config.sensor_a { + let sensor_a = if board.config.plants[plant_id].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, + board.config.plants[plant_id].moisture_sensor_min_frequency, + board.config.plants[plant_id].moisture_sensor_max_frequency, ) { Ok(moisture_percent) => MoistureSensorState::MoistureValue { raw_hz: raw, @@ -137,12 +137,12 @@ impl PlantState { MoistureSensorState::Disabled }; - let sensor_b = if config.sensor_b { + let sensor_b = if board.config.plants[plant_id].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, + board.config.plants[plant_id].moisture_sensor_min_frequency, + board.config.plants[plant_id].moisture_sensor_max_frequency, ) { Ok(moisture_percent) => MoistureSensorState::MoistureValue { raw_hz: raw, @@ -169,7 +169,7 @@ impl PlantState { }, }; if state.is_err() { - board.fault(plant_id, true); + let _ = board.fault(plant_id, true); } state } diff --git a/rust/src/tank.rs b/rust/src/tank.rs index 7c426ef..9217e21 100644 --- a/rust/src/tank.rs +++ b/rust/src/tank.rs @@ -1,9 +1,7 @@ use serde::Serialize; -use crate::{ - config::{PlantControllerConfig, TankConfig}, - plant_hal::PlantCtrlBoard, -}; +use crate::config::TankConfig; +use crate::plant_hal::{BoardInteraction, HAL}; const OPEN_TANK_VOLTAGE: f32 = 3.0; pub const WATER_FROZEN_THRESH: f32 = 4.0; @@ -158,10 +156,9 @@ impl TankState { } pub fn determine_tank_state( - board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, - config: &PlantControllerConfig, + board: &mut std::sync::MutexGuard<'_, HAL<'_>> ) -> TankState { - if config.tank.tank_sensor_enabled { + if board.config.tank.tank_sensor_enabled { match board.tank_sensor_voltage() { Ok(raw_sensor_value_mv) => TankState::Present(raw_sensor_value_mv), Err(err) => TankState::Error(TankError::BoardError(err.to_string())), diff --git a/rust/src/webserver/webserver.rs b/rust/src/webserver/webserver.rs index ea11f34..5d0b8a1 100644 --- a/rust/src/webserver/webserver.rs +++ b/rust/src/webserver/webserver.rs @@ -8,7 +8,6 @@ use anyhow::bail; use chrono::DateTime; use core::result::Result::Ok; use embedded_svc::http::Method; -use esp_idf_hal::delay::Delay; use esp_idf_svc::http::server::{Configuration, EspHttpConnection, EspHttpServer, Request}; use esp_idf_sys::{settimeofday, timeval, vTaskDelay}; use esp_ota::OtaUpdate; @@ -21,6 +20,7 @@ use std::{ use url::Url; use crate::config::PlantControllerConfig; +use crate::plant_hal::BoardInteraction; use crate::plant_state::MoistureSensorState; #[derive(Serialize, Debug)] @@ -114,11 +114,9 @@ fn get_timezones( fn get_live_moisture( _request: &mut Request<&mut EspHttpConnection>, ) -> Result, anyhow::Error> { - let mut board = BOARD_ACCESS.lock().unwrap(); - let config = board.get_config().unwrap(); - + let mut board = BOARD_ACCESS.lock().expect("Should never fail"); let plant_state = Vec::from_iter( - (0..PLANT_COUNT).map(|i| PlantState::read_hardware_state(i, &mut board, &config.plants[i])), + (0..PLANT_COUNT).map(|i| PlantState::read_hardware_state(i, &mut board)), ); let a = Vec::from_iter( plant_state @@ -159,11 +157,8 @@ fn get_live_moisture( fn get_config( _request: &mut Request<&mut EspHttpConnection>, ) -> Result, anyhow::Error> { - let mut board = BOARD_ACCESS.lock().unwrap(); - let json = match board.get_config() { - Ok(config) => serde_json::to_string(&config)?, - Err(_) => serde_json::to_string(&PlantControllerConfig::default())?, - }; + let board = BOARD_ACCESS.lock().expect("Should never fail"); + let json = serde_json::to_string(&board.config)?; anyhow::Ok(Some(json)) } @@ -193,7 +188,7 @@ fn get_backup_config( fn backup_info( _request: &mut Request<&mut EspHttpConnection>, ) -> Result, anyhow::Error> { - let mut board = BOARD_ACCESS.lock().unwrap(); + let mut board = BOARD_ACCESS.lock().expect("Should never fail"); let header = board.get_backup_info(); let json = match header { Ok(h) => { @@ -221,8 +216,11 @@ fn set_config( ) -> Result, anyhow::Error> { let all = read_up_to_bytes_from_request(request, Some(3072))?; let config: PlantControllerConfig = serde_json::from_slice(&all)?; + let mut board = BOARD_ACCESS.lock().unwrap(); - board.set_config(&config)?; + board.esp.set_config(&config)?; + + board.config = config; anyhow::Ok(Some("saved".to_owned())) } @@ -268,12 +266,11 @@ fn tank_info( _request: &mut Request<&mut EspHttpConnection>, ) -> Result, anyhow::Error> { let mut board = BOARD_ACCESS.lock().unwrap(); - let config = board.get_config()?; - let tank_info = determine_tank_state(&mut board, &config); + let tank_info = determine_tank_state(&mut board); //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(&board.config.tank, &water_temp), )?)) } @@ -302,8 +299,8 @@ fn wifi_scan( fn list_files( _request: &mut Request<&mut EspHttpConnection>, ) -> Result, anyhow::Error> { - let board = BOARD_ACCESS.lock().unwrap(); - let result = board.list_files(); + let board = BOARD_ACCESS.lock().expect("It should be possible to lock the board for exclusive fs access"); + let result = board.esp.list_files(); let file_list_json = serde_json::to_string(&result)?; anyhow::Ok(Some(file_list_json)) } @@ -328,7 +325,7 @@ fn ota( let iter = (total_read / 1024) % 8; if iter != lastiter { for i in 0..PLANT_COUNT { - board.fault(i, iter == i); + let _ = board.fault(i, iter == i); } lastiter = iter; } @@ -351,45 +348,6 @@ fn ota( anyhow::Ok(None) } -fn flash_bq(filename: &str, dryrun: bool) -> anyhow::Result<()> { - let mut board = BOARD_ACCESS.lock().unwrap(); - - let mut toggle = true; - let delay = Delay::new(1); - - let file_handle = board.get_file_handle(filename, false)?; - - let mut reader = std::io::BufRead::lines(std::io::BufReader::with_capacity(512, file_handle)); - let mut line = 0; - loop { - board.general_fault(toggle); - toggle = !toggle; - - delay.delay_us(2); - line += 1; - match reader.next() { - Some(next) => { - let input = next?; - println!("flashing bq34z100 dryrun:{dryrun} line {line} payload: {input}"); - match board.flash_bq34_z100(&input, dryrun) { - Ok(_) => { - println!("ok") - } - Err(err) => { - bail!( - "Error flashing bq34z100 in dryrun: {dryrun} line: {line} error: {err}" - ) - } - } - } - None => break, - } - } - println!("Finished flashing file {line} lines processed"); - board.general_fault(false); - anyhow::Ok(()) -} - fn query_param(uri: &str, param_name: &str) -> Option { println!("{uri} get {param_name}"); let parsed = Url::parse(&format!("http://127.0.0.1/{uri}")).unwrap(); @@ -532,6 +490,7 @@ pub fn httpd(reboot_now: Arc) -> Box> { let file_handle = BOARD_ACCESS .lock() .unwrap() + .esp .get_file_handle(&filename, false); match file_handle { Ok(mut file_handle) => { @@ -567,8 +526,8 @@ pub fn httpd(reboot_now: Arc) -> Box> { server .fn_handler("/file", Method::Post, move |mut request| { let filename = query_param(request.uri(), "filename").unwrap(); - let lock = BOARD_ACCESS.lock().unwrap(); - let file_handle = lock.get_file_handle(&filename, true); + let mut lock = BOARD_ACCESS.lock().unwrap(); + let file_handle = lock.esp.get_file_handle(&filename, true); match file_handle { //TODO get free filesystem size, check against during write if not to large Ok(mut file_handle) => { @@ -580,7 +539,7 @@ pub fn httpd(reboot_now: Arc) -> Box> { let iter = (total_read / 1024) % 8; if iter != lastiter { for i in 0..PLANT_COUNT { - lock.fault(i, iter == i); + let _ = lock.fault(i, iter == i); } lastiter = iter; } @@ -612,7 +571,7 @@ pub fn httpd(reboot_now: Arc) -> Box> { let filename = query_param(request.uri(), "filename").unwrap(); let copy = filename.clone(); let board = BOARD_ACCESS.lock().unwrap(); - match board.delete_file(&filename) { + match board.esp.delete_file(&filename) { Ok(_) => { let info = format!("Deleted file {copy}"); cors_response(request, 200, &info)?; @@ -630,35 +589,6 @@ pub fn httpd(reboot_now: Arc) -> Box> { cors_response(request, 200, "") }) .unwrap(); - - server - .fn_handler("/flashbattery", Method::Post, move |request| { - let filename = query_param(request.uri(),"filename").unwrap(); - let dryrun = true; - match flash_bq(&filename, false) { - Ok(_) => { - if !dryrun { - match flash_bq(&filename, true) { - Ok(_) => { - cors_response(request, 200, "Sucessfully flashed bq34z100")?; - }, - Err(err) => { - let info = format!("Could not flash bq34z100, could be bricked now! {filename} {err:?}"); - cors_response(request, 500, &info)?; - }, - } - } else { - cors_response(request, 200, "Sucessfully processed bq34z100")?; - } - }, - Err(err) => { - let info = format!("Could not process firmware file for, bq34z100, refusing to flash! {filename} {err:?}"); - cors_response(request, 500, &info)?; - }, - }; - anyhow::Ok(()) - }) - .unwrap(); unsafe { vTaskDelay(1) }; server .fn_handler("/", Method::Get, move |request| { diff --git a/rust/src_webpack/src/api.ts b/rust/src_webpack/src/api.ts index 531be75..faff2ca 100644 --- a/rust/src_webpack/src/api.ts +++ b/rust/src_webpack/src/api.ts @@ -1,6 +1,6 @@ -interface LogArray extends Array{} +export interface LogArray extends Array{} -interface LogEntry { +export interface LogEntry { timestamp: string, message_id: number, a: number, @@ -9,27 +9,28 @@ interface LogEntry { txt_long: string } -interface LogLocalisation extends Array{} +export interface LogLocalisation extends Array{} -interface LogLocalisationEntry { +export interface LogLocalisationEntry { msg_type: string, message: string } -interface BackupHeader { +export interface BackupHeader { timestamp: string, size: number } -interface NetworkConfig { +export interface NetworkConfig { ap_ssid: string, ssid: string, password: string, mqtt_url: string, - base_topic: string + base_topic: string, + max_wait: number } -interface FileList { +export interface FileList { total: number, used: number, files: FileInfo[], @@ -37,12 +38,12 @@ interface FileList { iter_error: string, } -interface FileInfo{ +export interface FileInfo{ filename: string, size: number, } -interface NightLampConfig { +export interface NightLampConfig { enabled: boolean, night_lamp_hour_start: number, night_lamp_hour_end: number, @@ -51,11 +52,11 @@ interface NightLampConfig { low_soc_restore: number } -interface NightLampCommand { +export interface NightLampCommand { active: boolean } -interface TankConfig { +export interface TankConfig { tank_sensor_enabled: boolean, tank_allow_pumping_if_sensor_error: boolean, tank_useable_ml: number, @@ -64,7 +65,26 @@ interface TankConfig { tank_full_percent: number, } -interface PlantControllerConfig { + +export enum BatteryBoardVersion { + Disabled = "Disabled", + BQ34Z100G1 = "BQ34Z100G1", + WchI2cSlave = "WchI2cSlave" +} +export enum BoardVersion{ + INITIAL = "INITIAL", + V3 = "V3", + V4 = "V4" +} + +export interface BoardHardware { + board: BoardVersion, + battery: BatteryBoardVersion, +} + +export interface PlantControllerConfig { + hardware: BoardHardware, + network: NetworkConfig, tank: TankConfig, night_lamp: NightLampConfig, @@ -72,7 +92,7 @@ interface PlantControllerConfig { timezone?: string, } -interface PlantConfig { +export interface PlantConfig { mode: string, target_moisture: number, pump_time_s: number, @@ -88,35 +108,35 @@ interface PlantConfig { } -interface SSIDList { +export interface SSIDList { ssids: [string] } -interface TestPump { +export interface TestPump { pump: number } -interface SetTime { +export interface SetTime { time: string } -interface GetTime { +export interface GetTime { rtc: string, native: string } -interface Moistures { +export interface Moistures { moisture_a: [string], moisture_b: [string], } -interface VersionInfo { +export interface VersionInfo { git_hash: string, build_time: string, partition: string } -interface BatteryState { +export interface BatteryState { temperature: string voltage_milli_volt: string, current_milli_ampere: string, @@ -127,7 +147,7 @@ interface BatteryState { state_of_health: string } -interface TankInfo { +export interface TankInfo { /// is there enough water in the tank enough_water: boolean, /// warning that water needs to be refilled soon @@ -145,4 +165,4 @@ interface TankInfo { /// water temperature water_temp: number | null, temp_sensor_error: string | null -} \ No newline at end of file +} diff --git a/rust/src_webpack/src/batteryview.ts b/rust/src_webpack/src/batteryview.ts index 5b0b6cf..1dcbaeb 100644 --- a/rust/src_webpack/src/batteryview.ts +++ b/rust/src_webpack/src/batteryview.ts @@ -1,4 +1,5 @@ import { Controller } from "./main"; +import {BatteryState} from "./api"; export class BatteryView{ voltage_milli_volt: HTMLSpanElement; diff --git a/rust/src_webpack/src/fileview.ts b/rust/src_webpack/src/fileview.ts index 2d5e870..4a30f72 100644 --- a/rust/src_webpack/src/fileview.ts +++ b/rust/src_webpack/src/fileview.ts @@ -1,5 +1,5 @@ import {Controller} from "./main"; - +import {FileInfo, FileList} from "./api"; const regex = /[^a-zA-Z0-9_.]/g; function sanitize(str:string){ diff --git a/rust/src_webpack/src/hardware.html b/rust/src_webpack/src/hardware.html new file mode 100644 index 0000000..71a7f32 --- /dev/null +++ b/rust/src_webpack/src/hardware.html @@ -0,0 +1,20 @@ + + +
Hardware:
+
+
BoardRevision
+ +
+
+
BatteryMonitor
+ +
diff --git a/rust/src_webpack/src/hardware.ts b/rust/src_webpack/src/hardware.ts new file mode 100644 index 0000000..ab1819a --- /dev/null +++ b/rust/src_webpack/src/hardware.ts @@ -0,0 +1,45 @@ +import { Controller } from "./main"; +import {BatteryBoardVersion, BoardHardware, BoardVersion} from "./api"; + +export class HardwareConfigView { + private readonly hardware_board_value: HTMLSelectElement; + private readonly hardware_battery_value: HTMLSelectElement; + constructor(controller:Controller){ + (document.getElementById("hardwareview") as HTMLElement).innerHTML = require('./hardware.html') as string; + + this.hardware_board_value = document.getElementById("hardware_board_value") as HTMLSelectElement; + this.hardware_board_value.onchange = controller.configChanged + + Object.keys(BoardVersion).forEach(version => { + let option = document.createElement("option"); + if (version == BoardVersion.INITIAL.toString()){ + option.selected = true + } + option.innerText = version.toString(); + this.hardware_board_value.appendChild(option); + }) + + this.hardware_battery_value = document.getElementById("hardware_battery_value") as HTMLSelectElement; + this.hardware_battery_value.onchange = controller.configChanged + Object.keys(BatteryBoardVersion).forEach(version => { + let option = document.createElement("option"); + if (version == BatteryBoardVersion.Disabled.toString()){ + option.selected = true + } + option.innerText = version.toString(); + this.hardware_battery_value.appendChild(option); + }) + } + + setConfig(hardware: BoardHardware) { + this.hardware_board_value.value = hardware.board.toString() + this.hardware_battery_value.value = hardware.battery.toString() + } + + getConfig(): BoardHardware { + return { + board : BoardVersion[this.hardware_board_value.value as keyof typeof BoardVersion], + battery : BatteryBoardVersion[this.hardware_battery_value.value as keyof typeof BatteryBoardVersion], + } + } + } \ No newline at end of file diff --git a/rust/src_webpack/src/log.ts b/rust/src_webpack/src/log.ts index b8239b6..1e38724 100644 --- a/rust/src_webpack/src/log.ts +++ b/rust/src_webpack/src/log.ts @@ -1,4 +1,5 @@ import { Controller } from "./main"; +import {LogArray, LogLocalisation} from "./api"; export class LogView { private readonly logpanel: HTMLElement; diff --git a/rust/src_webpack/src/main.html b/rust/src_webpack/src/main.html index 8716f63..a706228 100644 --- a/rust/src_webpack/src/main.html +++ b/rust/src_webpack/src/main.html @@ -138,6 +138,10 @@
+
+
+
+
diff --git a/rust/src_webpack/src/main.ts b/rust/src_webpack/src/main.ts index ab3fc44..d34145c 100644 --- a/rust/src_webpack/src/main.ts +++ b/rust/src_webpack/src/main.ts @@ -17,6 +17,19 @@ import { OTAView } from "./ota"; import { BatteryView } from "./batteryview"; import { FileView } from './fileview'; import { LogView } from './log'; +import {HardwareConfigView} from "./hardware"; +import { + BackupHeader, + BatteryState, + GetTime, LogArray, LogLocalisation, + Moistures, + NightLampCommand, + PlantControllerConfig, + SetTime, SSIDList, TankInfo, + TestPump, + VersionInfo, + FileList +} from "./api"; export class Controller { loadTankInfo() : Promise { @@ -66,7 +79,7 @@ export class Controller { } populateTimezones(): Promise { - return fetch('/timezones') + return fetch(PUBLIC_URL+'/timezones') .then(response => response.json()) .then(json => json as string[]) .then(timezones => { @@ -268,6 +281,12 @@ export class Controller { } } + selfTest(){ + fetch(PUBLIC_URL + "/boardtest", { + method: "POST" + }) + } + testNightLamp(active: boolean){ var body: NightLampCommand = { active: active @@ -313,6 +332,7 @@ export class Controller { getConfig(): PlantControllerConfig { return { + hardware: controller.hardwareView.getConfig(), network: controller.networkView.getConfig(), tank: controller.tankView.getConfig(), night_lamp: controller.nightLampView.getConfig(), @@ -360,6 +380,7 @@ export class Controller { this.nightLampView.setConfig(current.night_lamp); this.plantViews.setConfig(current.plants); this.timeView.setTimeZone(current.timezone); + this.hardwareView.setConfig(current.hardware); } measure_moisture() { @@ -437,6 +458,7 @@ export class Controller { readonly timeView: TimeView; readonly plantViews: PlantViews; readonly networkView: NetworkConfigView; + readonly hardwareView: HardwareConfigView; readonly tankView: TankConfigView; readonly nightLampView: NightLampView; readonly submitView: SubmitView; @@ -457,6 +479,7 @@ export class Controller { this.progressview = new ProgressView(this) this.fileview = new FileView(this) this.logView = new LogView(this) + this.hardwareView = new HardwareConfigView(this) this.rebootBtn = document.getElementById("reboot") as HTMLButtonElement this.rebootBtn.onclick = () => { controller.reboot(); @@ -466,6 +489,10 @@ export class Controller { controller.exit(); } } + + selftest() { + + } } const controller = new Controller(); controller.progressview.removeProgress("rebooting"); @@ -505,9 +532,6 @@ executeTasksSequentially().then(r => { controller.progressview.removeProgress("initial") }); - - - controller.progressview.removeProgress("rebooting"); window.addEventListener("beforeunload", (event) => { diff --git a/rust/src_webpack/src/network.html b/rust/src_webpack/src/network.html index 521155f..5bdca8d 100644 --- a/rust/src_webpack/src/network.html +++ b/rust/src_webpack/src/network.html @@ -42,6 +42,11 @@
+ +
+ + +
diff --git a/rust/src_webpack/src/network.ts b/rust/src_webpack/src/network.ts index 8f04063..df1ffbd 100644 --- a/rust/src_webpack/src/network.ts +++ b/rust/src_webpack/src/network.ts @@ -1,4 +1,5 @@ import { Controller } from "./main"; +import {NetworkConfig, SSIDList} from "./api"; export class NetworkConfigView { setScanResult(ssidList: SSIDList) { @@ -14,6 +15,7 @@ export class NetworkConfigView { private readonly password: HTMLInputElement; private readonly mqtt_url: HTMLInputElement; private readonly base_topic: HTMLInputElement; + private readonly max_wait: HTMLInputElement; private readonly ssidlist: HTMLElement; constructor(controller: Controller, publicIp: string) { @@ -28,6 +30,9 @@ export class NetworkConfigView { this.ssid.onchange = controller.configChanged this.password = (document.getElementById("password") as HTMLInputElement); this.password.onchange = controller.configChanged + this.max_wait = (document.getElementById("max_wait") as HTMLInputElement); + this.max_wait.onchange = controller.configChanged + this.mqtt_url = document.getElementById("mqtt_url") as HTMLInputElement; this.mqtt_url.onchange = controller.configChanged this.base_topic = document.getElementById("base_topic") as HTMLInputElement; @@ -47,10 +52,12 @@ export class NetworkConfigView { this.password.value = network.password; this.mqtt_url.value = network.mqtt_url; this.base_topic.value = network.base_topic; + this.max_wait.value = network.max_wait.toString(); } getConfig(): NetworkConfig { return { + max_wait: +this.max_wait.value, ap_ssid: this.ap_ssid.value, ssid: this.ssid.value ?? null, password: this.password.value ?? null, diff --git a/rust/src_webpack/src/nightlightview.ts b/rust/src_webpack/src/nightlightview.ts index d2d6417..f28d723 100644 --- a/rust/src_webpack/src/nightlightview.ts +++ b/rust/src_webpack/src/nightlightview.ts @@ -1,4 +1,5 @@ import { Controller } from "./main"; +import {NightLampConfig} from "./api"; export class NightLampView { private readonly night_lamp_only_when_dark: HTMLInputElement; diff --git a/rust/src_webpack/src/ota.html b/rust/src_webpack/src/ota.html index 32522b9..8584893 100644 --- a/rust/src_webpack/src/ota.html +++ b/rust/src_webpack/src/ota.html @@ -37,5 +37,5 @@
- +
\ No newline at end of file diff --git a/rust/src_webpack/src/ota.ts b/rust/src_webpack/src/ota.ts index 670f9e4..923e5fb 100644 --- a/rust/src_webpack/src/ota.ts +++ b/rust/src_webpack/src/ota.ts @@ -1,4 +1,5 @@ import { Controller } from "./main"; +import {VersionInfo} from "./api"; export class OTAView { readonly file1Upload: HTMLInputElement; @@ -9,6 +10,8 @@ export class OTAView { constructor(controller: Controller) { (document.getElementById("firmwareview") as HTMLElement).innerHTML = require("./ota.html") + let test = document.getElementById("test") as HTMLButtonElement; + this.firmware_buildtime = document.getElementById("firmware_buildtime") as HTMLDivElement; this.firmware_githash = document.getElementById("firmware_githash") as HTMLDivElement; this.firmware_partition = document.getElementById("firmware_partition") as HTMLDivElement; @@ -24,6 +27,10 @@ export class OTAView { } controller.uploadNewFirmware(selectedFile); }; + + test.onclick = () => { + controller.selftest(); + } } setVersion(versionInfo: VersionInfo) { diff --git a/rust/src_webpack/src/plant.ts b/rust/src_webpack/src/plant.ts index d71cf89..d80a120 100644 --- a/rust/src_webpack/src/plant.ts +++ b/rust/src_webpack/src/plant.ts @@ -1,3 +1,5 @@ +import {PlantConfig} from "./api"; + const PLANT_COUNT = 8; diff --git a/rust/src_webpack/src/submitView.ts b/rust/src_webpack/src/submitView.ts index d5ddbb8..e84c0d2 100644 --- a/rust/src_webpack/src/submitView.ts +++ b/rust/src_webpack/src/submitView.ts @@ -1,4 +1,5 @@ import { Controller } from "./main"; +import {BackupHeader} from "./api"; export class SubmitView { json: HTMLDivElement; diff --git a/rust/src_webpack/src/tankview.ts b/rust/src_webpack/src/tankview.ts index f701557..d1f4de8 100644 --- a/rust/src_webpack/src/tankview.ts +++ b/rust/src_webpack/src/tankview.ts @@ -1,4 +1,5 @@ import { Controller } from "./main"; +import {TankConfig, TankInfo} from "./api"; export class TankConfigView { private readonly tank_useable_ml: HTMLInputElement; diff --git a/rust/src_webpack/webpack.config.js b/rust/src_webpack/webpack.config.js index 6386702..00e4ebc 100644 --- a/rust/src_webpack/webpack.config.js +++ b/rust/src_webpack/webpack.config.js @@ -9,7 +9,7 @@ console.log("Dev server is " + isDevServer); var host; if (isDevServer){ //ensure no trailing / - host = 'http://192.168.251.37'; + host = 'http://192.168.71.1'; } else { host = ''; } diff --git a/website/themes/blowfish b/website/themes/blowfish index 26d1205..1d21656 160000 --- a/website/themes/blowfish +++ b/website/themes/blowfish @@ -1 +1 @@ -Subproject commit 26d1205439b460bee960fd4c29f3c5c20948875f +Subproject commit 1d21656d5efcf6a6b247245d057bf553f3209f39