diff --git a/rust/.cargo/config.toml b/rust/.cargo/config.toml index 400d3f0..6effd4d 100644 --- a/rust/.cargo/config.toml +++ b/rust/.cargo/config.toml @@ -3,8 +3,7 @@ target = "xtensa-esp32-espidf" [target.xtensa-esp32-espidf] linker = "ldproxy" -# runner = "espflash --monitor --baud 921600" # Select this runner for espflash v1.x.x - runner = "espflash flash --monitor --baud 921600 --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" rustflags = [ "--cfg", "espidf_time64"] # Extending time_t for ESP IDF 5: https://github.com/esp-rs/rust/issues/110 diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 17aae5d..7c679f4 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -58,7 +58,7 @@ log = { version = "0.4", default-features = false } esp-idf-svc = { version = "0.47.3", default-features = false } serde = { version = "1.0.192", features = ["derive"] } average = { version = "0.14.1" , features = ["std"] } -esp32 = "0.27.0" +#esp32 = "0.28.0" bit_field = "0.10.2" ds18b20 = "0.1.1" embedded-svc = { version = "0.26.4", features = ["experimental"] } @@ -69,7 +69,7 @@ esp_idf_build = "0.1.3" chrono = { version = "0.4.23", default-features = false , features = ["iana-time-zone"] } chrono-tz = {version="0.8.0", default-features = false , features = [ "filter-by-regex" ]} embedded-hal = "0.2.7" -shift-register-driver = "0.1.1" +#shift-register-driver = "0.1.1" one-wire-bus = "0.1.1" anyhow = { version = "1.0.75", features = ["std", "backtrace"] } schemars = "0.8.16" diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000..6c53449 --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,4 @@ +#![allow(dead_code)] +extern crate embedded_hal as hal; + +pub mod sipo; \ No newline at end of file diff --git a/rust/src/main.rs b/rust/src/main.rs index d0771cf..bd3919d 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -1,23 +1,15 @@ -use std::{ - ffi::CString, - fs::File, - io::{Read, Write}, - mem, - str::from_utf8, -}; +use std::{sync::{Arc, Mutex}, env}; use chrono::{Datelike, NaiveDateTime, Timelike}; -use anyhow::{Context, Result}; +use anyhow::Result; use chrono_tz::Europe::Berlin; use esp_idf_hal::delay::Delay; -use esp_idf_svc::http::server::EspHttpServer; -use esp_idf_sys::{esp_restart, vTaskDelay, TickType_t}; -use heapless::String; +use esp_idf_sys::{esp_restart, vTaskDelay}; use plant_hal::{CreatePlantHal, PlantCtrlBoard, PlantCtrlBoardInteraction, PlantHal, PLANT_COUNT}; -use webserver::webserver::httpd; -use crate::config::{Config, WifiConfig}; + +use crate::{config::{Config, WifiConfig}, webserver::webserver::httpd_initial}; mod config; pub mod plant_hal; mod webserver { @@ -33,14 +25,30 @@ enum OnlineMode { MqttRoundtrip } -fn wait_infinity(board: &mut PlantCtrlBoard<'_>) { +enum WaitType{ + InitialConfig, + FlashError +} + +fn wait_infinity(board_access: Arc>>, wait_type:WaitType) -> !{ + let delay = match wait_type { + WaitType::InitialConfig => 250_u32, + WaitType::FlashError => 100_u32, + }; + board_access.lock().unwrap().light(true); loop { unsafe { //do not trigger watchdog - board.general_fault(true); - vTaskDelay(500_u32); - board.general_fault(false); - vTaskDelay(500_u32); + for i in 0..7 { + board_access.lock().unwrap().fault(i, true); + } + board_access.lock().unwrap().general_fault(true); + vTaskDelay(delay); + board_access.lock().unwrap().general_fault(false); + for i in 0..7 { + board_access.lock().unwrap().fault(i, false); + } + vTaskDelay(delay); } } } @@ -59,7 +67,8 @@ fn main() -> Result<()> { println!("Version useing git has {}", git_hash); println!("Board hal init"); - let mut board = PlantHal::create()?; + let board_access = PlantHal::create()?; + let mut board = board_access.lock().unwrap(); println!("Mounting filesystem"); board.mountFileSystem()?; let free_space = board.fileSystemSize()?; @@ -103,7 +112,7 @@ fn main() -> Result<()> { Err(err) => { println!("Could not remove config files, system borked {}", err); //terminate main app and freeze - wait_infinity(&mut board); + wait_infinity( board_access.clone(), WaitType::FlashError); } } } @@ -118,12 +127,11 @@ fn main() -> Result<()> { }, Err(err) => { println!("Missing wifi config, entering initial config mode {}", err); + board.wifi_ap().unwrap(); //config upload will trigger reboot! - let _webserver = httpd(true); - wait_infinity(&mut board); - //how to do this better? - let ssid: String<32> = String::try_from("").unwrap(); - wifi = WifiConfig { ssid : ssid, password : None}; + drop(board); + let _webserver = httpd_initial(board_access.clone()); + wait_infinity(board_access.clone(), WaitType::InitialConfig); }, }; @@ -195,10 +203,10 @@ fn main() -> Result<()> { println!("Running logic at europe/berlin {}", europe_time); } - if(online_mode == OnlineMode::SnTp){ + if online_mode == OnlineMode::SnTp { //mqtt here } - if(online_mode == OnlineMode::Mqtt){ + if online_mode == OnlineMode::Mqtt { //mqtt roundtrip here } //TODO configmode webserver logic here diff --git a/rust/src/plant_hal.rs b/rust/src/plant_hal.rs index 1ff3d38..b209394 100644 --- a/rust/src/plant_hal.rs +++ b/rust/src/plant_hal.rs @@ -1,66 +1,71 @@ //mod config; -use embedded_svc::wifi::{Configuration, ClientConfiguration, AuthMethod, Wifi}; +use embedded_hal::blocking::delay::DelayMs; +use embedded_svc::wifi::{ + AccessPointConfiguration, AuthMethod, ClientConfiguration, Configuration, Wifi, AccessPointInfo, +}; use esp_idf_svc::eventloop::EspSystemEventLoop; use esp_idf_svc::nvs::EspDefaultNvsPartition; -use esp_idf_svc::wifi::EspWifi; +use esp_idf_svc::wifi::config::{ScanConfig, ScanType}; +use esp_idf_svc::wifi::{EspWifi, NonBlocking}; +use plant_ctrl2::sipo::ShiftRegister40; +use anyhow::anyhow; +use anyhow::{bail, Context, Ok, Result}; use std::ffi::CString; use std::fs::File; -use std::io::{Read, BufReader}; +use std::io::{BufReader, Read}; use std::path::Path; use std::str::from_utf8; -use std::sync::Mutex; -use anyhow::{Context, Result, bail, Ok}; -use anyhow::anyhow; +use std::sync::{Mutex, Arc}; +use std::time::Duration; -use chrono::{Utc, NaiveDateTime, DateTime}; +use chrono::{DateTime, NaiveDateTime, Utc}; 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::adc::{attenuation, AdcChannelDriver, AdcDriver}; use esp_idf_hal::delay::{Delay, FreeRtos}; -use esp_idf_hal::pcnt::{PcntDriver, PcntChannel, PinIndex, PcntChannelConfig, PcntControlMode, PcntCountMode}; +use esp_idf_hal::gpio::{AnyInputPin, Gpio39, Gpio4, Level, PinDriver}; +use esp_idf_hal::pcnt::{ + PcntChannel, PcntChannelConfig, PcntControlMode, PcntCountMode, PcntDriver, PinIndex, +}; +use esp_idf_hal::prelude::Peripherals; 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 esp_idf_sys::{vTaskDelay, EspError}; use one_wire_bus::OneWire; -use shift_register_driver::sipo::ShiftRegister40; -use esp_idf_hal::gpio::{PinDriver, Gpio39, Gpio4, AnyInputPin, Level}; -use esp_idf_hal::prelude::Peripherals; use serde::{Deserialize, Serialize}; use crate::config::{self, WifiConfig}; -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; +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; const SPIFFS_PARTITION_NAME: &str = "storage"; const WIFI_CONFIG_FILE: &str = "/spiffs/wifi.cfg"; const CONFIG_FILE: &str = "/spiffs/config.cfg"; - #[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; - +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, + design_milli_ampere_hour: u32, voltage_milli_volt: u16, average_current_milli_ampere: u16, temperature_tenth_kelvin: u32, @@ -68,53 +73,53 @@ pub struct BatteryState { average_time_to_full_minute: u16, average_discharge_power_cycle_milli_watt: u16, cycle_count: u16, - state_health_percent: u8 + state_health_percent: u8, } -pub struct FileSystemSizeInfo{ +pub struct FileSystemSizeInfo { pub total_size: usize, pub used_size: usize, - pub free_size: usize + pub free_size: usize, } #[derive(Debug)] -pub enum Sensor{ +pub enum Sensor { A, B, - PUMP + PUMP, } -pub trait PlantCtrlBoardInteraction{ +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 wifi(&mut self, ssid: &str, password: Option<&str>, max_wait: u32) -> Result<()>; + fn sntp(&mut self, max_wait: u32) -> Result>; fn mountFileSystem(&mut self) -> Result<()>; fn fileSystemSize(&mut self) -> 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 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<()>; + fn any_pump(&mut self, enabled: bool) -> Result<()>; //keep state during deepsleep - fn light(&mut self,enable:bool) -> Result<()>; + 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; + 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); + fn fault(&self, plant: usize, enable: bool); //config fn is_config_reset(&mut self) -> bool; @@ -122,25 +127,54 @@ pub trait PlantCtrlBoardInteraction{ fn get_config(&mut self) -> Result; fn get_wifi(&mut self) -> Result; fn set_wifi(&mut self, wifi: &WifiConfig) -> Result<()>; + fn wifi_ap(&mut self) -> Result<()>; + fn wifi_scan(&mut self) -> Result>; } pub trait CreatePlantHal<'a> { - fn create()-> Result>; + fn create() -> Result>>>; } -pub struct PlantHal { +pub struct PlantHal {} -} +impl CreatePlantHal<'_> for PlantHal { + fn create() -> Result>>> { + let peripherals = Peripherals::take()?; + let mut clock = PinDriver::output(peripherals.pins.gpio21)?; + let mut latch = PinDriver::output(peripherals.pins.gpio22)?; + let mut data = PinDriver::output(peripherals.pins.gpio19)?; + // loop { + // unsafe { + // let delay = Delay::new_default(); -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)?); + // latch.set_low().unwrap(); + // delay.delay_ms(1); + // for i in 1..2 { + // data.set_high().unwrap(); + // delay.delay_ms(1); + // clock.set_high().unwrap(); + // delay.delay_ms(1); + // clock.set_low().unwrap(); + // delay.delay_ms(1); + // } + // latch.set_high().unwrap(); + + // latch.set_low().unwrap(); + // delay.delay_ms(1); + // for i in 1..2 { + // data.set_low().unwrap(); + // delay.delay_ms(1); + // clock.set_high().unwrap(); + // delay.delay_ms(1); + // clock.set_low().unwrap(); + // delay.delay_ms(1); + // } + // latch.set_high().unwrap(); + // vTaskDelay(5); + // } + // } let one_wire_pin = PinDriver::input_output_od(peripherals.pins.gpio4)?; //TODO make to none if not possible to init @@ -201,61 +235,59 @@ impl CreatePlantHal<'_> for PlantHal{ counter_unit1.set_filter_value(1023)?; counter_unit1.filter_enable()?; - println!("Wifi start"); - + let sys_loop = EspSystemEventLoop::take()?; let nvs = EspDefaultNvsPartition::take()?; - let wifi_driver = EspWifi::new( - peripherals.modem, - sys_loop, - Some(nvs) - )?; + let wifi_driver = EspWifi::new(peripherals.modem, sys_loop, Some(nvs))?; let shift_register = ShiftRegister40::new(clock, latch, data); let last_watering_timestamp = Mutex::new(unsafe { LAST_WATERING_TIMESTAMP }); let consecutive_watering_plant = Mutex::new(unsafe { CONSECUTIVE_WATERING_PLANT }); let low_voltage_detected = Mutex::new(unsafe { LOW_VOLTAGE_DETECTED }); let tank_driver = AdcDriver::new(peripherals.adc1, &Config::new())?; - let tank_channel: AdcChannelDriver<'_, {attenuation::DB_11}, Gpio39> = AdcChannelDriver::new(peripherals.pins.gpio39)?; + let tank_channel: AdcChannelDriver<'_, { attenuation::DB_11 }, Gpio39> = + AdcChannelDriver::new(peripherals.pins.gpio39)?; let solar_is_day = PinDriver::input(peripherals.pins.gpio25)?; let boot_button = PinDriver::input(peripherals.pins.gpio0)?; let light = PinDriver::output(peripherals.pins.gpio26)?; let main_pump = PinDriver::output(peripherals.pins.gpio23)?; let tank_power = PinDriver::output(peripherals.pins.gpio27)?; let general_fault = PinDriver::output(peripherals.pins.gpio13)?; - let one_wire_bus = OneWire::new(one_wire_pin).map_err(|err| -> anyhow::Error {anyhow!("Missing attribute: {:?}", err)})?; + let one_wire_bus = OneWire::new(one_wire_pin) + .map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?; println!("After stuff"); - return Ok(PlantCtrlBoard { - shift_register : shift_register, - last_watering_timestamp : last_watering_timestamp, - consecutive_watering_plant : consecutive_watering_plant, - low_voltage_detected : low_voltage_detected, - tank_driver : tank_driver, + + let rv = Arc::new(Mutex::new(PlantCtrlBoard { + //shift_register : shift_register, + last_watering_timestamp: last_watering_timestamp, + consecutive_watering_plant: consecutive_watering_plant, + low_voltage_detected: low_voltage_detected, + tank_driver: tank_driver, tank_channel: tank_channel, - solar_is_day : solar_is_day, - boot_button : boot_button, + solar_is_day: solar_is_day, + boot_button: boot_button, light: light, main_pump: main_pump, tank_power: tank_power, general_fault: general_fault, one_wire_bus: one_wire_bus, - signal_counter : counter_unit1, - wifi_driver : wifi_driver - }); + signal_counter: counter_unit1, + wifi_driver: wifi_driver, + })); + return Ok(rv); } } - -pub struct PlantCtrlBoard<'a>{ - shift_register: ShiftRegister40>, OldOutputPin>, OldOutputPin>>, +pub struct PlantCtrlBoard<'a> { + //shift_register: ShiftRegister40>, 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 >, + 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>, boot_button: PinDriver<'a, esp_idf_hal::gpio::Gpio0, esp_idf_hal::gpio::Input>, signal_counter: PcntDriver<'a>, @@ -268,36 +300,45 @@ pub struct PlantCtrlBoard<'a>{ } impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> { - fn battery_state(&mut self,) -> Result { + fn battery_state(&mut self) -> Result { todo!() } - fn is_day(&self,) -> bool { + fn is_day(&self) -> bool { return self.solar_is_day.get_level().into(); } - fn water_temperature_c(&mut self,) -> Result { + 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)})?; + 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 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)})?; + 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)})?; + 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 { + fn tank_sensor_mv(&mut self) -> Result { let delay = Delay::new_default(); self.tank_power.set_high()?; //let stabilize @@ -307,68 +348,70 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> { return Ok(value); } - fn set_low_voltage_in_cycle(&mut self,) { + fn set_low_voltage_in_cycle(&mut self) { *self.low_voltage_detected.get_mut().unwrap() = true; } - fn clear_low_voltage_in_cycle(&mut self,) { + fn clear_low_voltage_in_cycle(&mut self) { *self.low_voltage_detected.get_mut().unwrap() = false; } - fn light(&mut self,enable:bool) -> Result<()>{ + 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; + 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(); + //self.shift_register.decompose()[index].set_state(enable.into()).unwrap(); Ok(()) } - fn last_pump_time(&self,plant:usize) -> Result> { + 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"))?; + 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) { + 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) { + 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 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 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() + return *self.low_voltage_detected.get_mut().unwrap(); } - fn any_pump(&mut self, enable:bool) -> Result<()> { + 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 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"))?; + 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> { + 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{ + while sntp.get_sync_status() != SyncStatus::Completed { let delay = Delay::new_default(); delay.delay_ms(100); counter += 100; @@ -380,7 +423,7 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> { return self.time(); } - fn measure_moisture_hz(&self, plant:usize, sensor:Sensor) -> Result { + fn measure_moisture_hz(&self, plant: usize, sensor: Sensor) -> Result { self.signal_counter.counter_pause()?; self.signal_counter.counter_clear()?; // @@ -389,55 +432,72 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> { Sensor::B => PLANT_MOIST_B_OFFSET, Sensor::PUMP => PLANT_MOIST_PUMP_OFFSET, }; - let index = plant*PINS_PER_PLANT+offset; - + let index = plant * PINS_PER_PLANT + offset; + let delay = Delay::new_default(); let measurement = 100; - let factor = 1000/100; + let factor = 1000 / 100; - self.shift_register.decompose()[index].set_high().unwrap(); + //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(); + //self.shift_register.decompose()[index].set_low().unwrap(); let unscaled = self.signal_counter.get_counter_value()? as i32; - let hz = unscaled*factor; + let hz = unscaled * factor; println!("Measuring {:?} @ {} with {}", sensor, plant, hz); return Ok(hz); } - fn general_fault(&mut self, enable:bool) { + 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{ + fn wifi_ap(&mut self) -> Result<()> { + + let apconfig = AccessPointConfiguration { + ssid: "PlantCtrl".into(), + auth_method: AuthMethod::None, + ssid_hidden: false, + ..Default::default() + }; + let clientconfig = ClientConfiguration::default(); + self.wifi_driver.set_configuration(&Configuration::Mixed(clientconfig, apconfig))?; + self.wifi_driver.start()?; + Ok(()) + } + + 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() - }))?; - }, + 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 + .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(); - + self.wifi_driver.start()?; + self.wifi_driver.connect()?; + 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(); + while !self.wifi_driver.is_connected()? { println!("Waiting for station connection"); //TODO blink status? delay.delay_ms(250); @@ -468,7 +528,7 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> { return Ok(()); } - fn mountFileSystem(&mut self) -> Result<()> { + fn mountFileSystem(&mut self) -> Result<()> { let base_path = CString::new("/spiffs")?; let storage = CString::new(SPIFFS_PARTITION_NAME)?; let conf = esp_idf_sys::esp_vfs_spiffs_conf_t { @@ -489,9 +549,17 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> { let mut total_size = 0; let mut used_size = 0; unsafe { - esp_idf_sys::esp!(esp_idf_sys::esp_spiffs_info(storage.as_ptr(),&mut total_size,&mut used_size))?; + esp_idf_sys::esp!(esp_idf_sys::esp_spiffs_info( + storage.as_ptr(), + &mut total_size, + &mut used_size + ))?; } - return Ok(FileSystemSizeInfo{total_size, used_size, free_size : total_size - used_size}); + return Ok(FileSystemSizeInfo { + total_size, + used_size, + free_size: total_size - used_size, + }); } fn is_config_reset(&mut self) -> bool { @@ -502,13 +570,13 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> { let wifi_config = Path::new(WIFI_CONFIG_FILE); if wifi_config.exists() { println!("Removing wifi config"); - std::fs::remove_file(wifi_config)?; + std::fs::remove_file(wifi_config)?; } - + let config = Path::new(CONFIG_FILE); if config.exists() { println!("Removing config"); - std::fs::remove_file(config)?; + std::fs::remove_file(config)?; } Ok(()) } @@ -519,7 +587,7 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> { return Ok(config); } - fn set_wifi(&mut self, wifi: &WifiConfig ) -> Result<()> { + fn set_wifi(&mut self, wifi: &WifiConfig) -> Result<()> { let mut cfg = File::create(WIFI_CONFIG_FILE)?; serde_json::to_writer(&mut cfg, &wifi)?; println!("Wrote wifi config {}", wifi); @@ -532,8 +600,24 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> { let read = cfg.read(&mut data)?; println!("Read file {}", from_utf8(&data[0..read])?); - bail!("todo") } -} \ No newline at end of file + fn wifi_scan(&mut self) -> Result> { + for i in 1..11 { + println!("Scanning channel {}", i); + self.wifi_driver.start_scan(&ScanConfig{ + scan_type : ScanType::Passive(Duration::from_secs(1)), + show_hidden: false, + channel: Some(i), + ..Default::default() + }, true)?; + let sr = self.wifi_driver.get_scan_result()?; + for r in sr.iter(){ + println!("Found wifi {}", r.ssid); + } + } + + return bail!("dummy"); + } +} diff --git a/rust/src/sipo.rs b/rust/src/sipo.rs new file mode 100644 index 0000000..b5fb849 --- /dev/null +++ b/rust/src/sipo.rs @@ -0,0 +1,146 @@ +//! Serial-in parallel-out shift register + +use core::cell::RefCell; +use core::mem::{self, MaybeUninit}; + +use crate::hal::digital::v2::OutputPin; + +trait ShiftRegisterInternal { + fn update(&self, index: usize, command: bool) -> Result<(), ()>; +} + +/// Output pin of the shift register +pub struct ShiftRegisterPin<'a> +{ + shift_register: &'a dyn ShiftRegisterInternal, + index: usize, +} + +impl<'a> ShiftRegisterPin<'a> +{ + fn new(shift_register: &'a dyn ShiftRegisterInternal, index: usize) -> Self { + ShiftRegisterPin { shift_register, index } + } +} + +impl OutputPin for ShiftRegisterPin<'_> +{ + type Error = (); + + fn set_low(&mut self) -> Result<(), Self::Error> { + self.shift_register.update(self.index, false)?; + Ok(()) + } + + fn set_high(&mut self) -> Result<(), Self::Error> { + self.shift_register.update(self.index, true)?; + Ok(()) + } +} + +macro_rules! ShiftRegisterBuilder { + ($name: ident, $size: expr) => { + /// Serial-in parallel-out shift register + pub struct $name + where Pin1: OutputPin, + Pin2: OutputPin, + Pin3: OutputPin + { + clock: RefCell, + latch: RefCell, + data: RefCell, + output_state: RefCell<[bool; $size]>, + } + + impl ShiftRegisterInternal for $name + where Pin1: OutputPin, + Pin2: OutputPin, + Pin3: OutputPin + { + /// Sets the value of the shift register output at `index` to value `command` + fn update(&self, index: usize, command: bool) -> Result<(), ()>{ + self.output_state.borrow_mut()[index] = command; + let output_state = self.output_state.borrow(); + self.latch.borrow_mut().set_low().map_err(|_e| ())?; + + for i in 1..=output_state.len() { + if output_state[output_state.len() - i] { + self.data.borrow_mut().set_high().map_err(|_e| ())?; + } else { + self.data.borrow_mut().set_low().map_err(|_e| ())?; + } + self.clock.borrow_mut().set_high().map_err(|_e| ())?; + self.clock.borrow_mut().set_low().map_err(|_e| ())?; + } + + self.latch.borrow_mut().set_high().map_err(|_e| ())?; + Ok(()) + } + } + + + impl $name + where Pin1: OutputPin, + Pin2: OutputPin, + Pin3: OutputPin + { + /// Creates a new SIPO shift register from clock, latch, and data output pins + pub fn new(clock: Pin1, latch: Pin2, data: Pin3) -> Self { + $name { + clock: RefCell::new(clock), + latch: RefCell::new(latch), + data: RefCell::new(data), + output_state: RefCell::new([false; $size]), + } + } + + /// Get embedded-hal output pins to control the shift register outputs + pub fn decompose(&self) -> [ShiftRegisterPin; $size] { + + // Create an uninitialized array of `MaybeUninit`. The `assume_init` is + // safe because the type we are claiming to have initialized here is a + // bunch of `MaybeUninit`s, which do not require initialization. + let mut pins: [MaybeUninit; $size] = unsafe { + MaybeUninit::uninit().assume_init() + }; + + // Dropping a `MaybeUninit` does nothing, so if there is a panic during this loop, + // we have a memory leak, but there is no memory safety issue. + for (index, elem) in pins.iter_mut().enumerate() { + elem.write(ShiftRegisterPin::new(self, index)); + } + + // Everything is initialized. Transmute the array to the + // initialized type. + unsafe { mem::transmute::<_, [ShiftRegisterPin; $size]>(pins) } + } + + /// Consume the shift register and return the original clock, latch, and data output pins + pub fn release(self) -> (Pin1, Pin2, Pin3) { + let Self{clock, latch, data, output_state: _} = self; + (clock.into_inner(), latch.into_inner(), data.into_inner()) + } + } + + } +} + +ShiftRegisterBuilder!(ShiftRegister8, 8); +ShiftRegisterBuilder!(ShiftRegister16, 16); +ShiftRegisterBuilder!(ShiftRegister24, 24); +ShiftRegisterBuilder!(ShiftRegister32, 32); +ShiftRegisterBuilder!(ShiftRegister40, 40); +ShiftRegisterBuilder!(ShiftRegister48, 48); +ShiftRegisterBuilder!(ShiftRegister56, 56); +ShiftRegisterBuilder!(ShiftRegister64, 64); +ShiftRegisterBuilder!(ShiftRegister72, 72); +ShiftRegisterBuilder!(ShiftRegister80, 80); +ShiftRegisterBuilder!(ShiftRegister88, 88); +ShiftRegisterBuilder!(ShiftRegister96, 96); +ShiftRegisterBuilder!(ShiftRegister104, 104); +ShiftRegisterBuilder!(ShiftRegister112, 112); +ShiftRegisterBuilder!(ShiftRegister120, 120); +ShiftRegisterBuilder!(ShiftRegister128, 128); + +/// 8 output serial-in parallel-out shift register +pub type ShiftRegister = ShiftRegister8; \ No newline at end of file diff --git a/rust/src/webserver/config.html b/rust/src/webserver/config.html index 60bf8f4..13f5a8f 100644 --- a/rust/src/webserver/config.html +++ b/rust/src/webserver/config.html @@ -1,72 +1,57 @@ - + -

firmeware OTA v3

-
-
- -

-

-

-
+

firmeware OTA v3

+
+
+ +

+

+

+
-

config

-
+

config

+ + +
+

Tank:

+
+ + Enable Tank Sensor
- - + +
+ + Tank Size mL +
+
+ + Tank Warn below mL +
+ +

Light:

+
+ Start + + Stop + +
+
+ + Light only when dark +
+ +

Plants:

+
+
+ +
+ \ No newline at end of file diff --git a/rust/src/webserver/form.js b/rust/src/webserver/form.js new file mode 100644 index 0000000..d841021 --- /dev/null +++ b/rust/src/webserver/form.js @@ -0,0 +1,109 @@ +var plantcount = 1; + +function createForm(){ + var current = {} + current.tank_sensor_enabled = true; + current.tank_full_ml = 400; + current.tank_warn_percent = 200; + current.night_lamp_time_start = "18:00"; + current.night_lamp_time_end = "02:00"; + current.night_lamp_only_when_dark = true; + current.plants = [ + { + target_moisture: 40, + pump_time_s:60 + } + + ] + current.plantcount = 1; + + plantcount = current.plantcount; + + + for(i=0;i +
@@ -13,5 +14,17 @@

+ +
+

WIFI

+ +
+ + + + +
\ No newline at end of file diff --git a/rust/src/webserver/webserver.rs b/rust/src/webserver/webserver.rs index f500c34..c42208e 100644 --- a/rust/src/webserver/webserver.rs +++ b/rust/src/webserver/webserver.rs @@ -1,23 +1,64 @@ //offer ota and config mode +use std::{vec, sync::{Mutex, Arc}}; + 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> { +use crate::plant_hal::{PlantCtrlBoard, PlantCtrlBoardInteraction}; - let mut server = EspHttpServer::new(&Default::default()).unwrap(); +pub fn httpd_initial(board_access:Arc>>) -> Box> { + let mut server = shared(); + server.fn_handler("/",Method::Get, move |request| { + let mut response = request.into_ok_response()?; + response.write(include_bytes!("initial_config.html"))?; + return Ok(()) + }).unwrap(); + + server.fn_handler("/wifiscan",Method::Get, move |request| { + let mut response = request.into_ok_response()?; + let mut board = board_access.lock().unwrap(); + match board.wifi_scan() { + Err(error) => { + response.write(format!("Error scanning wifi: {}", error).as_bytes())?; + }, + Ok(scan_result) => { + println!("Scan result is {:?}", scan_result); + response.write("{ ssids:[".as_bytes())?; + let mut first = true; + for ap in scan_result.iter(){ + if !first { + response.write(",".as_bytes())?; + } + response.write(ap.ssid.as_bytes())?; + first = false; + } + response.write("]".as_bytes())?; + }, + } + return Ok(()) + }).unwrap(); + return server +} + +pub fn httpd(board:&mut Box>) -> Box> { + let mut server = shared(); 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(); + .fn_handler("/",Method::Get, move |request| { + let mut response = request.into_ok_response()?; + response.write(include_bytes!("config.html"))?; + return Ok(()) + }).unwrap(); + + return server; + +} + +pub fn shared() -> Box> { + let mut server = Box::new(EspHttpServer::new(&Default::default()).unwrap()); + server .fn_handler("/version",Method::Get, |request| { let mut response = request.into_ok_response()?; @@ -78,9 +119,6 @@ pub fn httpd(initial_config:bool) -> EspHttpServer<'static> { println!("changing boot partition"); finalizer.set_as_boot_partition().unwrap(); finalizer.restart(); - - - //return Ok(()) }).unwrap(); return server; } diff --git a/rust/src/webserver/wifi.js b/rust/src/webserver/wifi.js new file mode 100644 index 0000000..2c75c4a --- /dev/null +++ b/rust/src/webserver/wifi.js @@ -0,0 +1,14 @@ +function scanWifi(){ + const req = new XMLHttpRequest(); + req.addEventListener("progress", updateProgress); + req.addEventListener("load", wifiTransferComplete); + req.addEventListener("error", transferFailed); + req.addEventListener("abort", transferCanceled); + req.open("GET", "/wifiscan"); + req.send(); +} + +function wifiTransferComplete(evt) { + console.log("The transfer is complete."); +} +