combine all branches
This commit is contained in:
parent
751f07e6fd
commit
e2cce88390
@ -1,11 +1,7 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use crate::hal::PLANT_COUNT;
|
||||||
use chrono_tz::{Europe::Berlin, Tz};
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::plant_state::PlantWateringMode;
|
use crate::plant_state::PlantWateringMode;
|
||||||
use crate::PLANT_COUNT;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@ -15,8 +11,7 @@ pub struct NetworkConfig {
|
|||||||
pub password: Option<heapless::String<64>>,
|
pub password: Option<heapless::String<64>>,
|
||||||
pub mqtt_url: Option<heapless::String<128>>,
|
pub mqtt_url: Option<heapless::String<128>>,
|
||||||
pub base_topic: Option<heapless::String<64>>,
|
pub base_topic: Option<heapless::String<64>>,
|
||||||
pub max_wait: u32,
|
pub max_wait: u32
|
||||||
pub timezone: heapless::String<64>,
|
|
||||||
}
|
}
|
||||||
impl Default for NetworkConfig {
|
impl Default for NetworkConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
@ -26,8 +21,7 @@ impl Default for NetworkConfig {
|
|||||||
password: None,
|
password: None,
|
||||||
mqtt_url: None,
|
mqtt_url: None,
|
||||||
base_topic: None,
|
base_topic: None,
|
||||||
max_wait: 10000,
|
max_wait: 10000
|
||||||
timezone: heapless::String::from_str("Europe/Berlin").unwrap(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
274
rust/src/hal/battery.rs
Normal file
274
rust/src/hal/battery.rs
Normal file
@ -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<u8>;
|
||||||
|
fn remaining_milli_ampere_hour(&mut self) -> anyhow::Result<u16>;
|
||||||
|
fn max_milli_ampere_hour(&mut self) -> anyhow::Result<u16>;
|
||||||
|
fn design_milli_ampere_hour(&mut self) -> anyhow::Result<u16>;
|
||||||
|
fn voltage_milli_volt(&mut self) -> anyhow::Result<u16>;
|
||||||
|
fn average_current_milli_ampere(&mut self) -> anyhow::Result<i16>;
|
||||||
|
fn cycle_count(&mut self) -> anyhow::Result<u16>;
|
||||||
|
fn state_health_percent(&mut self) -> anyhow::Result<u8>;
|
||||||
|
fn bat_temperature(&mut self) -> anyhow::Result<u16>;
|
||||||
|
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<MutexDevice<'a, I2cDriver<'a>>, Delay>
|
||||||
|
},
|
||||||
|
WchI2cSlave {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
impl BatteryInteraction for BatteryMonitor<'_> {
|
||||||
|
fn state_charge_percent(&mut self) -> anyhow::Result<u8> {
|
||||||
|
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<u16> {
|
||||||
|
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<u16> {
|
||||||
|
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<u16> {
|
||||||
|
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<u16> {
|
||||||
|
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<i16> {
|
||||||
|
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<u16> {
|
||||||
|
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<u8> {
|
||||||
|
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<u16> {
|
||||||
|
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<MutexDevice<I2cDriver<'_>>, Delay>,
|
||||||
|
) -> anyhow::Result<(), Bq34Z100Error<I2cError>> {
|
||||||
|
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(())
|
||||||
|
}
|
533
rust/src/hal/esp.rs
Normal file
533
rust/src/hal/esp.rs
Normal file
@ -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<FileInfo>,
|
||||||
|
file_system_corrupt: Option<String>,
|
||||||
|
iter_error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<MqttClient<'a>>,
|
||||||
|
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<IpInfo> {
|
||||||
|
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<PlantControllerConfig> {
|
||||||
|
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<FileSystemSizeInfo> {
|
||||||
|
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<File> {
|
||||||
|
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)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -14,7 +14,6 @@ use esp_idf_sys::{
|
|||||||
use esp_ota::{mark_app_valid, rollback_and_reboot};
|
use esp_ota::{mark_app_valid, rollback_and_reboot};
|
||||||
use log::{log, LogMessage};
|
use log::{log, LogMessage};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use plant_hal::{PlantHal, PLANT_COUNT};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::MutexGuard;
|
use std::sync::MutexGuard;
|
||||||
use std::{
|
use std::{
|
||||||
@ -23,23 +22,15 @@ use std::{
|
|||||||
};
|
};
|
||||||
mod config;
|
mod config;
|
||||||
mod log;
|
mod log;
|
||||||
pub mod plant_hal;
|
mod hal;
|
||||||
mod plant_state;
|
mod plant_state;
|
||||||
mod tank;
|
mod tank;
|
||||||
|
|
||||||
use crate::plant_hal::{BatteryInteraction, BoardHal, BoardInteraction, HAL};
|
|
||||||
use plant_state::PlantState;
|
use plant_state::PlantState;
|
||||||
use tank::*;
|
use tank::*;
|
||||||
|
use crate::hal::{BoardInteraction, PlantHal, HAL, PLANT_COUNT};
|
||||||
const MOIST_SENSOR_MAX_FREQUENCY: u32 = 6500; // 60kHz (500Hz margin)
|
use crate::hal::battery::BatteryInteraction;
|
||||||
const MOIST_SENSOR_MIN_FREQUENCY: u32 = 150; // this is really really dry, think like cactus levels
|
use crate::hal::BoardHal::Initial;
|
||||||
|
|
||||||
const FROM: (f32, f32) = (
|
|
||||||
MOIST_SENSOR_MIN_FREQUENCY as f32,
|
|
||||||
MOIST_SENSOR_MAX_FREQUENCY as f32,
|
|
||||||
);
|
|
||||||
const TO: (f32, f32) = (0_f32, 100_f32);
|
|
||||||
|
|
||||||
pub static BOARD_ACCESS: Lazy<Mutex<HAL>> = Lazy::new(|| PlantHal::create().unwrap());
|
pub static BOARD_ACCESS: Lazy<Mutex<HAL>> = Lazy::new(|| PlantHal::create().unwrap());
|
||||||
pub static STAY_ALIVE: Lazy<AtomicBool> = Lazy::new(|| AtomicBool::new(false));
|
pub static STAY_ALIVE: Lazy<AtomicBool> = Lazy::new(|| AtomicBool::new(false));
|
||||||
|
|
||||||
@ -218,9 +209,9 @@ fn safe_main() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match board.board_hal {
|
match board.board_hal {
|
||||||
BoardHal::Initial { .. } => {
|
Initial { .. } => {
|
||||||
//config upload will trigger reboot and then switch to selected board_hal
|
//config upload will trigger reboot and then switch to selected board_hal
|
||||||
let _ = board.wifi_ap();
|
let _ = board.esp.wifi_ap();
|
||||||
drop(board);
|
drop(board);
|
||||||
let reboot_now = Arc::new(AtomicBool::new(false));
|
let reboot_now = Arc::new(AtomicBool::new(false));
|
||||||
let _webserver = httpd(reboot_now.clone());
|
let _webserver = httpd(reboot_now.clone());
|
||||||
@ -240,7 +231,7 @@ fn safe_main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
if matches!(network_mode, NetworkMode::OFFLINE) && to_config {
|
if matches!(network_mode, NetworkMode::OFFLINE) && to_config {
|
||||||
println!("Could not connect to station and config mode forced, switching to ap mode!");
|
println!("Could not connect to station and config mode forced, switching to ap mode!");
|
||||||
match board.wifi_ap() {
|
match board.esp.wifi_ap() {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
println!("Started ap, continuing")
|
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
|
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 is_day = board.is_day();
|
||||||
let state_of_charge = board.battery_monitor.state_charge_percent().unwrap_or(0);
|
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) {
|
match serde_json::to_string(&light_state) {
|
||||||
Ok(state) => {
|
Ok(state) => {
|
||||||
let _ = board.mqtt_publish( "/light", state.as_bytes());
|
let _ = board.esp.mqtt_publish( "/light", state.as_bytes());
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!("Error publishing lightstate {}", 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 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
|
12 * 60
|
||||||
} else if is_day {
|
} else if is_day {
|
||||||
let _ = board.mqtt_publish( "/deepsleep", "normal 20m".as_bytes());
|
let _ = board.esp.mqtt_publish( "/deepsleep", "normal 20m".as_bytes());
|
||||||
20
|
20
|
||||||
} else {
|
} else {
|
||||||
let _ = board.mqtt_publish( "/deepsleep", "night 1h".as_bytes());
|
let _ = board.esp.mqtt_publish( "/deepsleep", "night 1h".as_bytes());
|
||||||
60
|
60
|
||||||
};
|
};
|
||||||
let _ = board.mqtt_publish( "/state", "sleep".as_bytes());
|
let _ = board.esp.mqtt_publish( "/state", "sleep".as_bytes());
|
||||||
|
|
||||||
//determine next event
|
//determine next event
|
||||||
//is light out of work trigger soon?
|
//is light out of work trigger soon?
|
||||||
@ -511,7 +501,7 @@ fn obtain_tank_temperature(board: &mut MutexGuard<HAL>) -> anyhow::Result<f32> {
|
|||||||
fn publish_tank_state(board: &mut MutexGuard<HAL>, tank_state: &TankState, water_temp: &anyhow::Result<f32>) {
|
fn publish_tank_state(board: &mut MutexGuard<HAL>, tank_state: &TankState, water_temp: &anyhow::Result<f32>) {
|
||||||
match serde_json::to_string(&tank_state.as_mqtt_info(&board.config.tank, water_temp)) {
|
match serde_json::to_string(&tank_state.as_mqtt_info(&board.config.tank, water_temp)) {
|
||||||
Ok(state) => {
|
Ok(state) => {
|
||||||
let _ = board.mqtt_publish("/water", state.as_bytes());
|
let _ = board.esp.mqtt_publish("/water", state.as_bytes());
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!("Error publishing tankstate {}", err);
|
println!("Error publishing tankstate {}", err);
|
||||||
@ -524,9 +514,9 @@ fn publish_plant_states(board: &mut MutexGuard<HAL>, timezone_time: &DateTime<Tz
|
|||||||
match serde_json::to_string(&plant_state.to_mqtt_info(plant_conf, &timezone_time)) {
|
match serde_json::to_string(&plant_state.to_mqtt_info(plant_conf, &timezone_time)) {
|
||||||
Ok(state) => {
|
Ok(state) => {
|
||||||
let plant_topic = format!("/plant{}", plant_id + 1);
|
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
|
//reduce speed as else messages will be dropped
|
||||||
Delay::new_default().delay_ms(200);
|
board.esp.delay.delay_ms(200);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!("Error publishing plant state {}", err);
|
println!("Error publishing plant state {}", err);
|
||||||
@ -536,26 +526,27 @@ fn publish_plant_states(board: &mut MutexGuard<HAL>, timezone_time: &DateTime<Tz
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn publish_firmware_info(version: VersionInfo, address: u32, ota_state_string: &str, board: &mut MutexGuard<HAL>, ip_address: &String, timezone_time: DateTime<Tz>) {
|
fn publish_firmware_info(version: VersionInfo, address: u32, ota_state_string: &str, board: &mut MutexGuard<HAL>, ip_address: &String, timezone_time: DateTime<Tz>) {
|
||||||
let _ = board.mqtt_publish("/firmware/address", ip_address.as_bytes());
|
let _ = board.esp.mqtt_publish("/firmware/address", ip_address.as_bytes());
|
||||||
let _ = board.mqtt_publish( "/firmware/githash", version.git_hash.as_bytes());
|
let _ = board.esp.mqtt_publish( "/firmware/githash", version.git_hash.as_bytes());
|
||||||
let _ = board.mqtt_publish(
|
let _ = board.esp.mqtt_publish(
|
||||||
"/firmware/buildtime",
|
"/firmware/buildtime",
|
||||||
version.build_time.as_bytes(),
|
version.build_time.as_bytes(),
|
||||||
);
|
);
|
||||||
let _ = board.mqtt_publish(
|
let _ = board.esp.mqtt_publish(
|
||||||
"/firmware/last_online",
|
"/firmware/last_online",
|
||||||
timezone_time.to_rfc3339().as_bytes(),
|
timezone_time.to_rfc3339().as_bytes(),
|
||||||
);
|
);
|
||||||
let _ = board.mqtt_publish( "/firmware/ota_state", ota_state_string.as_bytes());
|
let _ = board.esp.mqtt_publish( "/firmware/ota_state", ota_state_string.as_bytes());
|
||||||
let _ = board.mqtt_publish(
|
let _ = board.esp.mqtt_publish(
|
||||||
"/firmware/partition_address",
|
"/firmware/partition_address",
|
||||||
format!("{:#06x}", address).as_bytes(),
|
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<HAL>) -> NetworkMode{
|
fn try_connect_wifi_sntp_mqtt(board: &mut MutexGuard<HAL>) -> NetworkMode{
|
||||||
match board.wifi() {
|
let nw_conf = &board.config.network.clone();
|
||||||
|
match board.esp.wifi(nw_conf) {
|
||||||
Ok(ip_info) => {
|
Ok(ip_info) => {
|
||||||
let sntp_mode: SntpMode = match board.sntp(1000 * 10) {
|
let sntp_mode: SntpMode = match board.sntp(1000 * 10) {
|
||||||
Ok(new_time) => {
|
Ok(new_time) => {
|
||||||
@ -570,7 +561,8 @@ fn try_connect_wifi_sntp_mqtt(board: &mut MutexGuard<HAL>) -> NetworkMode{
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mqtt_connected = if let Some(_) = board.config.network.mqtt_url {
|
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(_) => {
|
Ok(_) => {
|
||||||
println!("Mqtt connection ready");
|
println!("Mqtt connection ready");
|
||||||
true
|
true
|
||||||
@ -606,7 +598,7 @@ fn pump_info(board: &mut MutexGuard<HAL>, plant_id: usize, pump_active: bool, pu
|
|||||||
let pump_topic = format!("/pump{}", plant_id + 1);
|
let pump_topic = format!("/pump{}", plant_id + 1);
|
||||||
match serde_json::to_string(&pump_info) {
|
match serde_json::to_string(&pump_info) {
|
||||||
Ok(state) => {
|
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
|
//reduce speed as else messages will be dropped
|
||||||
Delay::new_default().delay_ms(200);
|
Delay::new_default().delay_ms(200);
|
||||||
}
|
}
|
||||||
@ -619,8 +611,8 @@ fn pump_info(board: &mut MutexGuard<HAL>, plant_id: usize, pump_active: bool, pu
|
|||||||
fn publish_battery_state(
|
fn publish_battery_state(
|
||||||
board: &mut MutexGuard<'_, HAL<'_>>
|
board: &mut MutexGuard<'_, HAL<'_>>
|
||||||
) {
|
) {
|
||||||
let state = board.get_battery_state();
|
let state = board.battery_monitor.get_battery_state();
|
||||||
let _ = board.mqtt_publish( "/battery", state.as_bytes());
|
let _ = board.esp.mqtt_publish( "/battery", state.as_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wait_infinity(wait_type: WaitType, reboot_now: Arc<AtomicBool>) -> ! {
|
fn wait_infinity(wait_type: WaitType, reboot_now: Arc<AtomicBool>) -> ! {
|
||||||
|
@ -2,8 +2,8 @@ use chrono::{DateTime, TimeDelta, Utc};
|
|||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{config::PlantConfig, in_time_range, plant_hal};
|
use crate::{config::PlantConfig, in_time_range};
|
||||||
use crate::plant_hal::BoardInteraction;
|
use crate::hal::{BoardInteraction, Sensor, HAL};
|
||||||
|
|
||||||
const MOIST_SENSOR_MAX_FREQUENCY: f32 = 7500.; // 60kHz (500Hz margin)
|
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
|
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 {
|
impl PlantState {
|
||||||
pub fn read_hardware_state(
|
pub fn read_hardware_state(
|
||||||
plant_id: usize,
|
plant_id: usize,
|
||||||
board: &mut plant_hal::HAL
|
board: &mut HAL
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let sensor_a = if board.config.plants[plant_id].sensor_a {
|
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(
|
Ok(raw) => match map_range_moisture(
|
||||||
raw,
|
raw,
|
||||||
board.config.plants[plant_id].moisture_sensor_min_frequency,
|
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 {
|
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(
|
Ok(raw) => match map_range_moisture(
|
||||||
raw,
|
raw,
|
||||||
board.config.plants[plant_id].moisture_sensor_min_frequency,
|
board.config.plants[plant_id].moisture_sensor_min_frequency,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::config::TankConfig;
|
use crate::config::TankConfig;
|
||||||
use crate::plant_hal::{BoardInteraction, HAL};
|
use crate::hal::{BoardInteraction, HAL};
|
||||||
|
|
||||||
const OPEN_TANK_VOLTAGE: f32 = 3.0;
|
const OPEN_TANK_VOLTAGE: f32 = 3.0;
|
||||||
pub const WATER_FROZEN_THRESH: f32 = 4.0;
|
pub const WATER_FROZEN_THRESH: f32 = 4.0;
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
//offer ota and config mode
|
//offer ota and config mode
|
||||||
|
|
||||||
|
use crate::hal::battery::BatteryInteraction;
|
||||||
use crate::{
|
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,
|
plant_state::PlantState, BOARD_ACCESS,
|
||||||
};
|
};
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
@ -20,7 +21,7 @@ use std::{
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::config::PlantControllerConfig;
|
use crate::config::PlantControllerConfig;
|
||||||
use crate::plant_hal::BoardInteraction;
|
use crate::hal::{BoardInteraction, PLANT_COUNT};
|
||||||
use crate::plant_state::MoistureSensorState;
|
use crate::plant_state::MoistureSensorState;
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
@ -228,7 +229,7 @@ fn get_battery_state(
|
|||||||
_request: &mut Request<&mut EspHttpConnection>,
|
_request: &mut Request<&mut EspHttpConnection>,
|
||||||
) -> Result<Option<std::string::String>, anyhow::Error> {
|
) -> Result<Option<std::string::String>, anyhow::Error> {
|
||||||
let mut board = BOARD_ACCESS.lock().unwrap();
|
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)?;
|
let battery_json = serde_json::to_string(&battery_state)?;
|
||||||
anyhow::Ok(Some(battery_json))
|
anyhow::Ok(Some(battery_json))
|
||||||
}
|
}
|
||||||
@ -322,7 +323,7 @@ fn ota(
|
|||||||
total_read += read;
|
total_read += read;
|
||||||
let to_write = &buffer[0..read];
|
let to_write = &buffer[0..read];
|
||||||
//delay for watchdog and wifi stuff
|
//delay for watchdog and wifi stuff
|
||||||
Delay::new_default().delay_ms(1);
|
board.esp.delay.delay_ms(1);
|
||||||
|
|
||||||
let iter = (total_read / 1024) % 8;
|
let iter = (total_read / 1024) % 8;
|
||||||
if iter != lastiter {
|
if iter != lastiter {
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 1d21656d5efcf6a6b247245d057bf553f3209f39
|
Subproject commit 26d1205439b460bee960fd4c29f3c5c20948875f
|
Loading…
x
Reference in New Issue
Block a user