adjust rust code to new config file, fix bq34z100 flasher

This commit is contained in:
Empire Phoenix 2024-12-16 02:15:03 +01:00
parent c89a617d9d
commit 74f9c17461
4 changed files with 267 additions and 220 deletions

View File

@ -1,68 +1,77 @@
use std::{array::from_fn, str::FromStr}; use std::str::FromStr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::PLANT_COUNT; use crate::PLANT_COUNT;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct Config { pub struct NetworkConfig {
pub ap_ssid: heapless::String<32>, pub ap_ssid: heapless::String<32>,
pub ssid: Option<heapless::String<32>>, pub ssid: Option<heapless::String<32>>,
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_consecutive_pump_count: u8, }
impl Default for NetworkConfig {
fn default() -> Self {
Self {
ap_ssid: heapless::String::from_str("PlantCtrl Init").unwrap(),
ssid: None,
password: None,
mqtt_url: None,
base_topic: None,
}
}
}
pub tank_allow_pumping_if_sensor_error: bool, #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct NightLampConfig {
pub night_lamp_hour_start: u8,
pub night_lamp_hour_end: u8,
pub night_lamp_only_when_dark: bool,
}
impl Default for NightLampConfig {
fn default() -> Self {
Self {
night_lamp_hour_start: 19,
night_lamp_hour_end: 2,
night_lamp_only_when_dark: true,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct TankConfig {
pub tank_sensor_enabled: bool, pub tank_sensor_enabled: bool,
pub tank_allow_pumping_if_sensor_error: bool,
pub tank_useable_ml: u32, pub tank_useable_ml: u32,
pub tank_warn_percent: u8, pub tank_warn_percent: u8,
pub tank_empty_percent: u8, pub tank_empty_percent: u8,
pub tank_full_percent: u8, pub tank_full_percent: u8,
pub night_lamp_hour_start: u8,
pub night_lamp_hour_end: u8,
pub night_lamp_only_when_dark: bool,
pub plants: [Plant; PLANT_COUNT],
} }
impl Default for TankConfig {
impl Default for Config {
fn default() -> Self { fn default() -> Self {
Self { Self {
ap_ssid: heapless::String::from_str("Plantctrl").unwrap(), tank_sensor_enabled: false,
ssid: None,
password: None,
base_topic: Some(heapless::String::from_str("plant/one").unwrap()),
mqtt_url: Some(heapless::String::from_str("mqtt://192.168.1.1:1883").unwrap()),
tank_allow_pumping_if_sensor_error: true, tank_allow_pumping_if_sensor_error: true,
tank_sensor_enabled: true, tank_useable_ml: 50000,
tank_warn_percent: 50, tank_warn_percent: 40,
night_lamp_hour_start: 21, tank_empty_percent: 5,
night_lamp_hour_end: 2, tank_full_percent: 95,
night_lamp_only_when_dark: true,
plants: from_fn(|_i| Plant::default()),
max_consecutive_pump_count: 15,
tank_useable_ml: 5000,
tank_empty_percent: 0_u8,
tank_full_percent: 100_u8,
} }
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum Mode { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
OFF, pub struct PlantControllerConfig {
TargetMoisture, pub network: NetworkConfig,
TimerOnly, pub tank: TankConfig,
TimerAndDeadzone, pub night_lamp: NightLampConfig,
pub plants: [PlantConfig; PLANT_COUNT],
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct Plant { pub struct PlantConfig {
pub mode: Mode, pub mode: Mode,
pub target_moisture: u8, pub target_moisture: u8,
pub pump_time_s: u16, pub pump_time_s: u16,
@ -70,17 +79,27 @@ pub struct Plant {
pub pump_hour_start: u8, pub pump_hour_start: u8,
pub pump_hour_end: u8, pub pump_hour_end: u8,
pub sensor_b: bool, pub sensor_b: bool,
pub max_consecutive_pump_count: u8,
} }
impl Default for Plant { impl Default for PlantConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
target_moisture: 40,
pump_time_s: 60,
pump_cooldown_min: 60,
pump_hour_start: 8,
pump_hour_end: 20,
mode: Mode::OFF, mode: Mode::OFF,
target_moisture: 40,
pump_time_s: 30,
pump_cooldown_min: 60,
pump_hour_start: 9,
pump_hour_end: 20,
sensor_b: false, sensor_b: false,
max_consecutive_pump_count: 10,
} }
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum Mode {
OFF,
TargetMoisture,
TimerOnly,
TimerAndDeadzone,
}

View File

@ -22,7 +22,7 @@ use plant_hal::{PlantCtrlBoard, PlantHal, PLANT_COUNT};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
config::Config, config::PlantControllerConfig,
espota::{mark_app_valid, rollback_and_reboot}, espota::{mark_app_valid, rollback_and_reboot},
webserver::webserver::httpd, webserver::webserver::httpd,
}; };
@ -250,7 +250,7 @@ fn safe_main() -> anyhow::Result<()> {
} }
} }
let config: Config; let config: PlantControllerConfig;
match board.get_config() { match board.get_config() {
Ok(valid) => { Ok(valid) => {
config = valid; config = valid;
@ -271,8 +271,12 @@ fn safe_main() -> anyhow::Result<()> {
let mut sntp = false; let mut sntp = false;
println!("attempting to connect wifi"); println!("attempting to connect wifi");
let mut ip_address: Option<String> = None; let mut ip_address: Option<String> = None;
if config.ssid.is_some() { if config.network.ssid.is_some() {
match board.wifi(config.ssid.clone().unwrap(), config.password.clone(), 10000) { match board.wifi(
config.network.ssid.clone().unwrap(),
config.network.password.clone(),
10000,
) {
Ok(ip_info) => { Ok(ip_info) => {
ip_address = Some(ip_info.ip.to_string()); ip_address = Some(ip_info.ip.to_string());
wifi = true; wifi = true;
@ -412,16 +416,16 @@ fn safe_main() -> anyhow::Result<()> {
let mut did_pump = false; let mut did_pump = false;
match plant_to_pump { match plant_to_pump {
Some(plant) => { Some(plant) => {
let plant_config = &config.plants[plant];
let state = &mut plantstate[plant]; let state = &mut plantstate[plant];
state.consecutive_pump_count = board.consecutive_pump_count(plant) + 1; state.consecutive_pump_count = board.consecutive_pump_count(plant) + 1;
board.store_consecutive_pump_count(plant, state.consecutive_pump_count); board.store_consecutive_pump_count(plant, state.consecutive_pump_count);
if state.consecutive_pump_count > config.max_consecutive_pump_count.into() { if state.consecutive_pump_count > plant_config.max_consecutive_pump_count.into() {
state.not_effective = true; state.not_effective = true;
board.fault(plant, true); board.fault(plant, true);
} }
let plant_config = &config.plants[plant];
println!( println!(
"Trying to pump for {}s with pump {} now", "Trying to pump for {}s with pump {} now",
plant_config.pump_time_s, plant plant_config.pump_time_s, plant
@ -454,8 +458,8 @@ fn safe_main() -> anyhow::Result<()> {
light_state.is_day = is_day; light_state.is_day = is_day;
light_state.out_of_work_hour = !in_time_range( light_state.out_of_work_hour = !in_time_range(
&timezone_time, &timezone_time,
config.night_lamp_hour_start, config.night_lamp.night_lamp_hour_start,
config.night_lamp_hour_end, config.night_lamp.night_lamp_hour_end,
); );
let state_of_charge = board.state_charge_percent().unwrap_or(0); let state_of_charge = board.state_charge_percent().unwrap_or(0);
@ -467,7 +471,7 @@ fn safe_main() -> anyhow::Result<()> {
light_state.battery_low = board.low_voltage_in_cycle(); light_state.battery_low = board.low_voltage_in_cycle();
if !light_state.out_of_work_hour { if !light_state.out_of_work_hour {
if config.night_lamp_only_when_dark { if config.night_lamp.night_lamp_only_when_dark {
if !light_state.is_day { if !light_state.is_day {
if light_state.battery_low { if light_state.battery_low {
board.light(false).unwrap(); board.light(false).unwrap();
@ -537,7 +541,7 @@ fn safe_main() -> anyhow::Result<()> {
fn publish_battery_state( fn publish_battery_state(
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
config: &Config, config: &PlantControllerConfig,
) { ) {
let bat = BatteryState { let bat = BatteryState {
voltage_milli_volt: &to_string(&board.voltage_milli_volt()), voltage_milli_volt: &to_string(&board.voltage_milli_volt()),
@ -560,9 +564,9 @@ fn publish_battery_state(
fn determine_tank_state( fn determine_tank_state(
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
config: &Config, config: &PlantControllerConfig,
) -> TankState { ) -> TankState {
if config.tank_sensor_enabled { if config.tank.tank_sensor_enabled {
let mut rv: TankState = TankState { let mut rv: TankState = TankState {
..Default::default() ..Default::default()
}; };
@ -572,30 +576,30 @@ fn determine_tank_state(
rv.raw = raw; rv.raw = raw;
return map_range( return map_range(
( (
config.tank_empty_percent as f32, config.tank.tank_empty_percent as f32,
config.tank_full_percent as f32, config.tank.tank_full_percent as f32,
), ),
raw as f32, raw as f32,
); );
}) })
.and_then(|percent| { .and_then(|percent| {
rv.left_ml = ((percent * config.tank_useable_ml as f32) / 100_f32) as u32; rv.left_ml = ((percent * config.tank.tank_useable_ml as f32) / 100_f32) as u32;
println!( println!(
"Tank sensor returned mv {} as {}% leaving {} ml useable", "Tank sensor returned mv {} as {}% leaving {} ml useable",
rv.raw, percent as u8, rv.left_ml rv.raw, percent as u8, rv.left_ml
); );
if config.tank_warn_percent > percent as u8 { if config.tank.tank_warn_percent > percent as u8 {
board.general_fault(true); board.general_fault(true);
println!( println!(
"Low water, current percent is {}, minimum warn level is {}", "Low water, current percent is {}, minimum warn level is {}",
percent as u8, config.tank_warn_percent percent as u8, config.tank.tank_warn_percent
); );
rv.warn_level = true; rv.warn_level = true;
} }
if config.tank_empty_percent < percent as u8 { if config.tank.tank_empty_percent < percent as u8 {
println!( println!(
"Enough water, current percent is {}, minimum empty level is {}", "Enough water, current percent is {}, minimum empty level is {}",
percent as u8, config.tank_empty_percent percent as u8, config.tank.tank_empty_percent
); );
rv.enough_water = true; rv.enough_water = true;
} }
@ -624,7 +628,7 @@ fn determine_state_target_moisture_for_plant(
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
plant: usize, plant: usize,
state: &mut PlantState, state: &mut PlantState,
config: &Config, config: &PlantControllerConfig,
tank_state: &TankState, tank_state: &TankState,
cur: DateTime<Tz>, cur: DateTime<Tz>,
) { ) {
@ -671,7 +675,7 @@ fn determine_state_target_moisture_for_plant(
if a_low || b_low { if a_low || b_low {
state.dry = true; state.dry = true;
if tank_state.sensor_error && !config.tank_allow_pumping_if_sensor_error { if tank_state.sensor_error && !config.tank.tank_allow_pumping_if_sensor_error {
//ignore is ok //ignore is ok
} else if !tank_state.enough_water { } else if !tank_state.enough_water {
state.no_water = true; state.no_water = true;
@ -714,7 +718,7 @@ fn determine_state_timer_only_for_plant(
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
plant: usize, plant: usize,
state: &mut PlantState, state: &mut PlantState,
config: &Config, config: &PlantControllerConfig,
tank_state: &TankState, tank_state: &TankState,
cur: DateTime<Tz>, cur: DateTime<Tz>,
) { ) {
@ -730,7 +734,7 @@ fn determine_state_timer_only_for_plant(
state.next_pump = Some(europe_time); state.next_pump = Some(europe_time);
state.cooldown = true; state.cooldown = true;
} else { } else {
if tank_state.sensor_error && !config.tank_allow_pumping_if_sensor_error { if tank_state.sensor_error && !config.tank.tank_allow_pumping_if_sensor_error {
state.do_water = true; state.do_water = true;
} else if !tank_state.enough_water { } else if !tank_state.enough_water {
state.no_water = true; state.no_water = true;
@ -752,7 +756,7 @@ fn determine_state_timer_and_deadzone_for_plant(
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
plant: usize, plant: usize,
state: &mut PlantState, state: &mut PlantState,
config: &Config, config: &PlantControllerConfig,
tank_state: &TankState, tank_state: &TankState,
cur: DateTime<Tz>, cur: DateTime<Tz>,
) { ) {
@ -776,7 +780,7 @@ fn determine_state_timer_and_deadzone_for_plant(
state.out_of_work_hour = true; state.out_of_work_hour = true;
} }
if !state.cooldown && !state.out_of_work_hour { if !state.cooldown && !state.out_of_work_hour {
if tank_state.sensor_error && !config.tank_allow_pumping_if_sensor_error { if tank_state.sensor_error && !config.tank.tank_allow_pumping_if_sensor_error {
state.do_water = true; state.do_water = true;
} else if !tank_state.enough_water { } else if !tank_state.enough_water {
state.no_water = true; state.no_water = true;
@ -799,7 +803,7 @@ fn determine_next_plant(
cur: DateTime<Tz>, cur: DateTime<Tz>,
tank_state: &TankState, tank_state: &TankState,
water_frozen: bool, water_frozen: bool,
config: &Config, config: &PlantControllerConfig,
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
) -> Option<usize> { ) -> Option<usize> {
for plant in 0..PLANT_COUNT { for plant in 0..PLANT_COUNT {
@ -850,7 +854,7 @@ fn determine_next_plant(
fn update_plant_state( fn update_plant_state(
plantstate: &mut [PlantState; PLANT_COUNT], plantstate: &mut [PlantState; PLANT_COUNT],
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
config: &Config, config: &PlantControllerConfig,
) { ) {
for plant in 0..PLANT_COUNT { for plant in 0..PLANT_COUNT {
let state = &plantstate[plant]; let state = &plantstate[plant];

View File

@ -2,7 +2,6 @@ use bq34z100::{Bq34Z100Error, Bq34z100g1, Bq34z100g1Driver};
use ds323x::{DateTimeAccess, Ds323x}; use ds323x::{DateTimeAccess, Ds323x};
use eeprom24x::page_size::No;
use eeprom24x::{Eeprom24x, SlaveAddr}; use eeprom24x::{Eeprom24x, SlaveAddr};
use embedded_hal_bus::i2c::MutexDevice; use embedded_hal_bus::i2c::MutexDevice;
use embedded_svc::wifi::{ use embedded_svc::wifi::{
@ -14,7 +13,6 @@ use esp_idf_hal::adc::{attenuation, Resolution};
use esp_idf_hal::i2c::{APBTickType, I2cConfig, I2cDriver, I2cError}; use esp_idf_hal::i2c::{APBTickType, I2cConfig, I2cDriver, I2cError};
use esp_idf_hal::units::FromValueType; use esp_idf_hal::units::FromValueType;
use esp_idf_svc::eventloop::EspSystemEventLoop; use esp_idf_svc::eventloop::EspSystemEventLoop;
use esp_idf_svc::io::vfs;
use esp_idf_svc::ipv4::IpInfo; use esp_idf_svc::ipv4::IpInfo;
use esp_idf_svc::mqtt::client::QoS::AtLeastOnce; use esp_idf_svc::mqtt::client::QoS::AtLeastOnce;
use esp_idf_svc::mqtt::client::QoS::ExactlyOnce; use esp_idf_svc::mqtt::client::QoS::ExactlyOnce;
@ -30,8 +28,7 @@ use anyhow::{anyhow, Context};
use anyhow::{bail, Ok, Result}; use anyhow::{bail, Ok, Result};
use serde::Serialize; use serde::Serialize;
use std::ffi::CString; use std::ffi::CString;
use std::fs::{self, DirEntry, File}; use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::Path; use std::path::Path;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
@ -52,10 +49,10 @@ use esp_idf_hal::prelude::Peripherals;
use esp_idf_hal::reset::ResetReason; use esp_idf_hal::reset::ResetReason;
use esp_idf_svc::sntp::{self, SyncStatus}; use esp_idf_svc::sntp::{self, SyncStatus};
use esp_idf_svc::systime::EspSystemTime; use esp_idf_svc::systime::EspSystemTime;
use esp_idf_sys::{esp, esp_spiffs_check, f_opendir, gpio_hold_dis, gpio_hold_en, vTaskDelay, EspError}; use esp_idf_sys::{esp, esp_spiffs_check, gpio_hold_dis, gpio_hold_en, vTaskDelay, EspError};
use one_wire_bus::OneWire; use one_wire_bus::OneWire;
use crate::config::{self, Config}; use crate::config::{self, PlantControllerConfig};
use crate::{plant_hal, STAY_ALIVE}; use crate::{plant_hal, STAY_ALIVE};
//Only support for 8 right now! //Only support for 8 right now!
@ -171,37 +168,35 @@ pub struct PlantCtrlBoard<'a> {
} }
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct FileInfo{ pub struct FileInfo {
filename:String, filename: String,
size:usize size: usize,
} }
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct FileList{ pub struct FileList {
files: Vec<FileInfo>, files: Vec<FileInfo>,
file_system_corrupt: Option<String>, file_system_corrupt: Option<String>,
iter_error: Option<String>, iter_error: Option<String>,
} }
impl PlantCtrlBoard<'_> { impl PlantCtrlBoard<'_> {
pub fn list_files(&self, filename:&str) -> FileList { pub fn list_files(&self, filename: &str) -> FileList {
let storage = CString::new(SPIFFS_PARTITION_NAME).unwrap(); let storage = CString::new(SPIFFS_PARTITION_NAME).unwrap();
let error = unsafe { let error = unsafe {
esp!{ esp! {
esp_spiffs_check(storage.as_ptr()) esp_spiffs_check(storage.as_ptr())
} }
}; };
let mut file_system_corrupt = match error { let mut file_system_corrupt = match error {
OkStd(_) => { OkStd(_) => None,
None
},
Err(err) => { Err(err) => {
println!("Corrupt spiffs {err:?}"); println!("Corrupt spiffs {err:?}");
Some(format!("{err:?}")) Some(format!("{err:?}"))
}, }
}; };
let mut iter_error = None; let mut iter_error = None;
let mut result = Vec::new(); let mut result = Vec::new();
@ -214,49 +209,53 @@ impl PlantCtrlBoard<'_> {
println!("start loop"); println!("start loop");
match item { match item {
OkStd(file) => { OkStd(file) => {
let f = FileInfo{ let f = FileInfo {
filename: file.file_name().into_string().unwrap(), filename: file.file_name().into_string().unwrap(),
size: file.metadata().and_then(|it| core::result::Result::Ok(it.len())).unwrap_or_default() as usize size: file
.metadata()
.and_then(|it| core::result::Result::Ok(it.len()))
.unwrap_or_default()
as usize,
}; };
println!("fileinfo {f:?}"); println!("fileinfo {f:?}");
result.push(f); result.push(f);
}, }
Err(err) => { Err(err) => {
iter_error = Some (format!("{err:?}")); iter_error = Some(format!("{err:?}"));
break; break;
}, }
} }
} }
}, }
Err(err) => { Err(err) => {
file_system_corrupt = Some(format!("{err:?}")); file_system_corrupt = Some(format!("{err:?}"));
}, }
} }
return FileList{ return FileList {
file_system_corrupt, file_system_corrupt,
files: result, files: result,
iter_error iter_error,
}; };
} }
pub fn delete_file(&self, filename:&str) -> Result<()>{ pub fn delete_file(&self, filename: &str) -> Result<()> {
let filepath = Path::new(BASE_PATH).join(Path::new(filename)); let filepath = Path::new(BASE_PATH).join(Path::new(filename));
match (fs::remove_file(filepath)){ match fs::remove_file(filepath) {
OkStd(_) => Ok(()), OkStd(_) => Ok(()),
Err(err) => { Err(err) => {
bail!(format!("{err:?}")) bail!(format!("{err:?}"))
}, }
} }
} }
pub fn get_file_handle(&self, filename:&str, write:bool) -> Result<File> { pub fn get_file_handle(&self, filename: &str, write: bool) -> Result<File> {
let filepath = Path::new(BASE_PATH).join(Path::new(filename)); let filepath = Path::new(BASE_PATH).join(Path::new(filename));
return Ok(if write { return Ok(if write {
File::create(filepath)? File::create(filepath)?
} else { } else {
File::open(filepath)? File::open(filepath)?
}) });
} }
pub fn is_day(&self) -> bool { pub fn is_day(&self) -> bool {
@ -495,7 +494,10 @@ impl PlantCtrlBoard<'_> {
delay.delay_ms(10); delay.delay_ms(10);
let unscaled = self.signal_counter.get_counter_value()? as i32; let unscaled = self.signal_counter.get_counter_value()? as i32;
let hz = (unscaled as f32 * factor) as i32; let hz = (unscaled as f32 * factor) as i32;
println!("raw measure unscaled {} hz {}, plant {} sensor {:?}",unscaled, hz, plant, sensor); println!(
"raw measure unscaled {} hz {}, plant {} sensor {:?}",
unscaled, hz, plant, sensor
);
results[repeat] = hz; results[repeat] = hz;
//println!("Measuring {:?} @ {} with {}", sensor, plant, hz); //println!("Measuring {:?} @ {} with {}", sensor, plant, hz);
} }
@ -598,7 +600,7 @@ impl PlantCtrlBoard<'_> {
max_files: 5, max_files: 5,
format_if_mount_failed: true, format_if_mount_failed: true,
}; };
//TODO check fielsystem esp_spiffs_check //TODO check fielsystem esp_spiffs_check
unsafe { unsafe {
@ -661,13 +663,13 @@ impl PlantCtrlBoard<'_> {
} }
} }
pub fn get_config(&mut self) -> Result<config::Config> { pub fn get_config(&mut self) -> Result<config::PlantControllerConfig> {
let cfg = File::open(CONFIG_FILE)?; let cfg = File::open(CONFIG_FILE)?;
let config: Config = serde_json::from_reader(cfg)?; let config: PlantControllerConfig = serde_json::from_reader(cfg)?;
Ok(config) Ok(config)
} }
pub fn set_config(&mut self, config: &Config) -> Result<()> { pub fn set_config(&mut self, config: &PlantControllerConfig) -> Result<()> {
let mut cfg = File::create(CONFIG_FILE)?; let mut cfg = File::create(CONFIG_FILE)?;
serde_json::to_writer(&mut cfg, &config)?; serde_json::to_writer(&mut cfg, &config)?;
println!("Wrote config config {:?}", config); println!("Wrote config config {:?}", config);
@ -735,9 +737,17 @@ impl PlantCtrlBoard<'_> {
config.exists() config.exists()
} }
pub fn mqtt(&mut self, config: &Config) -> Result<()> { pub fn mqtt(&mut self, config: &PlantControllerConfig) -> Result<()> {
let base_topic = config.base_topic.as_ref().context("missing base topic")?; let base_topic = config
let mqtt_url = config.mqtt_url.as_ref().context("missing mqtt url")?; .network
.base_topic
.as_ref()
.context("missing base topic")?;
let mqtt_url = config
.network
.mqtt_url
.as_ref()
.context("missing mqtt url")?;
let last_will_topic = format!("{}/state", base_topic); let last_will_topic = format!("{}/state", base_topic);
let mqtt_client_config = MqttClientConfiguration { let mqtt_client_config = MqttClientConfiguration {
@ -865,7 +875,12 @@ impl PlantCtrlBoard<'_> {
bail!("Mqtt did not fire connection callback in time"); bail!("Mqtt did not fire connection callback in time");
} }
pub fn mqtt_publish(&mut self, config: &Config, subtopic: &str, message: &[u8]) -> Result<()> { pub fn mqtt_publish(
&mut self,
config: &PlantControllerConfig,
subtopic: &str,
message: &[u8],
) -> Result<()> {
if self.mqtt_client.is_none() { if self.mqtt_client.is_none() {
return Ok(()); return Ok(());
} }
@ -880,7 +895,7 @@ impl PlantCtrlBoard<'_> {
let client = self.mqtt_client.as_mut().unwrap(); let client = self.mqtt_client.as_mut().unwrap();
let mut full_topic: heapless::String<256> = heapless::String::new(); let mut full_topic: heapless::String<256> = heapless::String::new();
if full_topic if full_topic
.push_str(&config.base_topic.as_ref().unwrap()) .push_str(&config.network.base_topic.as_ref().unwrap())
.is_err() .is_err()
{ {
println!("Some error assembling full_topic 1"); println!("Some error assembling full_topic 1");

View File

@ -1,20 +1,23 @@
//offer ota and config mode //offer ota and config mode
use std::{ use std::{
collections::VecDeque, fs, io::{BufRead, Read, Write}, str::from_utf8, sync::{atomic::AtomicBool, Arc} io::{BufRead, BufReader, Read, Write},
str::from_utf8,
sync::{atomic::AtomicBool, Arc},
}; };
use crate::{espota::OtaUpdate, map_range_moisture, plant_hal::FileInfo, BOARD_ACCESS}; use crate::{espota::OtaUpdate, map_range_moisture, plant_hal::FileInfo, BOARD_ACCESS};
use anyhow::bail;
use chrono::DateTime; use chrono::DateTime;
use url::Url;
use core::result::Result::Ok; use core::result::Result::Ok;
use embedded_svc::http::Method; use embedded_svc::http::Method;
use esp_idf_hal::delay::Delay; use esp_idf_hal::delay::Delay;
use esp_idf_svc::{http::server::{Configuration, EspHttpConnection, EspHttpServer, Request}}; use esp_idf_svc::http::server::{Configuration, EspHttpConnection, EspHttpServer, Request};
use heapless::String; use heapless::String;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url;
use crate::config::Config; use crate::config::PlantControllerConfig;
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
struct SSIDList<'a> { struct SSIDList<'a> {
@ -87,8 +90,8 @@ fn get_data(
match a_pct { match a_pct {
Ok(result) => { Ok(result) => {
a.push(result); a.push(result);
}, }
Err(err) => { Err(_) => {
a.push(200); a.push(200);
} }
} }
@ -97,19 +100,18 @@ fn get_data(
match b_pct { match b_pct {
Ok(result) => { Ok(result) => {
b.push(result); b.push(result);
}, }
Err(err) => { Err(_) => {
b.push(200); b.push(200);
} }
} }
} }
let data = LoadData { let data = LoadData {
rtc: rtc.as_str(), rtc: rtc.as_str(),
native: native.as_str(), native: native.as_str(),
moisture_a: a, moisture_a: a,
moisture_b: b moisture_b: b,
}; };
let json = serde_json::to_string(&data)?; let json = serde_json::to_string(&data)?;
@ -122,7 +124,7 @@ fn get_config(
let mut board = BOARD_ACCESS.lock().unwrap(); let mut board = BOARD_ACCESS.lock().unwrap();
let json = match board.get_config() { let json = match board.get_config() {
Ok(config) => serde_json::to_string(&config)?, Ok(config) => serde_json::to_string(&config)?,
Err(_) => serde_json::to_string(&Config::default())?, Err(_) => serde_json::to_string(&PlantControllerConfig::default())?,
}; };
anyhow::Ok(Some(json)) anyhow::Ok(Some(json))
} }
@ -134,7 +136,7 @@ fn set_config(
let read = request.read(&mut buf)?; let read = request.read(&mut buf)?;
let actual_data = &buf[0..read]; let actual_data = &buf[0..read];
println!("Raw data {}", from_utf8(actual_data).unwrap()); println!("Raw data {}", from_utf8(actual_data).unwrap());
let config: Config = serde_json::from_slice(actual_data)?; let config: PlantControllerConfig = serde_json::from_slice(actual_data)?;
let mut board = BOARD_ACCESS.lock().unwrap(); let mut board = BOARD_ACCESS.lock().unwrap();
board.set_config(&config)?; board.set_config(&config)?;
anyhow::Ok(Some("saved".to_owned())) anyhow::Ok(Some("saved".to_owned()))
@ -176,7 +178,7 @@ fn wifi_scan(
fn list_files( fn list_files(
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 filename = query_param(request.uri(),"filename").unwrap_or_default(); let filename = query_param(request.uri(), "filename").unwrap_or_default();
let board = BOARD_ACCESS.lock().unwrap(); let board = BOARD_ACCESS.lock().unwrap();
let result = board.list_files(&filename); let result = board.list_files(&filename);
let file_list_json = serde_json::to_string(&result)?; let file_list_json = serde_json::to_string(&result)?;
@ -215,17 +217,54 @@ fn ota(
finalizer.restart(); finalizer.restart();
} }
fn query_param(uri:&str, param_name:&str) -> Option<std::string::String>{ fn flash_bq(filename: &str, dryrun: bool) -> anyhow::Result<()> {
let mut board = BOARD_ACCESS.lock().unwrap();
let mut toggle = true;
let delay = Delay::new(1);
let file_handle = board.get_file_handle(filename, false)?;
let mut reader = BufReader::with_capacity(512, file_handle).lines();
let mut line = 0;
loop {
board.general_fault(toggle);
toggle = !toggle;
delay.delay_us(2);
line += 1;
match reader.next() {
Some(next) => {
let input = next?;
println!("flashing bq34z100 dryrun:{dryrun} line {line} payload: {input}");
match board.flash_bq34_z100(&input, dryrun) {
Ok(_) => {
println!("ok")
}
Err(err) => {
bail!(
"Error flashing bq34z100 in dryrun: {dryrun} line: {line} error: {err}"
)
}
}
}
None => break,
}
}
println!("Finished flashing file {line} lines processed");
board.general_fault(false);
return anyhow::Ok(());
}
fn query_param(uri: &str, param_name: &str) -> Option<std::string::String> {
println!("{uri} get {param_name}"); println!("{uri} get {param_name}");
let parsed = Url::parse(&format!("http://127.0.0.1/{uri}")).unwrap(); let parsed = Url::parse(&format!("http://127.0.0.1/{uri}")).unwrap();
let value = parsed.query_pairs().filter(|it| it.0 == param_name).next(); let value = parsed.query_pairs().filter(|it| it.0 == param_name).next();
match value { match value {
Some(found) => { Some(found) => {
return Some(found.1.into_owned()); return Some(found.1.into_owned());
}, }
None => { None => return None,
return None
},
} }
} }
@ -284,18 +323,20 @@ pub fn httpd(_reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
}) })
.unwrap(); .unwrap();
server server
.fn_handler("/files", Method::Get, move |request| { .fn_handler("/files", Method::Get, move |request| {
handle_error_to500(request, list_files) handle_error_to500(request, list_files)
}) })
.unwrap(); .unwrap();
server server
.fn_handler("/file", Method::Get, move |request| { .fn_handler("/file", Method::Get, move |request| {
let filename = query_param(request.uri(),"filename").unwrap(); let filename = query_param(request.uri(), "filename").unwrap();
let file_handle = BOARD_ACCESS.lock().unwrap().get_file_handle(&filename, false); let file_handle = BOARD_ACCESS
.lock()
.unwrap()
.get_file_handle(&filename, false);
match file_handle { match file_handle {
Ok(mut file_handle) => { Ok(mut file_handle) => {
let headers = [("Access-Control-Allow-Origin", "*")];
let headers = [("Access-Control-Allow-Origin","*")];
let mut response = request.into_response(200, None, &headers)?; let mut response = request.into_response(200, None, &headers)?;
const BUFFER_SIZE: usize = 512; const BUFFER_SIZE: usize = 512;
let mut buffer: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; let mut buffer: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
@ -303,7 +344,10 @@ pub fn httpd(_reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
loop { loop {
let read = file_handle.read(&mut buffer)?; let read = file_handle.read(&mut buffer)?;
total_read += read; total_read += read;
println!("sending {read} bytes of {total_read} for file {}", &filename); println!(
"sending {read} bytes of {total_read} for file {}",
&filename
);
let to_write = &buffer[0..read]; let to_write = &buffer[0..read];
response.write(to_write)?; response.write(to_write)?;
println!("wrote {read} bytes of {total_read} for file {filename}"); println!("wrote {read} bytes of {total_read} for file {filename}");
@ -313,8 +357,7 @@ pub fn httpd(_reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
} }
drop(file_handle); drop(file_handle);
response.flush()?; response.flush()?;
}
},
Err(err) => { Err(err) => {
//todo set headers here for filename to be error //todo set headers here for filename to be error
let error_text = err.to_string(); let error_text = err.to_string();
@ -327,8 +370,11 @@ pub fn httpd(_reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
.unwrap(); .unwrap();
server server
.fn_handler("/file", Method::Post, move |mut request| { .fn_handler("/file", Method::Post, move |mut request| {
let filename = query_param(request.uri(),"filename").unwrap(); let filename = query_param(request.uri(), "filename").unwrap();
let file_handle = BOARD_ACCESS.lock().unwrap().get_file_handle(&filename,true); let file_handle = BOARD_ACCESS
.lock()
.unwrap()
.get_file_handle(&filename, true);
match file_handle { match file_handle {
Ok(mut file_handle) => { Ok(mut file_handle) => {
const BUFFER_SIZE: usize = 512; const BUFFER_SIZE: usize = 512;
@ -346,7 +392,7 @@ pub fn httpd(_reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
} }
} }
cors_response(request, 200, &format!("saved {total_read} bytes"))?; cors_response(request, 200, &format!("saved {total_read} bytes"))?;
}, }
Err(err) => { Err(err) => {
//todo set headers here for filename to be error //todo set headers here for filename to be error
let error_text = err.to_string(); let error_text = err.to_string();
@ -357,90 +403,51 @@ pub fn httpd(_reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
anyhow::Ok(()) anyhow::Ok(())
}) })
.unwrap(); .unwrap();
server server
.fn_handler("/file", Method::Delete, move |request| { .fn_handler("/file", Method::Delete, move |request| {
let filename = query_param(request.uri(),"filename").unwrap(); let filename = query_param(request.uri(), "filename").unwrap();
let copy = filename.clone(); let copy = filename.clone();
let board = BOARD_ACCESS.lock().unwrap(); let board = BOARD_ACCESS.lock().unwrap();
match board.delete_file(&filename) { match board.delete_file(&filename) {
Ok(_) => { Ok(_) => {
let info = format!("Deleted file {copy}"); let info = format!("Deleted file {copy}");
cors_response(request, 200, &info)?; cors_response(request, 200, &info)?;
}, }
Err(err) => { Err(err) => {
let info = format!("Could not delete file {copy} {err:?}"); let info = format!("Could not delete file {copy} {err:?}");
cors_response(request, 400, &info)?; cors_response(request, 400, &info)?;
}, }
} }
anyhow::Ok(()) anyhow::Ok(())
}) })
.unwrap(); .unwrap();
server server
.fn_handler("/flashbattery", Method::Post, move |mut request| { .fn_handler("/flashbattery", Method::Post, move |request| {
let filename = query_param(request.uri(),"filename").unwrap();
let mut board = BOARD_ACCESS.lock().unwrap(); let dryrun = true;
let mut buffer: [u8; 128] = [0; 128]; match flash_bq(&filename, false) {
let mut line_buffer: VecDeque<u8> = VecDeque::new(); Ok(_) => {
if !dryrun {
let is_dry_run = !request.uri().ends_with("?flash=true"); match flash_bq(&filename, true) {
let mut total_read: usize = 0; Ok(_) => {
cors_response(request, 200, "Sucessfully flashed bq34z100")?;
let mut toggle = true; },
let delay = Delay::new(1); Err(err) => {
todo!("Write to storage before attempting to flash!"); let info = format!("Could not flash bq34z100, could be bricked now! {filename} {err:?}");
loop { cors_response(request, 500, &info)?;
delay.delay_us(2); },
let read = request.read(&mut buffer).unwrap(); }
total_read += read; } else {
if read == 0 { cors_response(request, 200, "Sucessfully processed bq34z100")?;
if line_buffer.len() > 0 {
println!("No further body but no endline");
let mut line = std::string::String::new();
line_buffer.read_to_string(&mut line).unwrap();
let msg = format!("Finished reading, but there is still some leftover in buffer and no full line {line}<br>");
println!("{}", msg);
let mut response = request.into_status_response(400_u16).unwrap();
response.write(msg.as_bytes()).unwrap();
response.flush().unwrap();
return anyhow::Ok(())
} }
break; },
} Err(err) => {
let to_write = &buffer[0..read]; let info = format!("Could not process firmware file for, bq34z100, refusing to flash! {filename} {err:?}");
cors_response(request, 500, &info)?;
line_buffer.write_all(to_write).unwrap(); },
board.general_fault(toggle); };
toggle = !toggle;
loop {
let has_line = line_buffer.contains(&b'\n');
if !has_line {
break;
}
let mut line = std::string::String::new();
line_buffer.read_line(&mut line)?;
let line2 = &line[0..line.len()-1];
println!("Processing dry:{} line {}", is_dry_run, line2);
let validate = board.flash_bq34_z100(&line2, is_dry_run);
delay.delay_us(2);
if validate.is_err() {
let mut response = request.into_status_response(400_u16).unwrap();
let err = validate.unwrap_err();
let err_str = err.to_string();
let err_msg = err_str.as_bytes();
println!("Error writing {}", err_str);
response
.write(err_msg)
.unwrap();
return anyhow::Ok(())
}
}
}
let mut response = request.into_status_response(200_u16).unwrap();
let msg = format!("Finished writing {total_read} bytes<br>");
response.write(msg.as_bytes()).unwrap();
board.general_fault(false);
anyhow::Ok(()) anyhow::Ok(())
}) })
.unwrap(); .unwrap();
@ -448,7 +455,7 @@ pub fn httpd(_reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
server server
.fn_handler("/", Method::Get, move |request| { .fn_handler("/", Method::Get, move |request| {
let mut response = request.into_ok_response()?; let mut response = request.into_ok_response()?;
response.write(include_bytes!("index.html"))?;let mut buf = [0_u8; 3072]; response.write(include_bytes!("index.html"))?;
anyhow::Ok(()) anyhow::Ok(())
}) })
.unwrap(); .unwrap();
@ -458,7 +465,6 @@ pub fn httpd(_reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
.into_ok_response()? .into_ok_response()?
.write(include_bytes!("favicon.ico"))?; .write(include_bytes!("favicon.ico"))?;
anyhow::Ok(()) anyhow::Ok(())
}) })
.unwrap(); .unwrap();
server server
@ -467,14 +473,17 @@ pub fn httpd(_reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
.into_ok_response()? .into_ok_response()?
.write(include_bytes!("bundle.js"))?; .write(include_bytes!("bundle.js"))?;
anyhow::Ok(()) anyhow::Ok(())
}) })
.unwrap(); .unwrap();
server server
} }
fn cors_response(request: Request<&mut EspHttpConnection>, status: u16, body: &str) -> Result<(), anyhow::Error>{ fn cors_response(
let headers = [("Access-Control-Allow-Origin","*")]; request: Request<&mut EspHttpConnection>,
status: u16,
body: &str,
) -> Result<(), anyhow::Error> {
let headers = [("Access-Control-Allow-Origin", "*")];
let mut response = request.into_response(status, None, &headers)?; let mut response = request.into_response(status, None, &headers)?;
response.write(body.as_bytes())?; response.write(body.as_bytes())?;
response.flush()?; response.flush()?;
@ -504,4 +513,4 @@ fn handle_error_to500(
} }
} }
return anyhow::Ok(()); return anyhow::Ok(());
} }