Compare commits

...

3 Commits

Author SHA1 Message Date
bfee21796a fix rollback and windows build 2024-02-21 15:36:26 +01:00
57a0971c4b also print mode to mqtt 2024-02-17 18:43:36 +01:00
556e64740a allow build to run on windows as well 2024-02-17 18:40:11 +01:00
7 changed files with 190 additions and 109 deletions

View File

@ -4,8 +4,8 @@ target = "xtensa-esp32-espidf"
[target.xtensa-esp32-espidf]
linker = "ldproxy"
#runner = "espflash flash --monitor --partition-table partitions.csv" # Select this runner for espflash v2.x.x
runner = "espflash flash --monitor --baud 921600 --partition-table partitions.csv" # Select this runner for espflash v2.x.x
#runner = "cargo runner"
#runner = "espflash flash --monitor --baud 921600 --partition-table partitions.csv" # Select this runner for espflash v2.x.x
runner = "cargo runner"
rustflags = [ "--cfg", "espidf_time64"] # Extending time_t for ESP IDF 5: https://github.com/esp-rs/rust/issues/110
[unstable]

View File

@ -6,20 +6,37 @@ fn main() {
Command::new("rm")
.arg("./src/webserver/bundle.js")
.output()
.expect("failed to execute process");
.unwrap();
let output = Command::new("npx")
.arg("webpack")
.current_dir("./src_webpack")
.output()
.expect("failed to execute process");
println!("status: {}", output.status);
println!("stdout: {}", String::from_utf8_lossy(&output.stdout));
println!("stderr: {}", String::from_utf8_lossy(&output.stderr));
assert!(output.status.success());
match Command::new("cmd").spawn() {
Ok(_) => {
println!("Assuming build on windows");
let output = Command::new("cmd")
.arg("/K")
.arg("npx")
.arg("webpack")
.current_dir("./src_webpack")
.output()
.unwrap();
println!("status: {}", output.status);
println!("stdout: {}", String::from_utf8_lossy(&output.stdout));
println!("stderr: {}", String::from_utf8_lossy(&output.stderr));
assert!(output.status.success());
}
Err(_) => {
println!("Assuming build on linux");
let output = Command::new("bash")
.arg("webpack")
.current_dir("./src_webpack")
.output()
.unwrap();
println!("status: {}", output.status);
println!("stdout: {}", String::from_utf8_lossy(&output.stdout));
println!("stderr: {}", String::from_utf8_lossy(&output.stderr));
assert!(output.status.success());
}
}
embuild::espidf::sysenv::output();
let _ = EmitBuilder::builder().all_git().emit();
let _ = EmitBuilder::builder().all_git().all_build().emit();
}

View File

@ -1,3 +1,3 @@
[toolchain]
channel = "nightly"
toolchain = "esp"

View File

@ -5,4 +5,4 @@ CONFIG_ESP_MAIN_TASK_STACK_SIZE=20000
# This allows to use 1 ms granuality for thread sleeps (10 ms by default).
CONFIG_FREERTOS_HZ=1000
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=true
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y

View File

@ -60,7 +60,7 @@ pub struct Plant {
pub pump_hour_start: u8,
pub pump_hour_end: u8,
pub sensor_b: bool,
pub sensor_p: bool
pub sensor_p: bool,
}
impl Default for Plant {
fn default() -> Self {
@ -71,8 +71,8 @@ impl Default for Plant {
pump_hour_start: 8,
pump_hour_end: 20,
mode: Mode::OFF,
sensor_b : false,
sensor_p : false
sensor_b: false,
sensor_p: false,
}
}
}

View File

@ -1,16 +1,11 @@
use std::{
env,
sync::{atomic::AtomicBool, Arc, Mutex},
};
use std::sync::{atomic::AtomicBool, Arc, Mutex};
use chrono::{DateTime, Datelike, Duration, NaiveDateTime, Timelike};
use chrono_tz::{Europe::Berlin, Tz};
use config::Plant;
use embedded_svc::mqtt;
use esp_idf_hal::delay::Delay;
use esp_idf_sys::{
esp_deep_sleep, esp_restart, gpio_deep_sleep_hold_dis, gpio_deep_sleep_hold_en, vTaskDelay,
CONFIG_FREERTOS_HZ,
esp_deep_sleep, esp_ota_get_app_partition_count, esp_ota_get_running_partition, esp_ota_get_state_partition, esp_ota_img_states_t, esp_ota_img_states_t_ESP_OTA_IMG_ABORTED, esp_ota_img_states_t_ESP_OTA_IMG_INVALID, esp_ota_img_states_t_ESP_OTA_IMG_NEW, esp_ota_img_states_t_ESP_OTA_IMG_PENDING_VERIFY, esp_ota_img_states_t_ESP_OTA_IMG_UNDEFINED, esp_ota_img_states_t_ESP_OTA_IMG_VALID, esp_restart, gpio_deep_sleep_hold_dis, gpio_deep_sleep_hold_en, vTaskDelay, CONFIG_FREERTOS_HZ
};
use log::error;
use once_cell::sync::Lazy;
@ -19,7 +14,7 @@ use serde::{Deserialize, Serialize};
use crate::{
config::{Config, WifiConfig},
espota::rollback_and_reboot,
espota::{mark_app_valid, rollback_and_reboot},
webserver::webserver::{httpd, httpd_initial},
};
mod config;
@ -84,7 +79,7 @@ struct PlantState {
sensor_error_b: Option<SensorError>,
sensor_error_p: Option<SensorError>,
out_of_work_hour: bool,
next_pump: Option<DateTime<Tz>>
next_pump: Option<DateTime<Tz>>,
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]
@ -121,28 +116,41 @@ fn safe_main() -> anyhow::Result<()> {
log::info!("Startup Rust");
let git_hash = env!("VERGEN_GIT_DESCRIBE");
println!("Version useing git has {}", git_hash);
let build_timestamp = env!("VERGEN_BUILD_TIMESTAMP");
println!(
"Version useing git has {} build on {}",
git_hash, build_timestamp
);
let partition_state: embedded_svc::ota::SlotState = embedded_svc::ota::SlotState::Unknown;
match esp_idf_svc::ota::EspOta::new() {
Ok(ota) => {
//match ota.get_running_slot(){
// Ok(slot) => {
// partition_state = slot.state;
// println!(
// "Booting from {} with state {:?}",
// slot.label, partition_state
// );
//},
// Err(err) => {
// println!("Error getting running slot {}", err);
// },
//}
let count = unsafe { esp_ota_get_app_partition_count() };
println!("Partition count is {}", count);
let mut ota_state: esp_ota_img_states_t = 0;
let running_partition = unsafe { esp_ota_get_running_partition() };
let address = unsafe {
(*running_partition).address
};
println!("Partition address is {}", address);
let ota_state_string = unsafe {
esp_ota_get_state_partition(running_partition, &mut ota_state);
if ota_state == esp_ota_img_states_t_ESP_OTA_IMG_NEW {
format!("Partition state is {}", "ESP_OTA_IMG_NEW")
} else if ota_state == esp_ota_img_states_t_ESP_OTA_IMG_PENDING_VERIFY {
format!("Partition state is {}", "ESP_OTA_IMG_PENDING_VERIFY")
} else if ota_state == esp_ota_img_states_t_ESP_OTA_IMG_VALID {
format!("Partition state is {}", "ESP_OTA_IMG_VALID")
} else if ota_state == esp_ota_img_states_t_ESP_OTA_IMG_INVALID {
format!("Partition state is {}", "ESP_OTA_IMG_INVALID")
} else if ota_state == esp_ota_img_states_t_ESP_OTA_IMG_ABORTED {
format!("Partition state is {}", "ESP_OTA_IMG_ABORTED")
} else if ota_state == esp_ota_img_states_t_ESP_OTA_IMG_UNDEFINED {
format!("Partition state is {}", "ESP_OTA_IMG_UNDEFINED")
} else {
format!("Partition state is {}", ota_state)
}
Err(err) => {
println!("Error obtaining ota info {}", err);
}
}
};
println!("{}", ota_state_string);
println!("Board hal init");
let mut board: std::sync::MutexGuard<'_, PlantCtrlBoard<'_>> = BOARD_ACCESS.lock().unwrap();
@ -212,13 +220,9 @@ fn safe_main() -> anyhow::Result<()> {
}
Err(err) => {
if board.is_wifi_config_file_existant() {
match partition_state {
embedded_svc::ota::SlotState::Invalid
| embedded_svc::ota::SlotState::Unverified => {
println!("Config seem to be unparsable after upgrade, reverting");
rollback_and_reboot()?;
}
_ => {}
if ota_state == esp_ota_img_states_t_ESP_OTA_IMG_PENDING_VERIFY {
println!("Config seem to be unparsable after upgrade, reverting");
rollback_and_reboot()?;
}
}
println!("Missing wifi config, entering initial config mode {}", err);
@ -289,15 +293,26 @@ fn safe_main() -> anyhow::Result<()> {
if online_mode == OnlineMode::Online {
let _ = board.mqtt_publish(&config, "/firmware/githash", git_hash.as_bytes());
let _ = board.mqtt_publish(&config, "/firmware/buildtime", build_timestamp.as_bytes());
let _ = board.mqtt_publish(&config, "/firmware/last_online", europe_time.to_rfc3339().as_bytes());
let _ = board.mqtt_publish(&config, "/firmware/ota_state", ota_state_string.as_bytes());
let _ = board.mqtt_publish(&config, "/firmware/partition_address", format!("{:#06x}",address).as_bytes());
let _ = board.mqtt_publish(&config, "/state", "online".as_bytes());
let _ = board.mqtt_publish(&config, "/last_online", europe_time.to_rfc3339().as_bytes());
publish_battery_state(&mut board, &config);
}
let tank_state = determine_tank_state(&mut board, &config);
if online_mode == OnlineMode::Online {
if tank_state.sensor_error {
let _ = board.mqtt_publish(&config, "/water/ml", "error".to_string().as_bytes());
} else {
let _ = board.mqtt_publish(&config, "/water/ml", tank_state.left_ml.to_string().as_bytes());
let _ = board.mqtt_publish(&config, "/water/enough_water", tank_state.enough_water.to_string().as_bytes());
let _ = board.mqtt_publish(&config, "/water/raw", tank_state.raw.to_string().as_bytes());
}
}
let mut water_frozen = false;
for _attempt in 0..5 {
@ -354,10 +369,9 @@ fn safe_main() -> anyhow::Result<()> {
board.store_consecutive_pump_count(plant, consecutive_pump_count);
let plant_config = config.plants[plant];
if plant_config.sensor_p {
match map_range_moisture(
board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32
board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32,
) {
Ok(p) => state.p = Some(p),
Err(err) => {
@ -377,36 +391,34 @@ fn safe_main() -> anyhow::Result<()> {
board.pump(plant, true)?;
board.last_pump_time(plant);
state.active = true;
for t in 0..plant_config.pump_time_s {
//FIXME do periodic pump test here and state update
for _ in 0..plant_config.pump_time_s {
unsafe { vTaskDelay(CONFIG_FREERTOS_HZ) };
if plant_config.sensor_p {
let moist = map_range_moisture(
board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32);
let moist = map_range_moisture(
board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32,
);
if online_mode == OnlineMode::Online {
let _ = board.mqtt_publish(
&config,
format!("/plant{}/Sensor P after", plant+1).as_str(),
format!("/plant{}/Sensor P after", plant + 1).as_str(),
option_to_string(moist.ok()).as_bytes(),
);
}
} else {
if online_mode == OnlineMode::Online {
let _ = board.mqtt_publish(
&config,
format!("/plant{}/Sensor P after", plant+1).as_str(),
format!("/plant{}/Sensor P after", plant + 1).as_str(),
"disabled".as_bytes(),
);
}
}
}
board.pump(plant, false)?;
if plant_config.sensor_p {
match map_range_moisture(
board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32
board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32,
) {
Ok(p) => state.after_p = Some(p),
Err(err) => {
@ -423,7 +435,6 @@ fn safe_main() -> anyhow::Result<()> {
//mqtt sync pump error value
}
}
}
None => {
println!("Nothing to do");
@ -435,7 +446,8 @@ fn safe_main() -> anyhow::Result<()> {
let mut light_state = LightState {
..Default::default()
};
light_state.is_day = board.is_day();
let is_day = board.is_day();
light_state.is_day = is_day;
light_state.out_of_work_hour = !in_time_range(
europe_time,
config.night_lamp_hour_start,
@ -490,12 +502,42 @@ fn safe_main() -> anyhow::Result<()> {
unsafe { gpio_deep_sleep_hold_dis() };
unsafe { gpio_deep_sleep_hold_en() };
let deep_sleep_duration_minutes: u32 = if state_of_charge < 10 {
if online_mode == OnlineMode::Online {
let _ = board.mqtt_publish(
&config,
"/deepsleep",
"Entering low voltage long deep sleep".as_bytes(),
);
}
12 * 60
} else if is_day {
if online_mode == OnlineMode::Online {
let _ = board.mqtt_publish(
&config,
"/deepsleep",
"Entering normal mode 20m deep sleep".as_bytes(),
);
}
20
} else {
if online_mode == OnlineMode::Online {
let _ = board.mqtt_publish(
&config,
"/deepsleep",
"Entering night mode 1h deep sleep".as_bytes(),
);
}
60
};
//determine next event
//is light out of work trigger soon?
//is battery low ??
//is deep sleep
unsafe { esp_deep_sleep(1000 * 1000 * 20) };
mark_app_valid();
unsafe { esp_deep_sleep(1000 * 1000 * 60 * deep_sleep_duration_minutes as u64) };
}
fn publish_battery_state(
@ -764,16 +806,17 @@ fn determine_state_target_moisture_for_plant(
}
}
}
//FIXME how to average analyze whatever?
let a_low = state.a.is_some() && state.a.unwrap() < plant_config.target_moisture;
let b_low = state.b.is_some() && state.b.unwrap() < plant_config.target_moisture;
if a_low || b_low {
state.dry = true;
if tank_state.sensor_error && !config.tank_allow_pumping_if_sensor_error
|| !tank_state.enough_water
{
if tank_state.sensor_error && !config.tank_allow_pumping_if_sensor_error {
//ignore is ok
}
else if !tank_state.enough_water {
state.no_water = true;
}
}
@ -901,24 +944,39 @@ fn determine_next_plant(
return None;
}
fn update_plant_state(plantstate: &mut [PlantState; PLANT_COUNT], board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, config: &Config){
fn update_plant_state(
plantstate: &mut [PlantState; PLANT_COUNT],
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
config: &Config,
) {
for plant in 0..PLANT_COUNT {
let state = &plantstate[plant];
let plant_config = config.plants[plant];
let _ = board.mqtt_publish(
&config,
format!("/plant{}/mode", plant + 1).as_str(),
match plant_config.mode {
config::Mode::OFF => "OFF".as_bytes(),
config::Mode::TargetMoisture => "TargetMoisture".as_bytes(),
config::Mode::TimerOnly => "TimerOnly".as_bytes(),
config::Mode::TimerAndDeadzone => "TimerAndDeadzone".as_bytes(),
},
);
let last_time = board.last_pump_time(plant);
let europe_time = last_time.with_timezone(&Berlin);
if europe_time.year() > 2023 {
let time = europe_time.to_rfc3339();
let _ = board.mqtt_publish(
&config,
format!("/plant{}/last pump", plant+1).as_str(),
format!("/plant{}/last pump", plant + 1).as_str(),
time.as_bytes(),
);
} else {
let _ = board.mqtt_publish(
&config,
format!("/plant{}/last pump", plant+1).as_str(),
format!("/plant{}/last pump", plant + 1).as_str(),
"N/A".as_bytes(),
);
}
@ -928,101 +986,100 @@ fn update_plant_state(plantstate: &mut [PlantState; PLANT_COUNT], board: &mut st
let time = next.to_rfc3339();
let _ = board.mqtt_publish(
&config,
format!("/plant{}/next pump", plant+1).as_str(),
format!("/plant{}/next pump", plant + 1).as_str(),
time.as_bytes(),
);
},
}
None => {
let _ = board.mqtt_publish(
&config,
format!("/plant{}/next pump", plant+1).as_str(),
format!("/plant{}/next pump", plant + 1).as_str(),
"N/A".as_bytes(),
);
},
}
}
let _ = board.mqtt_publish(
&config,
format!("/plant{}/Sensor A", plant+1).as_str(),
format!("/plant{}/Sensor A", plant + 1).as_str(),
option_to_string(state.a).as_bytes(),
);
if plant_config.sensor_b {
let _ = board.mqtt_publish(
&config,
format!("/plant{}/Sensor B", plant+1).as_str(),
format!("/plant{}/Sensor B", plant + 1).as_str(),
option_to_string(state.b).as_bytes(),
);
} else {
let _ = board.mqtt_publish(
&config,
format!("/plant{}/Sensor B", plant+1).as_str(),
format!("/plant{}/Sensor B", plant + 1).as_str(),
"disabled".as_bytes(),
);
}
if plant_config.sensor_p {
let _ = board.mqtt_publish(
&config,
format!("/plant{}/Sensor P before", plant+1).as_str(),
format!("/plant{}/Sensor P before", plant + 1).as_str(),
option_to_string(state.p).as_bytes(),
);
let _ = board.mqtt_publish(
&config,
format!("/plant{}/Sensor P after", plant+1).as_str(),
format!("/plant{}/Sensor P after", plant + 1).as_str(),
option_to_string(state.after_p).as_bytes(),
);
} else {
let _ = board.mqtt_publish(
&config,
format!("/plant{}/Sensor P before", plant+1).as_str(),
format!("/plant{}/Sensor P before", plant + 1).as_str(),
"disabled".as_bytes(),
);
let _ = board.mqtt_publish(
&config,
format!("/plant{}/Sensor P after", plant+1).as_str(),
format!("/plant{}/Sensor P after", plant + 1).as_str(),
"disabled".as_bytes(),
);
}
let _ = board.mqtt_publish(
&config,
format!("/plant{}/Should water", plant+1).as_str(),
format!("/plant{}/Should water", plant + 1).as_str(),
state.do_water.to_string().as_bytes(),
);
let _ = board.mqtt_publish(
&config,
format!("/plant{}/Is frozen", plant+1).as_str(),
format!("/plant{}/Is frozen", plant + 1).as_str(),
state.frozen.to_string().as_bytes(),
);
let _ = board.mqtt_publish(
&config,
format!("/plant{}/Is dry", plant+1).as_str(),
format!("/plant{}/Is dry", plant + 1).as_str(),
state.dry.to_string().as_bytes(),
);
let _ = board.mqtt_publish(
&config,
format!("/plant{}/Pump Error", plant+1).as_str(),
format!("/plant{}/Pump Error", plant + 1).as_str(),
state.pump_error.to_string().as_bytes(),
);
let _ = board.mqtt_publish(
&config,
format!("/plant{}/Pump Ineffective", plant+1).as_str(),
format!("/plant{}/Pump Ineffective", plant + 1).as_str(),
state.not_effective.to_string().as_bytes(),
);
let _ = board.mqtt_publish(
&config,
format!("/plant{}/Is in Cooldown", plant+1).as_str(),
format!("/plant{}/Is in Cooldown", plant + 1).as_str(),
state.cooldown.to_string().as_bytes(),
);
let _ = board.mqtt_publish(
&config,
format!("/plant{}/No Water", plant+1).as_str(),
format!("/plant{}/No Water", plant + 1).as_str(),
state.no_water.to_string().as_bytes(),
);
let _ = board.mqtt_publish(
&config,
format!("/plant{}/Out of Work Hour", plant+1).as_str(),
format!("/plant{}/Out of Work Hour", plant + 1).as_str(),
state.out_of_work_hour.to_string().as_bytes(),
);
}
@ -1069,7 +1126,18 @@ fn wait_infinity(wait_type: WaitType, reboot_now: Arc<AtomicBool>) -> ! {
fn main() {
let result = safe_main();
result.unwrap();
match result {
Ok(_) => {
println!("Main app finished, restarting");
unsafe { esp_restart() };
},
Err(err) => {
println!("Failed main {}", err);
let rollback_successful = rollback_and_reboot();
println!("Failed to rollback :(");
rollback_successful.unwrap();
},
}
}
//error codes
//error_reading_config_after_upgrade

View File

@ -159,7 +159,6 @@ pub struct PlantCtrlBoard<'a> {
PinDriver<'a, esp_idf_hal::gpio::Gpio22, InputOutput>,
PinDriver<'a, esp_idf_hal::gpio::Gpio19, InputOutput>,
>,
low_voltage_detected: Mutex<bool>,
tank_driver: AdcDriver<'a, esp_idf_hal::adc::ADC1>,
tank_channel: esp_idf_hal::adc::AdcChannelDriver<'a, { attenuation::DB_11 }, Gpio39>,
solar_is_day: PinDriver<'a, esp_idf_hal::gpio::Gpio25, esp_idf_hal::gpio::Input>,
@ -963,8 +962,6 @@ impl CreatePlantHal<'_> for PlantHal {
let nvs = EspDefaultNvsPartition::take()?;
let wifi_driver = EspWifi::new(peripherals.modem, sys_loop, Some(nvs))?;
let low_voltage_detected = Mutex::new(unsafe { LOW_VOLTAGE_DETECTED });
let adc_config = esp_idf_hal::adc::config::Config {
resolution: esp_idf_hal::adc::config::Resolution::Resolution12Bit,
calibration: true,
@ -1001,7 +998,6 @@ impl CreatePlantHal<'_> for PlantHal {
}
let rv = Mutex::new(PlantCtrlBoard {
shift_register,
low_voltage_detected,
tank_driver,
tank_channel,
solar_is_day,