From e2cce8839058c0f9ade13be93c51c5b86ec321cb Mon Sep 17 00:00:00 2001 From: Empire Date: Thu, 19 Jun 2025 13:05:47 +0200 Subject: [PATCH] combine all branches --- rust/src/config.rs | 12 +- rust/src/hal/battery.rs | 274 ++++++++ rust/src/hal/esp.rs | 533 +++++++++++++++ rust/src/{plant_hal.rs => hal/mod.rs} | 940 +++----------------------- rust/src/main.rs | 66 +- rust/src/plant_state.rs | 10 +- rust/src/tank.rs | 2 +- rust/src/webserver/webserver.rs | 9 +- website/themes/blowfish | 2 +- 9 files changed, 939 insertions(+), 909 deletions(-) create mode 100644 rust/src/hal/battery.rs create mode 100644 rust/src/hal/esp.rs rename rust/src/{plant_hal.rs => hal/mod.rs} (60%) diff --git a/rust/src/config.rs b/rust/src/config.rs index fcdcffd..519be87 100644 --- a/rust/src/config.rs +++ b/rust/src/config.rs @@ -1,11 +1,7 @@ use serde::{Deserialize, Serialize}; use std::str::FromStr; - -use chrono_tz::{Europe::Berlin, Tz}; -use std::str::FromStr; - +use crate::hal::PLANT_COUNT; use crate::plant_state::PlantWateringMode; -use crate::PLANT_COUNT; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[serde(default)] @@ -15,8 +11,7 @@ pub struct NetworkConfig { pub password: Option>, pub mqtt_url: Option>, pub base_topic: Option>, - pub max_wait: u32, - pub timezone: heapless::String<64>, + pub max_wait: u32 } impl Default for NetworkConfig { fn default() -> Self { @@ -26,8 +21,7 @@ impl Default for NetworkConfig { password: None, mqtt_url: None, base_topic: None, - max_wait: 10000, - timezone: heapless::String::from_str("Europe/Berlin").unwrap(), + max_wait: 10000 } } } diff --git a/rust/src/hal/battery.rs b/rust/src/hal/battery.rs new file mode 100644 index 0000000..42619f1 --- /dev/null +++ b/rust/src/hal/battery.rs @@ -0,0 +1,274 @@ +use anyhow::bail; +use bq34z100::{Bq34Z100Error, Bq34z100g1, Bq34z100g1Driver}; +use embedded_hal_bus::i2c::MutexDevice; +use esp_idf_hal::delay::Delay; +use esp_idf_hal::i2c::{I2cDriver, I2cError}; +use measurements::Temperature; +use serde::Serialize; +use std::result::Result::Ok as OkStd; +use crate::to_string; + +pub trait BatteryInteraction { + fn state_charge_percent(&mut self) -> anyhow::Result; + fn remaining_milli_ampere_hour(&mut self) -> anyhow::Result; + fn max_milli_ampere_hour(&mut self) -> anyhow::Result; + fn design_milli_ampere_hour(&mut self) -> anyhow::Result; + fn voltage_milli_volt(&mut self) -> anyhow::Result; + fn average_current_milli_ampere(&mut self) -> anyhow::Result; + fn cycle_count(&mut self) -> anyhow::Result; + fn state_health_percent(&mut self) -> anyhow::Result; + fn bat_temperature(&mut self) -> anyhow::Result; + fn get_battery_state(&mut self) -> String; +} +#[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, +} +pub enum BatteryMonitor<'a> { + Disabled { + + }, + BQ34Z100G1 { + battery_driver: Bq34z100g1Driver>, Delay> + }, + WchI2cSlave { + + } +} + + + +impl BatteryInteraction for BatteryMonitor<'_> { + fn state_charge_percent(&mut self) -> anyhow::Result { + match self { + BatteryMonitor::BQ34Z100G1 { battery_driver} => { + match battery_driver.state_of_charge() { + OkStd(r) => anyhow::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) -> anyhow::Result { + match self { + BatteryMonitor::BQ34Z100G1 { battery_driver} => { + match battery_driver.remaining_capacity(){ + OkStd(r) => anyhow::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) -> anyhow::Result { + match self { + BatteryMonitor::BQ34Z100G1 { battery_driver} => { + match battery_driver.full_charge_capacity() { + OkStd(r) => anyhow::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) -> anyhow::Result { + match self { + BatteryMonitor::BQ34Z100G1 { battery_driver} => { + match battery_driver.design_capacity() { + OkStd(r) => anyhow::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) -> anyhow::Result { + match self { + BatteryMonitor::BQ34Z100G1 { battery_driver} => { + match battery_driver.voltage() { + OkStd(r) => anyhow::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) -> anyhow::Result { + match self { + BatteryMonitor::BQ34Z100G1 { battery_driver} => { + match battery_driver.average_current() { + OkStd(r) => anyhow::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) -> anyhow::Result { + match self { + BatteryMonitor::BQ34Z100G1 { battery_driver} => { + match battery_driver.cycle_count() { + OkStd(r) => anyhow::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) -> anyhow::Result { + match self { + BatteryMonitor::BQ34Z100G1 { battery_driver} => { + match battery_driver.state_of_health() { + OkStd(r) => anyhow::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) -> anyhow::Result { + match self { + BatteryMonitor::BQ34Z100G1 { battery_driver} => { + match battery_driver.temperature() { + OkStd(r) => anyhow::Ok(r), + Err(err) => bail!("Error reading bat_temperature {:?}", err), + } + }, + BatteryMonitor::WchI2cSlave { .. } => { + bail!("Not implemented") + }, + &mut BatteryMonitor::Disabled { } => { + bail!("Battery monitor is disabled") + } + } + } + + fn get_battery_state(&mut self) -> String { + let bat = 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()), + }; + + match serde_json::to_string(&bat) { + Ok(state) => { + state + } + Err(err) => { + format!("{:?}", err).to_owned() + } + } + } +} + +pub fn print_battery_bq34z100( + battery_driver: &mut Bq34z100g1Driver>, Delay>, +) -> anyhow::Result<(), Bq34Z100Error> { + println!("Try communicating with battery"); + let fwversion = battery_driver.fw_version().unwrap_or_else(|e| { + println!("Firmware {:?}", e); + 0 + }); + println!("fw version is {}", fwversion); + + let design_capacity = battery_driver.design_capacity().unwrap_or_else(|e| { + println!("Design capacity {:?}", e); + 0 + }); + println!("Design Capacity {}", design_capacity); + if design_capacity == 1000 { + println!("Still stock configuring battery, readouts are likely to be wrong!"); + } + + let flags = battery_driver.get_flags_decoded()?; + println!("Flags {:?}", flags); + + let chem_id = battery_driver.chem_id().unwrap_or_else(|e| { + println!("Chemid {:?}", e); + 0 + }); + + let bat_temp = battery_driver.internal_temperature().unwrap_or_else(|e| { + println!("Bat Temp {:?}", e); + 0 + }); + let temp_c = Temperature::from_kelvin(bat_temp as f64 / 10_f64).as_celsius(); + let voltage = battery_driver.voltage().unwrap_or_else(|e| { + println!("Bat volt {:?}", e); + 0 + }); + let current = battery_driver.current().unwrap_or_else(|e| { + println!("Bat current {:?}", e); + 0 + }); + let state = battery_driver.state_of_charge().unwrap_or_else(|e| { + println!("Bat Soc {:?}", e); + 0 + }); + let charge_voltage = battery_driver.charge_voltage().unwrap_or_else(|e| { + println!("Bat Charge Volt {:?}", e); + 0 + }); + let charge_current = battery_driver.charge_current().unwrap_or_else(|e| { + println!("Bat Charge Current {:?}", e); + 0 + }); + println!("ChemId: {} Current voltage {} and current {} with charge {}% and temp {} CVolt: {} CCur {}", chem_id, voltage, current, state, temp_c, charge_voltage, charge_current); + let _ = battery_driver.unsealed(); + let _ = battery_driver.it_enable(); + anyhow::Result::Ok(()) +} diff --git a/rust/src/hal/esp.rs b/rust/src/hal/esp.rs new file mode 100644 index 0000000..e9f14f4 --- /dev/null +++ b/rust/src/hal/esp.rs @@ -0,0 +1,533 @@ +use crate::config::{NetworkConfig, PlantControllerConfig}; +use crate::hal::{CONSECUTIVE_WATERING_PLANT, LAST_WATERING_TIMESTAMP, LOW_VOLTAGE_DETECTED, PLANT_COUNT, RESTART_TO_CONF}; +use crate::log::{log, LogMessage}; +use anyhow::{anyhow, bail, Context}; +use embedded_svc::ipv4::IpInfo; +use embedded_svc::wifi::{AccessPointConfiguration, AuthMethod, ClientConfiguration, Configuration}; +use esp_idf_hal::delay::Delay; +use esp_idf_hal::gpio::PinDriver; +use esp_idf_svc::mqtt::client::{EspMqttClient, LwtConfiguration, MqttClientConfiguration}; +use esp_idf_svc::wifi::EspWifi; +use esp_idf_sys::{esp_spiffs_info, vTaskDelay}; +use std::ffi::CString; +use std::fs; +use std::fs::File; +use std::path::Path; +use std::result::Result::Ok as OkStd; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; +use std::sync::atomic::AtomicBool; +use std::time::Duration; +use embedded_svc::mqtt::client::QoS::{AtLeastOnce, ExactlyOnce}; +use esp_idf_hal::i2c::I2cDriver; +use serde::Serialize; +use crate::STAY_ALIVE; + +#[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, +} + +pub struct FileSystemSizeInfo { + pub total_size: usize, + pub used_size: usize, + pub free_size: usize, +} + +pub struct MqttClient<'a> { + mqtt_client: EspMqttClient<'a>, + base_topic: heapless::String<64> +} +pub struct ESP<'a> { + pub(crate) mqtt_client: Option>, + pub(crate) wifi_driver: EspWifi<'a>, + pub(crate) boot_button: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, esp_idf_hal::gpio::Input>, + pub(crate) delay: Delay +} + + +impl ESP<'_> { + const SPIFFS_PARTITION_NAME: &'static str = "storage"; + const CONFIG_FILE: &'static str = "/spiffs/config.cfg"; + const BASE_PATH: &'static str = "/spiffs"; + + pub(crate) fn wifi_ap(&mut self) -> anyhow::Result<()> { + let ssid = match self.get_config(){ + Ok(config) => { + config.network.ap_ssid.clone() + } + Err(_) => { + heapless::String::from_str("PlantCtrl Emergency Mode").unwrap() + } + }; + + let apconfig = AccessPointConfiguration { + ssid, + auth_method: AuthMethod::None, + ssid_hidden: false, + ..Default::default() + }; + self.wifi_driver.set_configuration(&Configuration::AccessPoint(apconfig))?; + self.wifi_driver.start()?; + anyhow::Ok(()) + } + + + pub(crate) fn wifi( + &mut self, + network_config: &NetworkConfig + ) -> anyhow::Result { + let ssid = network_config.ssid.clone().ok_or(anyhow!("No ssid configured"))?; + let password = network_config.password.clone(); + let max_wait = network_config.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( + ClientConfiguration { + ssid, + password: pw, + ..Default::default() + }, + ))?; + } + None => { + self.wifi_driver.set_configuration(&Configuration::Client( + ClientConfiguration { + ssid, + auth_method: AuthMethod::None, + ..Default::default() + }, + ))?; + } + } + + self.wifi_driver.start()?; + self.wifi_driver.connect()?; + + let delay = Delay::new_default(); + let mut counter = 0_u32; + while !self.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(()); + bail!("Did not manage wifi connection within timeout"); + } + } + println!("Should be connected now, waiting for link to be up"); + + while !self.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(()); + bail!("Did not manage wifi connection within timeout"); + } + } + //update freertos registers ;) + let address = self.wifi_driver.sta_netif().get_ip_info()?; + log(LogMessage::WifiInfo, 0, 0, "", &format!("{address:?}")); + anyhow::Ok(address) + } + pub(crate) fn get_config(&mut self) -> anyhow::Result { + let cfg = File::open(Self::CONFIG_FILE)?; + let config: PlantControllerConfig = serde_json::from_reader(cfg)?; + anyhow::Ok(config) + } + pub(crate) fn set_config(&mut self, config: &PlantControllerConfig) -> anyhow::Result<()> { + let mut cfg = File::create(Self::CONFIG_FILE)?; + serde_json::to_writer(&mut cfg, &config)?; + println!("Wrote config config {:?}", config); + anyhow::Ok(()) + } + pub(crate) fn delete_config(&self) -> anyhow::Result<()> { + let config = Path::new(Self::CONFIG_FILE); + if config.exists() { + println!("Removing config"); + fs::remove_file(config)? + } + anyhow::Ok(()) + } + pub(crate) fn mount_file_system(&mut self) -> anyhow::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, + }; + + 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(), + "", + ); + anyhow::Ok(()) + } + fn file_system_size(&mut self) -> anyhow::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 + ))?; + } + anyhow::Ok(FileSystemSizeInfo { + total_size, + used_size, + free_size: total_size - used_size, + }) + } + + + 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(Self::BASE_PATH); + let read_dir = fs::read_dir(filepath); + match read_dir { + OkStd(read_dir) => { + for item in read_dir { + match item { + OkStd(file) => { + let f = FileInfo { + filename: file.file_name().into_string().unwrap(), + size: file + .metadata() + .and_then(|it| anyhow::Result::Ok(it.len())) + .unwrap_or_default() + as usize, + }; + result.push(f); + } + Err(err) => { + iter_error = Some(format!("{err:?}")); + break; + } + } + } + } + Err(err) => { + file_system_corrupt = Some(format!("{err:?}")); + } + } + let mut total: usize = 0; + let mut used: usize = 0; + unsafe { + esp_spiffs_info(storage.as_ptr(), &mut total, &mut used); + } + + FileList { + total, + used, + file_system_corrupt, + files: result, + iter_error, + } + } + pub(crate) fn delete_file(&self, filename: &str) -> anyhow::Result<()> { + let filepath = Path::new(Self::BASE_PATH).join(Path::new(filename)); + match fs::remove_file(filepath) { + OkStd(_) => anyhow::Ok(()), + Err(err) => { + bail!(format!("{err:?}")) + } + } + } + pub(crate) fn get_file_handle(&self, filename: &str, write: bool) -> anyhow::Result { + let filepath = Path::new(Self::BASE_PATH).join(Path::new(filename)); + anyhow::Ok(if write { + File::create(filepath)? + } else { + File::open(filepath)? + }) + } + + pub(crate) 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(crate) fn mqtt(&mut self, network_config: &NetworkConfig) -> anyhow::Result<()> { + let base_topic = network_config + .base_topic + .as_ref() + .context("missing base topic")?; + if base_topic.is_empty() { + bail!("Mqtt base_topic was empty") + } + let base_topic_copy = base_topic.clone(); + let mqtt_url = network_config + .mqtt_url + .as_ref() + .context("missing mqtt url")?; + if mqtt_url.is_empty() { + bail!("Mqtt url was empty") + } + + let last_will_topic = format!("{}/state", base_topic); + let mqtt_client_config = MqttClientConfiguration { + lwt: Some(LwtConfiguration { + topic: &last_will_topic, + payload: "lost".as_bytes(), + qos: AtLeastOnce, + retain: true, + }), + client_id: Some("plantctrl"), + keep_alive_interval: Some(Duration::from_secs(60 * 60 * 2)), + //room for improvement + ..Default::default() + }; + + let mqtt_connected_event_received = Arc::new(AtomicBool::new(false)); + let mqtt_connected_event_ok = Arc::new(AtomicBool::new(false)); + + let round_trip_ok = Arc::new(AtomicBool::new(false)); + let round_trip_topic = format!("{}/internal/roundtrip", base_topic); + let stay_alive_topic = format!("{}/stay_alive", base_topic); + log(LogMessage::StayAlive, 0, 0, "", &stay_alive_topic); + + let mqtt_connected_event_received_copy = mqtt_connected_event_received.clone(); + let mqtt_connected_event_ok_copy = mqtt_connected_event_ok.clone(); + let stay_alive_topic_copy = stay_alive_topic.clone(); + let round_trip_topic_copy = round_trip_topic.clone(); + let round_trip_ok_copy = round_trip_ok.clone(); + let client_id = mqtt_client_config.client_id.unwrap_or("not set"); + log(LogMessage::MqttInfo, 0, 0, client_id, mqtt_url); + let mut client = EspMqttClient::new_cb(mqtt_url, &mqtt_client_config, move |event| { + let payload = event.payload(); + match payload { + embedded_svc::mqtt::client::EventPayload::Received { + id: _, + topic, + data, + details: _, + } => { + let data = String::from_utf8_lossy(data); + if let Some(topic) = topic { + //todo use enums + if topic.eq(round_trip_topic_copy.as_str()) { + round_trip_ok_copy.store(true, std::sync::atomic::Ordering::Relaxed); + } else if topic.eq(stay_alive_topic_copy.as_str()) { + let value = + data.eq_ignore_ascii_case("true") || data.eq_ignore_ascii_case("1"); + log(LogMessage::MqttStayAliveRec, 0, 0, &data, ""); + STAY_ALIVE.store(value, std::sync::atomic::Ordering::Relaxed); + } else { + log(LogMessage::UnknownTopic, 0, 0, "", topic); + } + } + } + esp_idf_svc::mqtt::client::EventPayload::Connected(_) => { + mqtt_connected_event_received_copy + .store(true, std::sync::atomic::Ordering::Relaxed); + mqtt_connected_event_ok_copy.store(true, std::sync::atomic::Ordering::Relaxed); + println!("Mqtt connected"); + } + esp_idf_svc::mqtt::client::EventPayload::Disconnected => { + mqtt_connected_event_received_copy + .store(true, std::sync::atomic::Ordering::Relaxed); + mqtt_connected_event_ok_copy.store(false, std::sync::atomic::Ordering::Relaxed); + println!("Mqtt disconnected"); + } + esp_idf_svc::mqtt::client::EventPayload::Error(esp_error) => { + println!("EspMqttError reported {:?}", esp_error); + mqtt_connected_event_received_copy + .store(true, std::sync::atomic::Ordering::Relaxed); + mqtt_connected_event_ok_copy.store(false, std::sync::atomic::Ordering::Relaxed); + println!("Mqtt error"); + } + esp_idf_svc::mqtt::client::EventPayload::BeforeConnect => { + println!("Mqtt before connect") + } + esp_idf_svc::mqtt::client::EventPayload::Subscribed(_) => { + println!("Mqtt subscribed") + } + esp_idf_svc::mqtt::client::EventPayload::Unsubscribed(_) => { + println!("Mqtt unsubscribed") + } + esp_idf_svc::mqtt::client::EventPayload::Published(_) => { + println!("Mqtt published") + } + esp_idf_svc::mqtt::client::EventPayload::Deleted(_) => { + println!("Mqtt deleted") + } + } + })?; + + let mut wait_for_connections_event = 0; + while wait_for_connections_event < 100 { + wait_for_connections_event += 1; + match mqtt_connected_event_received.load(std::sync::atomic::Ordering::Relaxed) { + true => { + println!("Mqtt connection callback received, progressing"); + match mqtt_connected_event_ok.load(std::sync::atomic::Ordering::Relaxed) { + true => { + println!("Mqtt did callback as connected, testing with roundtrip now"); + //subscribe to roundtrip + client.subscribe(round_trip_topic.as_str(), ExactlyOnce)?; + client.subscribe(stay_alive_topic.as_str(), ExactlyOnce)?; + //publish to roundtrip + client.publish( + round_trip_topic.as_str(), + ExactlyOnce, + false, + "online_test".as_bytes(), + )?; + + let mut wait_for_roundtrip = 0; + while wait_for_roundtrip < 100 { + wait_for_roundtrip += 1; + match round_trip_ok.load(std::sync::atomic::Ordering::Relaxed) { + true => { + println!("Round trip registered, proceeding"); + self.mqtt_client = Some(MqttClient{ + mqtt_client: client, + base_topic: base_topic_copy + }); + return anyhow::Ok(()); + } + false => { + unsafe { vTaskDelay(10) }; + } + } + } + bail!("Mqtt did not complete roundtrip in time"); + } + false => { + bail!("Mqtt did respond but with failure") + } + } + } + false => { + unsafe { vTaskDelay(10) }; + } + } + } + bail!("Mqtt did not fire connection callback in time"); + } + pub(crate) fn mqtt_publish( + &mut self, + subtopic: &str, + message: &[u8], + ) -> anyhow::Result<()> { + if self.mqtt_client.is_none() { + return anyhow::Ok(()); + } + if !subtopic.starts_with("/") { + println!("Subtopic without / at start {}", subtopic); + bail!("Subtopic without / at start {}", subtopic); + } + if subtopic.len() > 192 { + println!("Subtopic exceeds 192 chars {}", subtopic); + bail!("Subtopic exceeds 192 chars {}", subtopic); + } + let client = self.mqtt_client.as_mut().unwrap(); + let mut full_topic: heapless::String<256> = heapless::String::new(); + if full_topic + .push_str(client.base_topic.as_str()) + .is_err() + { + println!("Some error assembling full_topic 1"); + bail!("Some error assembling full_topic 1") + }; + if full_topic.push_str(subtopic).is_err() { + println!("Some error assembling full_topic 2"); + bail!("Some error assembling full_topic 2") + }; + let publish = client.mqtt_client.publish(&full_topic, ExactlyOnce, true, message); + Delay::new(10).delay_ms(50); + match publish { + OkStd(message_id) => { + println!( + "Published mqtt topic {} with message {:#?} msgid is {:?}", + full_topic, + String::from_utf8_lossy(message), + message_id + ); + anyhow::Ok(()) + } + Err(err) => { + println!( + "Error during mqtt send on topic {} with message {:#?} error is {:?}", + full_topic, + String::from_utf8_lossy(message), + err + ); + Err(err)? + } + } + } +} \ No newline at end of file diff --git a/rust/src/plant_hal.rs b/rust/src/hal/mod.rs similarity index 60% rename from rust/src/plant_hal.rs rename to rust/src/hal/mod.rs index c1201c5..d1ec766 100644 --- a/rust/src/plant_hal.rs +++ b/rust/src/hal/mod.rs @@ -1,4 +1,7 @@ -use bq34z100::{Bq34Z100Error, Bq34z100g1, Bq34z100g1Driver}; +mod esp; +pub(crate) mod battery; + +use bq34z100::{Bq34z100g1Driver}; use crate::log::LogMessage; use ds323x::{DateTimeAccess, Ds323x}; @@ -6,50 +9,43 @@ use esp_ota::mark_app_valid; use eeprom24x::{Eeprom24x, Eeprom24xTrait, SlaveAddr}; use embedded_hal_bus::i2c::MutexDevice; -use embedded_svc::wifi::{ - AccessPointConfiguration, AccessPointInfo, AuthMethod, ClientConfiguration, Configuration, -}; +use embedded_svc::wifi::AccessPointInfo; use esp_idf_hal::adc::oneshot::config::AdcChannelConfig; use esp_idf_hal::adc::oneshot::{AdcChannelDriver, AdcDriver}; use esp_idf_hal::adc::{attenuation, Resolution, ADC1}; -use esp_idf_hal::i2c::{APBTickType, I2cConfig, I2cDriver, I2cError}; +use esp_idf_hal::i2c::{APBTickType, I2cConfig, I2cDriver}; use esp_idf_hal::units::FromValueType; use esp_idf_svc::eventloop::EspSystemEventLoop; -use esp_idf_svc::ipv4::IpInfo; -use esp_idf_svc::mqtt::client::QoS::AtLeastOnce; -use esp_idf_svc::mqtt::client::QoS::ExactlyOnce; -use esp_idf_svc::mqtt::client::{EspMqttClient, LwtConfiguration, MqttClientConfiguration}; use esp_idf_svc::nvs::EspDefaultNvsPartition; use esp_idf_svc::wifi::config::{ScanConfig, ScanType}; use esp_idf_svc::wifi::EspWifi; use esp_idf_sys::esp_restart; use esp_idf_sys::{ esp_deep_sleep, esp_sleep_enable_ext1_wakeup, - esp_sleep_ext1_wakeup_mode_t_ESP_EXT1_WAKEUP_ANY_LOW, esp_spiffs_info, + esp_sleep_ext1_wakeup_mode_t_ESP_EXT1_WAKEUP_ANY_LOW, }; -use measurements::Temperature; use once_cell::sync::Lazy; use plant_ctrl2::sipo::ShiftRegister40; -use anyhow::{anyhow, Context}; +use anyhow::{anyhow}; use anyhow::{bail, Ok, Result}; use serde::{Deserialize, Serialize}; -use std::ffi::CString; -use std::fs::{self, File}; -use std::path::Path; use chrono::{DateTime, Utc}; use ds18b20::Ds18b20; use std::result::Result::Ok as OkStd; -use std::str::FromStr; -use std::sync::atomic::AtomicBool; -use std::sync::{Arc, Mutex}; +use std::sync::Mutex; use std::time::Duration; +use crate::config::{BatteryBoardVersion, BoardVersion, PlantControllerConfig}; +use crate::hal::battery::{print_battery_bq34z100, BatteryMonitor}; +use crate::hal::esp::ESP; +use crate::hal::BoardHal::{Initial, V3, V4}; +use crate::log::log; use embedded_hal::digital::OutputPin; use esp_idf_hal::delay::Delay; -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::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, Output, 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; @@ -58,10 +54,6 @@ use esp_idf_svc::systime::EspSystemTime; use esp_idf_sys::{gpio_hold_dis, gpio_hold_en, vTaskDelay, EspError}; use one_wire_bus::OneWire; use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface}; -use crate::config::{BatteryBoardVersion, BoardVersion, PlantControllerConfig}; -use crate::log::log; -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; @@ -69,19 +61,21 @@ const REPEAT_MOIST_MEASURE: usize = 1; const TANK_MULTI_SAMPLE: usize = 11; -const PUMP8_BIT: usize = 0; -const PUMP1_BIT: usize = 1; -const PUMP2_BIT: usize = 2; -const PUMP3_BIT: usize = 3; -const PUMP4_BIT: usize = 4; -const PUMP5_BIT: usize = 5; -const PUMP6_BIT: usize = 6; -const PUMP7_BIT: usize = 7; +pub static I2C_DRIVER: Lazy>> = Lazy::new(PlantHal::create_i2c); #[non_exhaustive] struct V3Constants; impl V3Constants { + + const PUMP8_BIT: usize = 0; + const PUMP1_BIT: usize = 1; + const PUMP2_BIT: usize = 2; + const PUMP3_BIT: usize = 3; + const PUMP4_BIT: usize = 4; + 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; @@ -137,18 +131,6 @@ static mut LOW_VOLTAGE_DETECTED: bool = false; #[link_section = ".rtc.data"] static mut RESTART_TO_CONF: bool = false; -pub struct FileSystemSizeInfo { - pub total_size: usize, - pub used_size: usize, - pub free_size: usize, -} - -#[derive(strum::Display)] -pub enum StartMode { - AP, - Normal, -} - #[derive(Debug, PartialEq)] pub enum Sensor { A, @@ -156,191 +138,8 @@ pub enum Sensor { } pub struct PlantHal {} -pub struct ESP<'a> { - mqtt_client: Option>, - wifi_driver: EspWifi<'a>, - boot_button: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, esp_idf_hal::gpio::Input> -} - -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(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(()) - } - 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, - }; - - 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(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(Self::BASE_PATH); - let read_dir = fs::read_dir(filepath); - match read_dir { - OkStd(read_dir) => { - for item in read_dir { - match item { - OkStd(file) => { - let f = FileInfo { - filename: file.file_name().into_string().unwrap(), - size: file - .metadata() - .and_then(|it| Result::Ok(it.len())) - .unwrap_or_default() - as usize, - }; - result.push(f); - } - Err(err) => { - iter_error = Some(format!("{err:?}")); - break; - } - } - } - } - Err(err) => { - file_system_corrupt = Some(format!("{err:?}")); - } - } - let mut total: usize = 0; - let mut used: usize = 0; - unsafe { - esp_spiffs_info(storage.as_ptr(), &mut total, &mut used); - } - - FileList { - total, - used, - file_system_corrupt, - files: result, - iter_error, - } - } - 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) => { - bail!(format!("{err:?}")) - } - } - } - 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 { - File::open(filepath)? - }) - } - - 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, @@ -349,17 +148,7 @@ pub struct HAL<'a>{ pub battery_monitor: BatteryMonitor<'a> } -pub enum BatteryMonitor<'a> { - Disabled { - }, - BQ34Z100G1 { - battery_driver: Bq34z100g1Driver>, Delay> - }, - WchI2cSlave { - - } -} pub enum BoardHal<'a>{ Initial { @@ -394,6 +183,8 @@ pub enum BoardHal<'a>{ 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>, + charge_indicator: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, + awake: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, Output>, light: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, tank_power: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, one_wire_bus: OneWire>, @@ -411,32 +202,8 @@ pub enum BoardHal<'a>{ } } -#[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 { @@ -445,154 +212,6 @@ pub struct BackupHeader { 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 { @@ -601,7 +220,9 @@ impl BoardInteraction for HAL<'_> { .set_state(charging.into()) .unwrap(); } - V4 { .. } => {}, + V4 { charge_indicator , ..} => { + charge_indicator.set_state(charging.into()).unwrap(); + }, Initial { .. } => { } @@ -612,7 +233,9 @@ impl BoardInteraction for HAL<'_> { V3 { shift_register, .. } => { shift_register.decompose()[AWAKE].set_low().unwrap(); } - V4 { .. } => {}, + V4 { awake, .. } => { + awake.set_low().unwrap(); + }, Initial { .. } => { } @@ -636,9 +259,9 @@ impl BoardInteraction for HAL<'_> { } 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 { .. } => { + V3 { eeprom, .. } => {eeprom} + V4 { eeprom, .. } => {eeprom }, + &mut Initial { .. } => { bail!("Board not configured yet") } }; @@ -660,9 +283,9 @@ impl BoardInteraction for HAL<'_> { } 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 { .. } => { + V3 { eeprom, .. } => {eeprom} + V4 { eeprom, .. } => {eeprom } + &mut Initial { .. } => { bail!("Board not configured yet") } }; @@ -706,8 +329,8 @@ impl BoardInteraction for HAL<'_> { V3 { eeprom, .. } => { eeprom } - BoardHal::V4 { eeprom, .. } => { eeprom }, - &mut plant_hal::BoardHal::Initial { .. } => { + V4 { eeprom, .. } => { eeprom }, + &mut Initial { .. } => { bail!("Board not configured yet") } }; @@ -749,8 +372,8 @@ impl BoardInteraction for HAL<'_> { V3 { eeprom, .. } => { eeprom } - BoardHal::V4 { eeprom, .. } => { eeprom } - &mut plant_hal::BoardHal::Initial { .. } => { + V4 { eeprom, .. } => { eeprom } + &mut Initial { .. } => { bail!("Board not configured yet") } }; @@ -773,52 +396,29 @@ impl BoardInteraction for HAL<'_> { } 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 { .. } => { + V3 { solar_is_day, .. } => {solar_is_day.get_level().into()} + V4 { solar_is_day, .. } => {solar_is_day.get_level().into() } + Initial { .. } => { false } } } //should be multsampled fn water_temperature_c(&mut self) -> Result { - let mut delay = Delay::new_default(); - 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 { .. } => { + V3 { one_wire_bus, .. } => one_wire_bus, + V4 { one_wire_bus, .. } => one_wire_bus, + &mut Initial { .. } => { bail!("Board not configured yet") } }; one_wire_bus - .reset(&mut delay) + .reset(&mut self.esp.delay) .map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?; - let first = one_wire_bus.devices(false, &mut delay).next(); + let first = one_wire_bus.devices(false, &mut self.esp.delay).next(); if first.is_none() { bail!("Not found any one wire Ds18b20"); } @@ -830,11 +430,11 @@ impl BoardInteraction for HAL<'_> { .map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?; water_temp_sensor - .start_temp_measurement(one_wire_bus, &mut delay) + .start_temp_measurement(one_wire_bus, &mut self.esp.delay) .map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?; - ds18b20::Resolution::Bits12.delay_for_measurement_time(&mut delay); + ds18b20::Resolution::Bits12.delay_for_measurement_time(&mut self.esp.delay); let sensor_data = water_temp_sensor - .read_data(one_wire_bus, &mut delay) + .read_data(one_wire_bus, &mut self.esp.delay) .map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?; if sensor_data.temperature == 85_f32 { bail!("Ds18b20 dummy temperature returned"); @@ -844,18 +444,17 @@ impl BoardInteraction for HAL<'_> { /// return median tank sensor value in milli volt 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 { .. } => { + V3 { tank_power, tank_channel, .. } => {(tank_power,tank_channel)} + V4 { tank_power, tank_channel, .. } => {(tank_power,tank_channel) } + &mut Initial { .. } => { bail!("Board not configured yet") } }; - let delay = Delay::new_default(); tank_power.set_high()?; //let stabilize - delay.delay_ms(100); + self.esp.delay.delay_ms(100); let mut store = [0_u16; TANK_MULTI_SAMPLE]; for multisample in 0..TANK_MULTI_SAMPLE { @@ -880,9 +479,9 @@ impl BoardInteraction for HAL<'_> { } 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 { .. } => { + V3 { light, .. } => light, + V4 { light, .. } => light, + &mut Initial { .. } => { bail!("Board not configured yet") } }; @@ -896,14 +495,14 @@ impl BoardInteraction for HAL<'_> { 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, + 0 => V3Constants::PUMP1_BIT, + 1 => V3Constants::PUMP2_BIT, + 2 => V3Constants::PUMP3_BIT, + 3 => V3Constants::PUMP4_BIT, + 4 => V3Constants::PUMP5_BIT, + 5 => V3Constants::PUMP6_BIT, + 6 => V3Constants::PUMP7_BIT, + 7 => V3Constants::PUMP8_BIT, _ => bail!("Invalid pump {plant}",), }; //currently infallible error, keep for future as result anyway @@ -982,7 +581,7 @@ impl BoardInteraction for HAL<'_> { V4 { .. } => { //does not exist in v4, ignore it } - &mut plant_hal::BoardHal::Initial { .. } => { + &mut Initial { .. } => { bail!("Board not configured yet") } } @@ -1012,7 +611,7 @@ impl BoardInteraction for HAL<'_> { } fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result { match &mut self.board_hal { - V3{ signal_counter, shift_register, .. } => { + V3{ signal_counter, shift_register, .. } => { let mut results = [0_f32; REPEAT_MOIST_MEASURE]; for repeat in 0..REPEAT_MOIST_MEASURE { signal_counter.counter_pause()?; @@ -1167,10 +766,10 @@ impl BoardInteraction for HAL<'_> { 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); + 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; @@ -1205,88 +804,7 @@ impl BoardInteraction for HAL<'_> { general_fault.set_state(enable.into()).unwrap(); unsafe { gpio_hold_en(general_fault.pin()) }; } - 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.esp.wifi_driver - .set_configuration(&Configuration::AccessPoint(apconfig))?; - self.esp.wifi_driver.start()?; - Ok(()) - } - 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.esp.wifi_driver.set_configuration(&Configuration::Client( - ClientConfiguration { - ssid, - password: pw, - ..Default::default() - }, - ))?; - } - None => { - self.esp.wifi_driver.set_configuration(&Configuration::Client( - ClientConfiguration { - ssid, - auth_method: AuthMethod::None, - ..Default::default() - }, - ))?; - } - } - - self.esp.wifi_driver.start()?; - self.esp.wifi_driver.connect()?; - - let delay = Delay::new_default(); - let mut counter = 0_u32; - 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.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, waiting for link to be 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.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.esp.wifi_driver.sta_netif().get_ip_info()?; - log(LogMessage::WifiInfo, 0, 0, "", &format!("{address:?}")); - Ok(address) - } fn mode_override_pressed(&mut self) -> bool { self.esp.boot_button.get_level() == Level::Low } @@ -1390,214 +908,7 @@ impl BoardInteraction for HAL<'_> { Delay::new_default().delay_ms(10); Ok(()) } - fn mqtt(&mut self) -> Result<()> { - let base_topic = self.config - .network - .base_topic - .as_ref() - .context("missing base topic")?; - if base_topic.is_empty() { - bail!("Mqtt base_topic was empty") - } - let mqtt_url = self.config - .network - .mqtt_url - .as_ref() - .context("missing mqtt url")?; - if mqtt_url.is_empty() { - bail!("Mqtt url was empty") - } - let last_will_topic = format!("{}/state", base_topic); - let mqtt_client_config = MqttClientConfiguration { - lwt: Some(LwtConfiguration { - topic: &last_will_topic, - payload: "lost".as_bytes(), - qos: AtLeastOnce, - retain: true, - }), - client_id: Some("plantctrl"), - keep_alive_interval: Some(Duration::from_secs(60 * 60 * 2)), - //room for improvement - ..Default::default() - }; - - let mqtt_connected_event_received = Arc::new(AtomicBool::new(false)); - let mqtt_connected_event_ok = Arc::new(AtomicBool::new(false)); - - let round_trip_ok = Arc::new(AtomicBool::new(false)); - let round_trip_topic = format!("{}/internal/roundtrip", base_topic); - let stay_alive_topic = format!("{}/stay_alive", base_topic); - log(LogMessage::StayAlive, 0, 0, "", &stay_alive_topic); - - let mqtt_connected_event_received_copy = mqtt_connected_event_received.clone(); - let mqtt_connected_event_ok_copy = mqtt_connected_event_ok.clone(); - let stay_alive_topic_copy = stay_alive_topic.clone(); - let round_trip_topic_copy = round_trip_topic.clone(); - let round_trip_ok_copy = round_trip_ok.clone(); - let client_id = mqtt_client_config.client_id.unwrap_or("not set"); - log(LogMessage::MqttInfo, 0, 0, client_id, mqtt_url); - let mut client = EspMqttClient::new_cb(mqtt_url, &mqtt_client_config, move |event| { - let payload = event.payload(); - match payload { - embedded_svc::mqtt::client::EventPayload::Received { - id: _, - topic, - data, - details: _, - } => { - let data = String::from_utf8_lossy(data); - if let Some(topic) = topic { - //todo use enums - if topic.eq(round_trip_topic_copy.as_str()) { - round_trip_ok_copy.store(true, std::sync::atomic::Ordering::Relaxed); - } else if topic.eq(stay_alive_topic_copy.as_str()) { - let value = - data.eq_ignore_ascii_case("true") || data.eq_ignore_ascii_case("1"); - log(LogMessage::MqttStayAliveRec, 0, 0, &data, ""); - STAY_ALIVE.store(value, std::sync::atomic::Ordering::Relaxed); - } else { - log(LogMessage::UnknownTopic, 0, 0, "", topic); - } - } - } - esp_idf_svc::mqtt::client::EventPayload::Connected(_) => { - mqtt_connected_event_received_copy - .store(true, std::sync::atomic::Ordering::Relaxed); - mqtt_connected_event_ok_copy.store(true, std::sync::atomic::Ordering::Relaxed); - println!("Mqtt connected"); - } - esp_idf_svc::mqtt::client::EventPayload::Disconnected => { - mqtt_connected_event_received_copy - .store(true, std::sync::atomic::Ordering::Relaxed); - mqtt_connected_event_ok_copy.store(false, std::sync::atomic::Ordering::Relaxed); - println!("Mqtt disconnected"); - } - esp_idf_svc::mqtt::client::EventPayload::Error(esp_error) => { - println!("EspMqttError reported {:?}", esp_error); - mqtt_connected_event_received_copy - .store(true, std::sync::atomic::Ordering::Relaxed); - mqtt_connected_event_ok_copy.store(false, std::sync::atomic::Ordering::Relaxed); - println!("Mqtt error"); - } - esp_idf_svc::mqtt::client::EventPayload::BeforeConnect => { - println!("Mqtt before connect") - } - esp_idf_svc::mqtt::client::EventPayload::Subscribed(_) => { - println!("Mqtt subscribed") - } - esp_idf_svc::mqtt::client::EventPayload::Unsubscribed(_) => { - println!("Mqtt unsubscribed") - } - esp_idf_svc::mqtt::client::EventPayload::Published(_) => { - println!("Mqtt published") - } - esp_idf_svc::mqtt::client::EventPayload::Deleted(_) => { - println!("Mqtt deleted") - } - } - })?; - - let mut wait_for_connections_event = 0; - while wait_for_connections_event < 100 { - wait_for_connections_event += 1; - match mqtt_connected_event_received.load(std::sync::atomic::Ordering::Relaxed) { - true => { - println!("Mqtt connection callback received, progressing"); - match mqtt_connected_event_ok.load(std::sync::atomic::Ordering::Relaxed) { - true => { - println!("Mqtt did callback as connected, testing with roundtrip now"); - //subscribe to roundtrip - client.subscribe(round_trip_topic.as_str(), ExactlyOnce)?; - client.subscribe(stay_alive_topic.as_str(), ExactlyOnce)?; - //publish to roundtrip - client.publish( - round_trip_topic.as_str(), - ExactlyOnce, - false, - "online_test".as_bytes(), - )?; - - let mut wait_for_roundtrip = 0; - while wait_for_roundtrip < 100 { - wait_for_roundtrip += 1; - match round_trip_ok.load(std::sync::atomic::Ordering::Relaxed) { - true => { - println!("Round trip registered, proceeding"); - self.esp.mqtt_client = Some(client); - return Ok(()); - } - false => { - unsafe { vTaskDelay(10) }; - } - } - } - bail!("Mqtt did not complete roundtrip in time"); - } - false => { - bail!("Mqtt did respond but with failure") - } - } - } - false => { - unsafe { vTaskDelay(10) }; - } - } - } - bail!("Mqtt did not fire connection callback in time"); - } - fn mqtt_publish( - &mut self, - subtopic: &str, - message: &[u8], - ) -> Result<()> { - if self.esp.mqtt_client.is_none() { - return Ok(()); - } - if !subtopic.starts_with("/") { - println!("Subtopic without / at start {}", subtopic); - bail!("Subtopic without / at start {}", subtopic); - } - if subtopic.len() > 192 { - println!("Subtopic exceeds 192 chars {}", subtopic); - bail!("Subtopic exceeds 192 chars {}", subtopic); - } - let client = self.esp.mqtt_client.as_mut().unwrap(); - let mut full_topic: heapless::String<256> = heapless::String::new(); - if full_topic - .push_str(self.config.network.base_topic.as_ref().unwrap()) - .is_err() - { - println!("Some error assembling full_topic 1"); - bail!("Some error assembling full_topic 1") - }; - if full_topic.push_str(subtopic).is_err() { - println!("Some error assembling full_topic 2"); - bail!("Some error assembling full_topic 2") - }; - let publish = client.publish(&full_topic, ExactlyOnce, true, message); - Delay::new(10).delay_ms(50); - match publish { - OkStd(message_id) => { - println!( - "Published mqtt topic {} with message {:#?} msgid is {:?}", - full_topic, - String::from_utf8_lossy(message), - message_id - ); - Ok(()) - } - Err(err) => { - println!( - "Error during mqtt send on topic {} with message {:#?} error is {:?}", - full_topic, - String::from_utf8_lossy(message), - err - ); - Err(err)? - } - } - } fn get_restart_to_conf(&mut self) -> bool { unsafe { RESTART_TO_CONF } } @@ -1608,17 +919,7 @@ impl BoardInteraction for HAL<'_> { } } -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); @@ -1626,7 +927,6 @@ pub trait BoardInteraction { 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; @@ -1647,10 +947,6 @@ pub trait BoardInteraction { 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>; @@ -1658,74 +954,11 @@ pub trait BoardInteraction { 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"); - let fwversion = battery_driver.fw_version().unwrap_or_else(|e| { - println!("Firmware {:?}", e); - 0 - }); - println!("fw version is {}", fwversion); - - let design_capacity = battery_driver.design_capacity().unwrap_or_else(|e| { - println!("Design capacity {:?}", e); - 0 - }); - println!("Design Capacity {}", design_capacity); - if design_capacity == 1000 { - println!("Still stock configuring battery, readouts are likely to be wrong!"); - } - - let flags = battery_driver.get_flags_decoded()?; - println!("Flags {:?}", flags); - - let chem_id = battery_driver.chem_id().unwrap_or_else(|e| { - println!("Chemid {:?}", e); - 0 - }); - - let bat_temp = battery_driver.internal_temperature().unwrap_or_else(|e| { - println!("Bat Temp {:?}", e); - 0 - }); - let temp_c = Temperature::from_kelvin(bat_temp as f64 / 10_f64).as_celsius(); - let voltage = battery_driver.voltage().unwrap_or_else(|e| { - println!("Bat volt {:?}", e); - 0 - }); - let current = battery_driver.current().unwrap_or_else(|e| { - println!("Bat current {:?}", e); - 0 - }); - let state = battery_driver.state_of_charge().unwrap_or_else(|e| { - println!("Bat Soc {:?}", e); - 0 - }); - let charge_voltage = battery_driver.charge_voltage().unwrap_or_else(|e| { - println!("Bat Charge Volt {:?}", e); - 0 - }); - let charge_current = battery_driver.charge_current().unwrap_or_else(|e| { - println!("Bat Charge Current {:?}", e); - 0 - }); - println!("ChemId: {} Current voltage {} and current {} with charge {}% and temp {} CVolt: {} CCur {}", chem_id, voltage, current, state, temp_c, charge_voltage, charge_current); - let _ = battery_driver.unsealed(); - let _ = battery_driver.it_enable(); - Result::Ok(()) -} pub struct FreePeripherals { @@ -1738,6 +971,7 @@ pub struct FreePeripherals { pub gpio6: Gpio6, pub gpio7: Gpio7, pub gpio8: Gpio8, + //config button here pub gpio10: Gpio10, pub gpio11: Gpio11, pub gpio12: Gpio12, @@ -1747,8 +981,7 @@ pub struct FreePeripherals { pub gpio16: Gpio16, pub gpio17: Gpio17, pub gpio18: Gpio18, - pub gpio19: Gpio19, - pub gpio20: Gpio20, + //i2c here pub gpio21: Gpio21, pub gpio22: Gpio22, pub gpio23: Gpio23, @@ -1763,7 +996,7 @@ pub struct FreePeripherals { pub adc1: ADC1, } -pub static I2C_DRIVER: Lazy>> = Lazy::new(PlantHal::create_i2c); + impl PlantHal { fn create_i2c() -> Mutex> { let peripherals = unsafe { Peripherals::new() }; @@ -1813,8 +1046,6 @@ impl PlantHal { 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, @@ -1830,7 +1061,8 @@ impl PlantHal { let mut esp = ESP { mqtt_client: None, wifi_driver, - boot_button + boot_button, + delay: Delay::new(1000) }; @@ -1956,8 +1188,6 @@ impl PlantHal { general_fault.set_pull(Pull::Floating)?; general_fault.set_low()?; - - println!("Init rtc driver"); let mut rtc = Ds323x::new_ds3231(MutexDevice::new(&I2C_DRIVER)); @@ -2035,6 +1265,10 @@ impl PlantHal { let mut tank_power = PinDriver::input_output(peripherals.gpio11.downgrade())?; tank_power.set_pull(Pull::Floating)?; + let mut charge_indicator = PinDriver::input_output(peripherals.gpio3.downgrade())?; + charge_indicator.set_pull(Pull::Floating)?; + charge_indicator.set_low()?; + let mut pump_expander = Pca9535Immediate::new(MutexDevice::new(&I2C_DRIVER), 32); @@ -2055,6 +1289,7 @@ impl PlantHal { } Ok(V4 { + awake, tank_channel, solar_is_day, signal_counter, @@ -2065,7 +1300,8 @@ impl PlantHal { eeprom, general_fault, pump_expander, - sensor_expander + sensor_expander, + charge_indicator }) } diff --git a/rust/src/main.rs b/rust/src/main.rs index c4003bd..b739965 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -14,7 +14,6 @@ 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::{PlantHal, PLANT_COUNT}; use serde::{Deserialize, Serialize}; use std::sync::MutexGuard; use std::{ @@ -23,23 +22,15 @@ use std::{ }; mod config; mod log; -pub mod plant_hal; +mod hal; mod plant_state; mod tank; -use crate::plant_hal::{BatteryInteraction, BoardHal, BoardInteraction, HAL}; use plant_state::PlantState; use tank::*; - -const MOIST_SENSOR_MAX_FREQUENCY: u32 = 6500; // 60kHz (500Hz margin) -const MOIST_SENSOR_MIN_FREQUENCY: u32 = 150; // this is really really dry, think like cactus levels - -const FROM: (f32, f32) = ( - MOIST_SENSOR_MIN_FREQUENCY as f32, - MOIST_SENSOR_MAX_FREQUENCY as f32, -); -const TO: (f32, f32) = (0_f32, 100_f32); - +use crate::hal::{BoardInteraction, PlantHal, HAL, PLANT_COUNT}; +use crate::hal::battery::BatteryInteraction; +use crate::hal::BoardHal::Initial; pub static BOARD_ACCESS: Lazy> = Lazy::new(|| PlantHal::create().unwrap()); pub static STAY_ALIVE: Lazy = Lazy::new(|| AtomicBool::new(false)); @@ -218,9 +209,9 @@ fn safe_main() -> anyhow::Result<()> { } match board.board_hal { - BoardHal::Initial { .. } => { + Initial { .. } => { //config upload will trigger reboot and then switch to selected board_hal - let _ = board.wifi_ap(); + let _ = board.esp.wifi_ap(); drop(board); let reboot_now = Arc::new(AtomicBool::new(false)); let _webserver = httpd(reboot_now.clone()); @@ -240,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() { + match board.esp.wifi_ap() { Ok(_) => { println!("Started ap, continuing") } @@ -395,7 +386,6 @@ fn safe_main() -> anyhow::Result<()> { board.any_pump(false)?; // disable main power output, eg for a central pump with valve setup or a main water valve for the risk affine } } - update_plant_state(&mut plantstate, &mut board, &config, &timezone_time.timezone()); let is_day = board.is_day(); let state_of_charge = board.battery_monitor.state_charge_percent().unwrap_or(0); @@ -445,7 +435,7 @@ fn safe_main() -> anyhow::Result<()> { match serde_json::to_string(&light_state) { Ok(state) => { - let _ = board.mqtt_publish( "/light", state.as_bytes()); + let _ = board.esp.mqtt_publish( "/light", state.as_bytes()); } Err(err) => { println!("Error publishing lightstate {}", err); @@ -453,16 +443,16 @@ fn safe_main() -> anyhow::Result<()> { }; let deep_sleep_duration_minutes: u32 = if state_of_charge < 10 { - let _ = board.mqtt_publish( "/deepsleep", "low Volt 12h".as_bytes()); + let _ = board.esp.mqtt_publish( "/deepsleep", "low Volt 12h".as_bytes()); 12 * 60 } else if is_day { - let _ = board.mqtt_publish( "/deepsleep", "normal 20m".as_bytes()); + let _ = board.esp.mqtt_publish( "/deepsleep", "normal 20m".as_bytes()); 20 } else { - let _ = board.mqtt_publish( "/deepsleep", "night 1h".as_bytes()); + let _ = board.esp.mqtt_publish( "/deepsleep", "night 1h".as_bytes()); 60 }; - let _ = board.mqtt_publish( "/state", "sleep".as_bytes()); + let _ = board.esp.mqtt_publish( "/state", "sleep".as_bytes()); //determine next event //is light out of work trigger soon? @@ -511,7 +501,7 @@ fn obtain_tank_temperature(board: &mut MutexGuard) -> anyhow::Result { 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("/water", state.as_bytes()); + let _ = board.esp.mqtt_publish("/water", state.as_bytes()); } Err(err) => { println!("Error publishing tankstate {}", err); @@ -524,9 +514,9 @@ fn publish_plant_states(board: &mut MutexGuard, timezone_time: &DateTime { let plant_topic = format!("/plant{}", plant_id + 1); - let _ = board.mqtt_publish(&plant_topic, state.as_bytes()); + let _ = board.esp.mqtt_publish(&plant_topic, state.as_bytes()); //reduce speed as else messages will be dropped - Delay::new_default().delay_ms(200); + board.esp.delay.delay_ms(200); } Err(err) => { println!("Error publishing plant state {}", err); @@ -536,26 +526,27 @@ fn publish_plant_states(board: &mut MutexGuard, timezone_time: &DateTime, 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( + let _ = board.esp.mqtt_publish("/firmware/address", ip_address.as_bytes()); + let _ = board.esp.mqtt_publish( "/firmware/githash", version.git_hash.as_bytes()); + let _ = board.esp.mqtt_publish( "/firmware/buildtime", version.build_time.as_bytes(), ); - let _ = board.mqtt_publish( + let _ = board.esp.mqtt_publish( "/firmware/last_online", timezone_time.to_rfc3339().as_bytes(), ); - let _ = board.mqtt_publish( "/firmware/ota_state", ota_state_string.as_bytes()); - let _ = board.mqtt_publish( + let _ = board.esp.mqtt_publish( "/firmware/ota_state", ota_state_string.as_bytes()); + let _ = board.esp.mqtt_publish( "/firmware/partition_address", format!("{:#06x}", address).as_bytes(), ); - let _ = board.mqtt_publish( "/state", "online".as_bytes()); + let _ = board.esp.mqtt_publish( "/state", "online".as_bytes()); } fn try_connect_wifi_sntp_mqtt(board: &mut MutexGuard) -> NetworkMode{ - match board.wifi() { + let nw_conf = &board.config.network.clone(); + match board.esp.wifi(nw_conf) { Ok(ip_info) => { let sntp_mode: SntpMode = match board.sntp(1000 * 10) { Ok(new_time) => { @@ -570,7 +561,8 @@ fn try_connect_wifi_sntp_mqtt(board: &mut MutexGuard) -> NetworkMode{ } }; let mqtt_connected = if let Some(_) = board.config.network.mqtt_url { - match board.mqtt() { + let nw_config = &board.config.network.clone(); + match board.esp.mqtt(nw_config) { Ok(_) => { println!("Mqtt connection ready"); true @@ -606,7 +598,7 @@ fn pump_info(board: &mut MutexGuard, plant_id: usize, pump_active: bool, pu let pump_topic = format!("/pump{}", plant_id + 1); match serde_json::to_string(&pump_info) { Ok(state) => { - let _ = board.mqtt_publish(&pump_topic, state.as_bytes()); + let _ = board.esp.mqtt_publish(&pump_topic, state.as_bytes()); //reduce speed as else messages will be dropped Delay::new_default().delay_ms(200); } @@ -619,8 +611,8 @@ fn pump_info(board: &mut MutexGuard, plant_id: usize, pump_active: bool, pu fn publish_battery_state( board: &mut MutexGuard<'_, HAL<'_>> ) { - let state = board.get_battery_state(); - let _ = board.mqtt_publish( "/battery", state.as_bytes()); + let state = board.battery_monitor.get_battery_state(); + let _ = board.esp.mqtt_publish( "/battery", state.as_bytes()); } fn wait_infinity(wait_type: WaitType, reboot_now: Arc) -> ! { diff --git a/rust/src/plant_state.rs b/rust/src/plant_state.rs index e0e67ef..3e6f569 100644 --- a/rust/src/plant_state.rs +++ b/rust/src/plant_state.rs @@ -2,8 +2,8 @@ use chrono::{DateTime, TimeDelta, Utc}; use chrono_tz::Tz; use serde::{Deserialize, Serialize}; -use crate::{config::PlantConfig, in_time_range, plant_hal}; -use crate::plant_hal::BoardInteraction; +use crate::{config::PlantConfig, in_time_range}; +use crate::hal::{BoardInteraction, Sensor, HAL}; const MOIST_SENSOR_MAX_FREQUENCY: f32 = 7500.; // 60kHz (500Hz margin) const MOIST_SENSOR_MIN_FREQUENCY: f32 = 150.; // this is really, really dry, think like cactus levels @@ -114,10 +114,10 @@ fn map_range_moisture( impl PlantState { pub fn read_hardware_state( plant_id: usize, - board: &mut plant_hal::HAL + board: &mut HAL ) -> Self { let sensor_a = if board.config.plants[plant_id].sensor_a { - match board.measure_moisture_hz(plant_id, plant_hal::Sensor::A) { + match board.measure_moisture_hz(plant_id, Sensor::A) { Ok(raw) => match map_range_moisture( raw, board.config.plants[plant_id].moisture_sensor_min_frequency, @@ -138,7 +138,7 @@ impl PlantState { }; let sensor_b = if board.config.plants[plant_id].sensor_b { - match board.measure_moisture_hz(plant_id, plant_hal::Sensor::B) { + match board.measure_moisture_hz(plant_id, Sensor::B) { Ok(raw) => match map_range_moisture( raw, board.config.plants[plant_id].moisture_sensor_min_frequency, diff --git a/rust/src/tank.rs b/rust/src/tank.rs index 9217e21..c6a2e60 100644 --- a/rust/src/tank.rs +++ b/rust/src/tank.rs @@ -1,7 +1,7 @@ use serde::Serialize; use crate::config::TankConfig; -use crate::plant_hal::{BoardInteraction, HAL}; +use crate::hal::{BoardInteraction, HAL}; const OPEN_TANK_VOLTAGE: f32 = 3.0; pub const WATER_FROZEN_THRESH: f32 = 4.0; diff --git a/rust/src/webserver/webserver.rs b/rust/src/webserver/webserver.rs index 23ce132..f89fe61 100644 --- a/rust/src/webserver/webserver.rs +++ b/rust/src/webserver/webserver.rs @@ -1,7 +1,8 @@ //offer ota and config mode +use crate::hal::battery::BatteryInteraction; use crate::{ - determine_tank_state, get_version, log::LogMessage, plant_hal::PLANT_COUNT, + determine_tank_state, get_version, log::LogMessage, plant_state::PlantState, BOARD_ACCESS, }; use anyhow::bail; @@ -20,7 +21,7 @@ use std::{ use url::Url; use crate::config::PlantControllerConfig; -use crate::plant_hal::BoardInteraction; +use crate::hal::{BoardInteraction, PLANT_COUNT}; use crate::plant_state::MoistureSensorState; #[derive(Serialize, Debug)] @@ -228,7 +229,7 @@ fn get_battery_state( _request: &mut Request<&mut EspHttpConnection>, ) -> Result, anyhow::Error> { let mut board = BOARD_ACCESS.lock().unwrap(); - let battery_state = board.get_battery_state(); + let battery_state = board.battery_monitor.get_battery_state(); let battery_json = serde_json::to_string(&battery_state)?; anyhow::Ok(Some(battery_json)) } @@ -322,7 +323,7 @@ fn ota( total_read += read; let to_write = &buffer[0..read]; //delay for watchdog and wifi stuff - Delay::new_default().delay_ms(1); + board.esp.delay.delay_ms(1); let iter = (total_read / 1024) % 8; if iter != lastiter { diff --git a/website/themes/blowfish b/website/themes/blowfish index 1d21656..26d1205 160000 --- a/website/themes/blowfish +++ b/website/themes/blowfish @@ -1 +1 @@ -Subproject commit 1d21656d5efcf6a6b247245d057bf553f3209f39 +Subproject commit 26d1205439b460bee960fd4c29f3c5c20948875f