diff --git a/rust/.cargo/config.toml b/rust/.cargo/config.toml index 663ef03..e5fe9ad 100644 --- a/rust/.cargo/config.toml +++ b/rust/.cargo/config.toml @@ -17,4 +17,4 @@ MCU="esp32" # Note: this variable is not used by the pio builder (`cargo build --features pio`) ESP_IDF_VERSION = "v5.1.1" CHRONO_TZ_TIMEZONE_FILTER="UTC|Europe/Berlin" -CARGO_WORKSPACE_DIR = { value = "", relative = true } \ No newline at end of file +CARGO_WORKSPACE_DIR = { value = "", relative = true } diff --git a/rust/build.rs b/rust/build.rs index af70f78..ea2d340 100644 --- a/rust/build.rs +++ b/rust/build.rs @@ -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(); } diff --git a/rust/rust-toolchain.toml b/rust/rust-toolchain.toml index a25e7e0..fd2fa13 100644 --- a/rust/rust-toolchain.toml +++ b/rust/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] channel = "nightly" - +toolchain = "esp" diff --git a/rust/sdkconfig.defaults b/rust/sdkconfig.defaults index 46ddd06..c22133f 100644 --- a/rust/sdkconfig.defaults +++ b/rust/sdkconfig.defaults @@ -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 \ No newline at end of file +CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y \ No newline at end of file diff --git a/rust/src/config.rs b/rust/src/config.rs index 7797c98..8b329f6 100644 --- a/rust/src/config.rs +++ b/rust/src/config.rs @@ -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, } } } diff --git a/rust/src/main.rs b/rust/src/main.rs index 6a71722..d9a52cb 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -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, sensor_error_p: Option, out_of_work_hour: bool, - next_pump: Option> + next_pump: Option>, } #[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) -> ! { 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 diff --git a/rust/src/plant_hal.rs b/rust/src/plant_hal.rs index 47695ea..2f34ec6 100644 --- a/rust/src/plant_hal.rs +++ b/rust/src/plant_hal.rs @@ -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, 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,