From 0a0ac6babf509b9ef520a192f06ec35b62119e6a Mon Sep 17 00:00:00 2001 From: Empire Date: Thu, 23 Nov 2023 22:50:17 +0100 Subject: [PATCH] added chrono-tz filter --- .gitignore | 4 + rust/.cargo/config.toml | 5 +- rust/.gitignore | 4 - rust/Cargo.toml | 34 ++- rust/sdkconfig.defaults | 2 +- rust/setup.txt | 6 + rust/src/main.rs | 90 ++++-- rust/src/plant_hal.rs | 404 +++++++++++++++++++++++++ rust/src/webserver/config.html | 15 + rust/src/webserver/initial_config.html | 17 ++ rust/src/webserver/ota.js | 40 +++ rust/src/webserver/webserver.rs | 88 ++++++ 12 files changed, 664 insertions(+), 45 deletions(-) delete mode 100644 rust/.gitignore create mode 100644 rust/src/plant_hal.rs create mode 100644 rust/src/webserver/config.html create mode 100644 rust/src/webserver/initial_config.html create mode 100644 rust/src/webserver/ota.js create mode 100644 rust/src/webserver/webserver.rs diff --git a/.gitignore b/.gitignore index c6a753f..edf12b3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ PlantCtrlESP32-backups/ board/production/PlantCtrlESP32_2023-11-08_00-45-35/PlantCtrlESP32.zip board/production/PlantCtrlESP32_2023-11-08_00-45-35/netlist.ipc +.vscode +.embuild/ +target +Cargo.lock diff --git a/rust/.cargo/config.toml b/rust/.cargo/config.toml index c2924b9..1070354 100644 --- a/rust/.cargo/config.toml +++ b/rust/.cargo/config.toml @@ -4,7 +4,8 @@ target = "xtensa-esp32-espidf" [target.xtensa-esp32-espidf] linker = "ldproxy" # runner = "espflash --monitor" # Select this runner for espflash v1.x.x -runner = "espflash flash --monitor" # Select this runner for espflash v2.x.x +#runner = "espflash flash --monitor" # 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] @@ -14,4 +15,4 @@ build-std = ["std", "panic_abort"] 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" diff --git a/rust/.gitignore b/rust/.gitignore deleted file mode 100644 index 73a638b..0000000 --- a/rust/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/.vscode -/.embuild -/target -/Cargo.lock diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 3d97cdd..0171913 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -7,17 +7,35 @@ resolver = "2" rust-version = "1.71" [profile.release] +# Explicitly disable LTO which the Xtensa codegen backend has issues +lto = "thin" opt-level = "s" -strip = true -lto = "fat" +strip = false codegen-units = 1 [profile.dev] +# Explicitly disable LTO which the Xtensa codegen backend has issues +lto = "thin" debug = true # Symbols are nice and they don't increase the size on Flash opt-level = "s" -lto = "fat" +strip = false codegen-units = 1 +[package.metadata.cargo_runner] +# The string `$TARGET_FILE` will be replaced with the path from cargo. +command = [ + "cargo", + "espflash", + "save-image", + "--chip", + "esp32", + "image.bin" +] + + +[package.metadata.espflash] +partition_table = "partitions2.csv" + [features] default = ["std", "embassy", "esp-idf-svc/native"] @@ -32,7 +50,6 @@ embassy = ["esp-idf-svc/embassy-sync", "esp-idf-svc/critical-section", "esp-idf- log = { version = "0.4", default-features = false } esp-idf-svc = { version = "0.47.3", default-features = false } serde = { version = "1.0.192", features = ["derive"] } -minimq = "0.8.0" average = { version = "0.14.1" , features = ["std"] } esp32 = "0.27.0" bit_field = "0.10.2" @@ -42,19 +59,14 @@ esp-idf-hal = "0.42.5" esp-idf-sys = { version = "0.33.7", features = ["binstart", "native"] } esp-ota = "0.2.0" esp_idf_build = "0.1.3" -datetime = "0.5.2" build-time = "0.1.2" chrono = { version = "0.4.23", default-features = false , features = ["iana-time-zone"] } -chrono-tz = "0.8.0" -paste = "1.0.14" +chrono-tz = {version="0.8.0", default-features = false , features = [ "filter-by-regex" ]} embedded-hal = "0.2.7" -dummy-pin = "0.1.1" shift-register-driver = "0.1.1" one-wire-bus = "0.1.1" -anyhow = "1.0.75" +anyhow = { version = "1.0.75", features = ["std", "backtrace"] } #?bq34z100 required - - [build-dependencies] embuild = "0.31.3" diff --git a/rust/sdkconfig.defaults b/rust/sdkconfig.defaults index 9ea5d73..bbd6cb1 100644 --- a/rust/sdkconfig.defaults +++ b/rust/sdkconfig.defaults @@ -3,7 +3,7 @@ CONFIG_ESP_MAIN_TASK_STACK_SIZE=8000 # Use this to set FreeRTOS kernel tick frequency to 1000 Hz (100 Hz by default). # This allows to use 1 ms granuality for thread sleeps (10 ms by default). -#CONFIG_FREERTOS_HZ=1000 +CONFIG_FREERTOS_HZ=1000 # Workaround for https://github.com/espressif/esp-idf/issues/7631 #CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=n diff --git a/rust/setup.txt b/rust/setup.txt index 3440199..ef80c80 100644 --- a/rust/setup.txt +++ b/rust/setup.txt @@ -10,3 +10,9 @@ export PATH="$PATH:$HOME/.cargo/bin" espup install rustup toolchain link esp ~/.rustup/toolchains/esp/ cargo install ldproxy + +cargo espflash save-image --chip esp32 image.bin + + +esptool.py --chip ESP32-C3 elf2image --output my-app.bin target/release/my-app +$ espflash save-image ESP32-C3 target/release/my-app my-app.bin \ No newline at end of file diff --git a/rust/src/main.rs b/rust/src/main.rs index 3c96e2a..178fe2d 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -1,11 +1,22 @@ -use chrono::{Datelike, Timelike}; - -use crate::plant_hal::{PlantCtrlBoardInteraction, PlantHal, CreatePlantHal, PLANT_COUNT}; +use chrono::{Datelike, Timelike, NaiveDateTime}; +use build_time::build_time_utc; +use chrono_tz::Europe::Berlin; +use esp_idf_hal::delay::Delay; +use plant_hal::{PlantCtrlBoardInteraction, PlantHal, CreatePlantHal, PLANT_COUNT}; +use anyhow::{Context, Result}; +use webserver::webserver::httpd; pub mod plant_hal; +mod webserver { + pub mod webserver; +} -fn main() { +fn web_initial_mode() { + //expect running wifi access point! + let _httpd = httpd(true); +} +fn main() -> Result<()>{ // It is necessary to call this function once. Otherwise some patches to the runtime // implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71 esp_idf_svc::sys::link_patches(); @@ -13,21 +24,30 @@ fn main() { // Bind the log crate to the ESP Logging facilities esp_idf_svc::log::EspLogger::initialize_default(); - log::info!("Hello, world!"); + log::info!("Startup Rust"); + let utc_build_time = build_time_utc!(); + println!("Version was build {}", utc_build_time); - let mut board = PlantHal::create(); - + + let mut board = PlantHal::create()?; - let mut cur = board.time(); + + let time = board.time(); + let mut cur = match time { + Ok(cur) => cur, + Err(err) => { + log::error!("sntp error {}", err); + NaiveDateTime::from_timestamp_millis(0).unwrap().and_utc() + } + }; //check if we know the time current > 2020 if cur.year() < 2020 { if board.is_day() { - //assume 13:00 if solar reports day - cur = *cur.with_hour(13).get_or_insert(cur); + //assume TZ safe times ;) + cur = *cur.with_hour(15).get_or_insert(cur); } else { - //assume 01:00 if solar reports night - cur = *cur.with_hour(1).get_or_insert(cur); + cur = *cur.with_hour(3).get_or_insert(cur); } } @@ -49,10 +69,26 @@ fn main() { //measure tank level (without wifi due to interference) //TODO this should be a result// detect invalid measurement value let tank_value = board.tank_sensor_mv(); - //if not possible value, blink general fault error_tank_sensor_fault + match tank_value { + Ok(_) => todo!(), + Err(_) => { + //if not possible value, blink general fault error_tank_sensor_fault board.general_fault(true); //set general fault persistent //set tank sensor state to fault + }, + } + + + //measure each plant moisture + let mut initial_measurements_a: [i32;PLANT_COUNT] = [0;PLANT_COUNT]; + let mut initial_measurements_b: [i32;PLANT_COUNT] = [0;PLANT_COUNT]; + let mut initial_measurements_p: [i32;PLANT_COUNT] = [0;PLANT_COUNT]; + for plant in 0..PLANT_COUNT { + initial_measurements_a[plant] = board.measure_moisture_hz(plant, plant_hal::Sensor::A)?; + initial_measurements_b[plant] = board.measure_moisture_hz(plant, plant_hal::Sensor::B)?; + initial_measurements_p[plant] = board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)?; + } //try connect wifi and do mqtt roundtrip @@ -62,18 +98,14 @@ fn main() { match board.sntp(1000*120) { Ok(new_time) => cur = new_time, - Err(_) => todo!(), - } - - //measure each plant moisture - let mut initial_measurements_a: [i32;PLANT_COUNT] = [0;PLANT_COUNT]; - let mut initial_measurements_b: [i32;PLANT_COUNT] = [0;PLANT_COUNT]; - let mut initial_measurements_p: [i32;PLANT_COUNT] = [0;PLANT_COUNT]; - for plant in 0..PLANT_COUNT { - initial_measurements_a[plant] = board.measure_moisture_hz(plant, plant_hal::Sensor::A); - initial_measurements_b[plant] = board.measure_moisture_hz(plant, plant_hal::Sensor::B); - initial_measurements_p[plant] = board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP); + Err(err) => { + println!("sntp error: {}", err); + } } + println!("Running logic at utc {}", cur); + let europe_time = cur.with_timezone(&Berlin); + println!("Running logic at europe/berlin {}", europe_time); + //if config battery mode @@ -116,11 +148,15 @@ fn main() { //if no preventing lightstate, enable light //lightstate = active - - - + web_initial_mode(); + let delay = Delay::new_default(); + loop { + //let freertos do shit + delay.delay_ms(1001); + } + return Ok(()) } diff --git a/rust/src/plant_hal.rs b/rust/src/plant_hal.rs new file mode 100644 index 0000000..1f9c25b --- /dev/null +++ b/rust/src/plant_hal.rs @@ -0,0 +1,404 @@ +use embedded_svc::wifi::{Configuration, ClientConfiguration, AuthMethod}; +use esp_idf_svc::eventloop::EspSystemEventLoop; +use esp_idf_svc::nvs::EspDefaultNvsPartition; +use esp_idf_svc::wifi::EspWifi; + +use std::sync::Mutex; +use anyhow::{Context, Result, bail}; +use anyhow::anyhow; + +use chrono::{Utc, NaiveDateTime, DateTime}; +use ds18b20::Ds18b20; +use embedded_hal::digital::v1_compat::OldOutputPin; +use embedded_hal::digital::v2::OutputPin; +use esp_idf_hal::adc::config::Config; +use esp_idf_hal::adc::{AdcDriver, AdcChannelDriver, attenuation}; +use esp_idf_hal::delay::Delay; +use esp_idf_hal::pcnt::{PcntDriver, PcntChannel, PinIndex, PcntChannelConfig, PcntControlMode, PcntCountMode}; +use esp_idf_hal::reset::ResetReason; +use esp_idf_svc::sntp::{self, SyncStatus}; +use esp_idf_svc::systime::EspSystemTime; +use esp_idf_sys::EspError; +use one_wire_bus::OneWire; +use shift_register_driver::sipo::ShiftRegister24; +use esp_idf_hal::gpio::{PinDriver, Gpio39, Gpio4, AnyInputPin}; +use esp_idf_hal::prelude::Peripherals; + +pub const PLANT_COUNT:usize = 8; +const PINS_PER_PLANT:usize = 5; +const PLANT_PUMP_OFFSET:usize = 0; +const PLANT_FAULT_OFFSET:usize = 1; +const PLANT_MOIST_PUMP_OFFSET:usize = 2; +const PLANT_MOIST_B_OFFSET:usize = 3; +const PLANT_MOIST_A_OFFSET:usize = 4; + + +#[link_section = ".rtc.data"] +static mut LAST_WATERING_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT]; +#[link_section = ".rtc.data"] +static mut CONSECUTIVE_WATERING_PLANT: [u32; PLANT_COUNT] = [0; PLANT_COUNT]; +#[link_section = ".rtc.data"] +static mut LOW_VOLTAGE_DETECTED:bool = false; + + +pub struct BatteryState { + state_charge_percent: u8, + max_error_percent: u8, + remaining_milli_ampere_hour: u32, + max_milli_ampere_hour: u32, + design_milli_ampere_hour:u32, + voltage_milli_volt: u16, + average_current_milli_ampere: u16, + temperature_tenth_kelvin: u32, + average_time_to_empty_minute: u16, + average_time_to_full_minute: u16, + average_discharge_power_cycle_milli_watt: u16, + cycle_count: u16, + state_health_percent: u8 +} + +pub enum Sensor{ + A, + B, + PUMP +} +pub trait PlantCtrlBoardInteraction{ + fn time(&mut self) -> Result>; + fn wifi(&mut self, ssid:&str, password:Option<&str>, max_wait:u32) -> Result<()>; + fn sntp(&mut self, max_wait:u32) -> Result>; + + fn battery_state(&mut self) -> Result; + + fn general_fault(&mut self, enable: bool); + + fn is_day(&self,) -> bool; + fn water_temperature_c(&mut self,) -> Result; + fn tank_sensor_mv(&mut self,) -> Result; + + fn set_low_voltage_in_cycle(&mut self,); + fn clear_low_voltage_in_cycle(&mut self,); + fn low_voltage_in_cycle(&mut self) -> bool; + fn any_pump(&mut self, enabled:bool) -> Result<()>; + + //keep state during deepsleep + fn light(&mut self,enable:bool) -> Result<()>; + + fn measure_moisture_hz(&self, plant:usize, sensor:Sensor) -> Result; + fn pump(&self,plant:usize, enable:bool) -> Result<()>; + fn last_pump_time(&self,plant:usize) -> Result>; + fn store_last_pump_time(&mut self,plant:usize, time: chrono::DateTime); + fn store_consecutive_pump_count(&mut self,plant:usize, count:u32); + fn consecutive_pump_count(&mut self,plant:usize) -> u32; + + //keep state during deepsleep + fn fault(&self,plant:usize, enable:bool); +} + +pub trait CreatePlantHal<'a> { + fn create()-> Result>; +} + +pub struct PlantHal { + +} + + + +impl CreatePlantHal<'_> for PlantHal{ + fn create() -> Result> { + let peripherals = Peripherals::take()?; + + let clock = OldOutputPin::from(PinDriver::output(peripherals.pins.gpio21)?); + let latch = OldOutputPin::from(PinDriver::output(peripherals.pins.gpio22)?); + let data = OldOutputPin::from(PinDriver::output(peripherals.pins.gpio19)?); + + let one_wire_pin = PinDriver::input_output_od(peripherals.pins.gpio4)?; + //TODO make to none if not possible to init + + //init,reset rtc memory depending on cause + let reasons = ResetReason::get(); + let reset_store = match reasons { + ResetReason::Software => false, + ResetReason::ExternalPin => false, + ResetReason::Watchdog => true, + ResetReason::Sdio => true, + ResetReason::Panic => true, + ResetReason::InterruptWatchdog => true, + ResetReason::PowerOn => true, + ResetReason::Unknown => true, + ResetReason::Brownout => true, + ResetReason::TaskWatchdog => true, + ResetReason::DeepSleep => false, + }; + if reset_store { + println!("Clear and reinit RTC store"); + unsafe { + LAST_WATERING_TIMESTAMP = [0; PLANT_COUNT]; + CONSECUTIVE_WATERING_PLANT = [0; PLANT_COUNT]; + LOW_VOLTAGE_DETECTED = false; + }; + } else { + println!("Keeping RTC store"); + } + + let mut counter_unit1 = PcntDriver::new( + peripherals.pcnt0, + Some(peripherals.pins.gpio18), + Option::::None, + Option::::None, + Option::::None, + )?; + + counter_unit1.channel_config( + PcntChannel::Channel0, + PinIndex::Pin0, + PinIndex::Pin1, + &PcntChannelConfig { + lctrl_mode: PcntControlMode::Reverse, + hctrl_mode: PcntControlMode::Keep, + pos_mode: PcntCountMode::Decrement, + neg_mode: PcntCountMode::Increment, + counter_h_lim: i16::MAX, + counter_l_lim: 0, + }, + )?; + + //TODO validate filter value! currently max allowed value + counter_unit1.set_filter_value(1023)?; + counter_unit1.filter_enable()?; + + + + let sys_loop = EspSystemEventLoop::take()?; + let nvs = EspDefaultNvsPartition::take()?; + let wifi_driver = EspWifi::new( + peripherals.modem, + sys_loop, + Some(nvs) + )?; + + return Ok(PlantCtrlBoard { + shift_register : ShiftRegister24::new(clock, latch, data), + last_watering_timestamp : Mutex::new(unsafe { LAST_WATERING_TIMESTAMP }), + consecutive_watering_plant : Mutex::new(unsafe { CONSECUTIVE_WATERING_PLANT }), + low_voltage_detected : Mutex::new(unsafe { LOW_VOLTAGE_DETECTED }), + tank_driver : AdcDriver::new(peripherals.adc1, &Config::new().calibration(true))?, + tank_channel: AdcChannelDriver::new(peripherals.pins.gpio39)?, + solar_is_day : PinDriver::input(peripherals.pins.gpio25)?, + light: PinDriver::output(peripherals.pins.gpio26)?, + main_pump: PinDriver::output(peripherals.pins.gpio23)?, + tank_power: PinDriver::output(peripherals.pins.gpio27)?, + general_fault: PinDriver::output(peripherals.pins.gpio13)?, + one_wire_bus: OneWire::new(one_wire_pin).map_err(|err| -> anyhow::Error {anyhow!("Missing attribute: {:?}", err)})?, + signal_counter : counter_unit1, + wifi_driver : wifi_driver + }); + } +} + + +pub struct PlantCtrlBoard<'a>{ + shift_register: ShiftRegister24>, OldOutputPin>, OldOutputPin>>, + consecutive_watering_plant: Mutex<[u32; PLANT_COUNT]>, + last_watering_timestamp: Mutex<[i64; PLANT_COUNT]>, + 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>, + signal_counter: PcntDriver<'a>, + light: PinDriver<'a, esp_idf_hal::gpio::Gpio26, esp_idf_hal::gpio::Output>, + main_pump: PinDriver<'a, esp_idf_hal::gpio::Gpio23, esp_idf_hal::gpio::Output>, + tank_power: PinDriver<'a, esp_idf_hal::gpio::Gpio27, esp_idf_hal::gpio::Output>, + general_fault: PinDriver<'a, esp_idf_hal::gpio::Gpio13, esp_idf_hal::gpio::Output>, + wifi_driver: EspWifi<'a>, + one_wire_bus: OneWire>, +} + +impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> { + fn battery_state(&mut self,) -> Result { + todo!() + } + + fn is_day(&self,) -> bool { + return self.solar_is_day.get_level().into(); + } + + fn water_temperature_c(&mut self,) -> Result { + let mut delay = Delay::new_default(); + + self.one_wire_bus.reset(&mut delay).map_err(|err| -> anyhow::Error {anyhow!("Missing attribute: {:?}", err)})?; + let first = self.one_wire_bus.devices(false, &mut delay).next(); + if first.is_none() { + bail!("Not found any one wire Ds18b20"); + } + let device_address = first.unwrap().map_err(|err| -> anyhow::Error {anyhow!("Missing attribute: {:?}", err)})?; + + let water_temp_sensor = Ds18b20::new::(device_address).map_err(|err| -> anyhow::Error {anyhow!("Missing attribute: {:?}", err)})?; + + water_temp_sensor.start_temp_measurement(&mut self.one_wire_bus, &mut delay).map_err(|err| -> anyhow::Error {anyhow!("Missing attribute: {:?}", err)})?; + ds18b20::Resolution::Bits12.delay_for_measurement_time(&mut delay); + let sensor_data = water_temp_sensor.read_data(&mut self.one_wire_bus, &mut delay).map_err(|err| -> anyhow::Error {anyhow!("Missing attribute: {:?}", err)})?; + if sensor_data.temperature == 85_f32 { + bail!("Ds18b20 dummy temperature returned"); + } + return Ok(sensor_data.temperature); + } + + fn tank_sensor_mv(&mut self,) -> Result { + let delay = Delay::new_default(); + self.tank_power.set_high()?; + //let stabilize + delay.delay_ms(100); + let value = self.tank_driver.read(&mut self.tank_channel)?; + self.tank_power.set_low()?; + return Ok(value); + } + + fn set_low_voltage_in_cycle(&mut self,) { + *self.low_voltage_detected.get_mut().unwrap() = true; + } + + fn clear_low_voltage_in_cycle(&mut self,) { + *self.low_voltage_detected.get_mut().unwrap() = false; + } + + fn light(&mut self,enable:bool) -> Result<()>{ + self.light.set_state(enable.into())?; + Ok(()) + } + + fn pump(&self,plant:usize, enable:bool) -> Result<()> { + let index = plant*PINS_PER_PLANT*PLANT_PUMP_OFFSET; + //currently infailable error, keep for future as result anyway + self.shift_register.decompose()[index].set_state(enable.into()).unwrap(); + Ok(()) + } + + fn last_pump_time(&self,plant:usize) -> Result> { + let ts = unsafe { LAST_WATERING_TIMESTAMP }[plant]; + let timestamp = NaiveDateTime::from_timestamp_millis(ts).ok_or(anyhow!("could not convert timestamp"))?; + return Ok(DateTime::::from_naive_utc_and_offset(timestamp, Utc)); + } + + fn store_last_pump_time(&mut self,plant:usize, time: chrono::DateTime) { + self.last_watering_timestamp.get_mut().unwrap()[plant] = time.timestamp_millis(); + } + + fn store_consecutive_pump_count(&mut self,plant:usize, count:u32) { + self.consecutive_watering_plant.get_mut().unwrap()[plant] = count; + } + + fn consecutive_pump_count(&mut self,plant:usize) -> u32 { + return self.consecutive_watering_plant.get_mut().unwrap()[plant] + } + + fn fault(&self,plant:usize, enable:bool) { + let index = plant*PINS_PER_PLANT*PLANT_FAULT_OFFSET; + self.shift_register.decompose()[index].set_state(enable.into()).unwrap() + } + + fn low_voltage_in_cycle(&mut self) -> bool { + return *self.low_voltage_detected.get_mut().unwrap() + } + + fn any_pump(&mut self, enable:bool) -> Result<()> { + return Ok(self.main_pump.set_state(enable.into()).unwrap()); + } + + fn time(&mut self) -> Result> { + let time = EspSystemTime{}.now().as_millis(); + let smaller_time = time as i64; + let local_time = NaiveDateTime::from_timestamp_millis(smaller_time).ok_or(anyhow!("could not convert timestamp"))?; + return Ok(local_time.and_utc()); + } + + fn sntp(&mut self, max_wait_ms:u32) -> Result> { + let sntp = sntp::EspSntp::new_default()?; + let mut counter = 0; + while sntp.get_sync_status() != SyncStatus::Completed{ + let delay = Delay::new_default(); + delay.delay_ms(100); + counter += 100; + if counter > max_wait_ms { + bail!("Reached sntp timeout, aborting") + } + } + + return self.time(); + } + + fn measure_moisture_hz(&self, plant:usize, sensor:Sensor) -> Result { + self.signal_counter.counter_pause()?; + self.signal_counter.counter_clear()?; + // + let offset = match sensor { + Sensor::A => PLANT_MOIST_A_OFFSET, + Sensor::B => PLANT_MOIST_B_OFFSET, + Sensor::PUMP => PLANT_MOIST_PUMP_OFFSET, + }; + let index = plant*PINS_PER_PLANT*offset; + + let delay = Delay::new_default(); + let measurement = 100; + let factor = 1000/100; + + self.shift_register.decompose()[index].set_high().unwrap(); + //give some time to stabilize + delay.delay_ms(10); + self.signal_counter.counter_resume()?; + delay.delay_ms(measurement); + self.signal_counter.counter_pause()?; + self.shift_register.decompose()[index].set_low().unwrap(); + let unscaled = self.signal_counter.get_counter_value()? as i32; + return Ok(unscaled*factor); + } + + fn general_fault(&mut self, enable:bool) { + self.general_fault.set_state(enable.into()).unwrap(); + } + + fn wifi(&mut self, ssid:&str, password:Option<&str>,max_wait:u32) -> Result<()> { + 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: ssid.into(), + password: pw.into(), + ..Default::default() + }))?; + }, + None => { + self.wifi_driver.set_configuration(&Configuration::Client(ClientConfiguration { + ssid: ssid.into(), + auth_method: AuthMethod::None, + ..Default::default() + })).unwrap(); + }, + } + + self.wifi_driver.start().unwrap(); + self.wifi_driver.connect().unwrap(); + + let delay = Delay::new_default(); + let mut counter = 0_u32; + while !self.wifi_driver.is_connected().unwrap(){ + let config = self.wifi_driver.get_configuration().unwrap(); + println!("Waiting for station {:?}", config); + //TODO blink status? + delay.delay_ms(250); + counter += 250; + if counter > max_wait { + //ignore these errors, wifi 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"); + let address = self.wifi_driver.sta_netif().get_ip_info().unwrap(); + println!("IP info: {:?}", address); + return Ok(()); + } + + +} \ No newline at end of file diff --git a/rust/src/webserver/config.html b/rust/src/webserver/config.html new file mode 100644 index 0000000..6680e7c --- /dev/null +++ b/rust/src/webserver/config.html @@ -0,0 +1,15 @@ + + + + + +

firmeware OTA v3

+
+
+ +

+

+

+
+ + \ No newline at end of file diff --git a/rust/src/webserver/initial_config.html b/rust/src/webserver/initial_config.html new file mode 100644 index 0000000..83976d4 --- /dev/null +++ b/rust/src/webserver/initial_config.html @@ -0,0 +1,17 @@ + + + + + +
+

firmeware OTA v3

+
+
+ +

+

+

+
+
+ + \ No newline at end of file diff --git a/rust/src/webserver/ota.js b/rust/src/webserver/ota.js new file mode 100644 index 0000000..b2d007c --- /dev/null +++ b/rust/src/webserver/ota.js @@ -0,0 +1,40 @@ +function _(el) { + return document.getElementById(el); + } + + function uploadFile() { + var file = _("file1").files[0]; + // alert(file.name+" | "+file.size+" | "+file.type); + var ajax = new XMLHttpRequest(); + ajax.upload.addEventListener("progress", progressHandler, false); + ajax.addEventListener("load", completeHandler, false); + ajax.addEventListener("error", errorHandler, false); + ajax.addEventListener("abort", abortHandler, false); + ajax.open("POST", "/ota"); // http://www.developphp.com/video/JavaScript/File-Upload-Progress-Bar-Meter-Tutorial-Ajax-PHP + //use file_upload_parser.php from above url + ajax.send(file); + } + + function progressHandler(event) { + _("loaded_n_total").innerHTML = "Uploaded " + event.loaded + " bytes of " + event.total; + var percent = (event.loaded / event.total) * 100; + _("progressBar").value = Math.round(percent); + _("status").innerHTML = Math.round(percent) + "%"; + _("answer").innerHTML = "in progress"; + } + + function completeHandler(event) { + _("status").innerHTML = event.target.responseText; + _("answer").innerHTML = "finished"; + _("progressBar").value = 0; //wil clear progress bar after successful upload + } + + function errorHandler(event) { + _("status").innerHTML = event.target.responseText; + _("answer").innerHTML = "failed"; + } + + function abortHandler(event) { + _("status").innerHTML = event.target.responseText; + _("answer").innerHTML = "aborted"; + } \ No newline at end of file diff --git a/rust/src/webserver/webserver.rs b/rust/src/webserver/webserver.rs new file mode 100644 index 0000000..bf150b1 --- /dev/null +++ b/rust/src/webserver/webserver.rs @@ -0,0 +1,88 @@ +//offer ota and config mode + +use build_time::build_time_utc; +use embedded_svc::http::Method; +use esp_idf_svc::http::server::EspHttpServer; +use esp_ota::OtaUpdate; + +#[allow(unused_variables)] +pub fn httpd(initial_config:bool) -> EspHttpServer<'static> { + + let mut server = EspHttpServer::new(&Default::default()).unwrap(); + + server + .fn_handler("/",Method::Get, move |request| { + let mut response = request.into_ok_response()?; + match initial_config { + true => response.write(include_bytes!("initial_config.html"))?, + false => response.write(include_bytes!("config.html"))? + }; + return Ok(()) + }).unwrap(); + server + .fn_handler("/buildtime",Method::Get, |request| { + let mut response = request.into_ok_response()?; + response.write(build_time_utc!().as_bytes())?; + return Ok(()) + }).unwrap(); + server + .fn_handler("/ota.js",Method::Get, |request| { + let mut response = request.into_ok_response()?; + response.write(include_bytes!("ota.js"))?; + return Ok(()) + }).unwrap(); + + server + .fn_handler("/ota", Method::Post, |mut request| { + let ota = OtaUpdate::begin(); + if ota.is_err(){ + let error_text = ota.unwrap_err().to_string(); + request.into_status_response(500)?.write(error_text.as_bytes())?; + return Ok(()); + } + let mut ota = ota.unwrap(); + println!("start ota"); + + //having a larger buffer is not really faster, requires more stack and prevents the progress bar from working ;) + const BUFFER_SIZE:usize = 512; + let mut buffer :[u8;BUFFER_SIZE] = [0;BUFFER_SIZE]; + let mut total_read: usize = 0; + loop { + let read = request.read(&mut buffer).unwrap(); + total_read += read; + println!("received {read} bytes ota {total_read}"); + let to_write = & buffer[0 .. read]; + + + let write_result = ota.write(to_write); + if write_result.is_err(){ + let error_text = write_result.unwrap_err().to_string(); + request.into_status_response(500)?.write(error_text.as_bytes())?; + return Ok(()); + } + println!("wrote {read} bytes ota {total_read}"); + if read == 0 { + break; + } + } + println!("finish ota"); + let partition = ota.raw_partition(); + println!("finalizing and changing boot partition to {partition:?}"); + + let finalizer = ota.finalize(); + if finalizer.is_err(){ + let error_text = finalizer.err().unwrap().to_string(); + request.into_status_response(500)?.write(error_text.as_bytes())?; + return Ok(()); + } + let mut finalizer = finalizer.unwrap(); + + println!("changing boot partition"); + finalizer.set_as_boot_partition().unwrap(); + finalizer.restart(); + + + //return Ok(()) + }).unwrap(); + return server; +}