diff --git a/rust/src/hal/initial_hal.rs b/rust/src/hal/initial_hal.rs index cc15945..cda3d10 100644 --- a/rust/src/hal/initial_hal.rs +++ b/rust/src/hal/initial_hal.rs @@ -9,6 +9,7 @@ use chrono::{DateTime, Utc}; use embedded_hal::digital::OutputPin; use esp_idf_hal::gpio::{IOPin, Pull}; use esp_idf_hal::gpio::{InputOutput, PinDriver}; +use measurements::{Current, Voltage}; pub struct Initial<'a> { pub(crate) general_fault: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, @@ -125,4 +126,12 @@ impl<'a> BoardInteraction<'a> for Initial<'a> { self.esp.save_config(&self.config)?; anyhow::Ok(()) } + + fn get_mptt_voltage(&mut self) -> Result { + bail!("Please configure board revision") + } + + fn get_mptt_current(&mut self) -> Result { + bail!("Please configure board revision") + } } diff --git a/rust/src/hal/mod.rs b/rust/src/hal/mod.rs index 92d1a8b..6d2ddbb 100644 --- a/rust/src/hal/mod.rs +++ b/rust/src/hal/mod.rs @@ -42,6 +42,7 @@ use serde::{Deserialize, Serialize}; use std::result::Result::Ok as OkStd; use std::sync::Mutex; use std::time::Duration; +use measurements::{Current, Voltage}; //Only support for 8 right now! pub const PLANT_COUNT: usize = 8; @@ -125,6 +126,8 @@ pub trait BoardInteraction<'a> { fn test_pump(&mut self, plant: usize) -> Result<()>; fn test(&mut self) -> Result<()>; fn set_config(&mut self, config: PlantControllerConfig) -> Result<()>; + fn get_mptt_voltage(&mut self) -> anyhow::Result; + fn get_mptt_current(&mut self) -> anyhow::Result; } #[allow(dead_code)] diff --git a/rust/src/hal/v3_hal.rs b/rust/src/hal/v3_hal.rs index 8961c60..3645905 100644 --- a/rust/src/hal/v3_hal.rs +++ b/rust/src/hal/v3_hal.rs @@ -26,6 +26,7 @@ use esp_idf_hal::{ pcnt::{PcntChannel, PcntChannelConfig, PcntControlMode, PcntCountMode, PcntDriver, PinIndex}, }; use esp_idf_sys::{gpio_hold_dis, gpio_hold_en, vTaskDelay, EspError}; +use measurements::{Current, Voltage}; use one_wire_bus::OneWire; use plant_ctrl2::sipo::ShiftRegister40; use std::result::Result::Ok as OkStd; @@ -249,6 +250,26 @@ pub(crate) fn create_v3( } impl<'a> BoardInteraction<'a> for V3<'a> { + fn set_charge_indicator(&mut self, charging: bool) -> Result<()> { + Ok(self.shift_register.decompose()[CHARGING].set_state(charging.into())?) + } + + fn is_day(&self) -> bool { + self.solar_is_day.get_level().into() + } + + fn get_mptt_voltage(&mut self) -> Result { + //if working this is the hardware set mppt voltage + if self.is_day() { + Ok(Voltage::from_volts(15_f64)) + } else { + Ok(Voltage::from_volts(0_f64)) + } + } + + fn get_mptt_current(&mut self) -> Result { + bail!("Board does not have current sensor") + } fn get_esp(&mut self) -> &mut ESP<'a> { &mut self.esp } @@ -261,10 +282,6 @@ impl<'a> BoardInteraction<'a> for V3<'a> { &mut self.battery_monitor } - fn set_charge_indicator(&mut self, charging: bool) -> Result<()> { - Ok(self.shift_register.decompose()[CHARGING].set_state(charging.into())?) - } - fn deep_sleep(&mut self, duration_in_ms: u64) -> ! { let _ = self.shift_register.decompose()[AWAKE].set_low(); deep_sleep(duration_in_ms) @@ -366,10 +383,6 @@ impl<'a> BoardInteraction<'a> for V3<'a> { Ok(()) } - fn is_day(&self) -> bool { - self.solar_is_day.get_level().into() - } - fn water_temperature_c(&mut self) -> Result { self.one_wire_bus .reset(&mut self.esp.delay) diff --git a/rust/src/hal/v4_hal.rs b/rust/src/hal/v4_hal.rs index ab60976..e3b121b 100644 --- a/rust/src/hal/v4_hal.rs +++ b/rust/src/hal/v4_hal.rs @@ -1,4 +1,4 @@ -pub use crate::config::PlantControllerConfig; +use crate::config::PlantControllerConfig; use crate::hal::battery::BatteryInteraction; use crate::hal::esp::ESP; use crate::hal::{ @@ -13,22 +13,24 @@ use ds323x::{DateTimeAccess, Ds323x}; use eeprom24x::{Eeprom24x, Eeprom24xTrait, SlaveAddr}; use embedded_hal::digital::OutputPin; use embedded_hal_bus::i2c::MutexDevice; -use esp_idf_hal::{ - adc::{ - attenuation, - oneshot::{config::AdcChannelConfig, AdcChannelDriver, AdcDriver}, - Resolution, - }, - delay::Delay, - gpio::{AnyInputPin, Gpio5, IOPin, InputOutput, Output, PinDriver, Pull}, - i2c::I2cDriver, - pcnt::{PcntChannel, PcntChannelConfig, PcntControlMode, PcntCountMode, PcntDriver, PinIndex}, +use esp_idf_hal::adc::oneshot::config::AdcChannelConfig; +use esp_idf_hal::adc::oneshot::{AdcChannelDriver, AdcDriver}; +use esp_idf_hal::adc::{attenuation, Resolution}; +use esp_idf_hal::delay::Delay; +use esp_idf_hal::gpio::{AnyInputPin, Gpio5, IOPin, InputOutput, Output, PinDriver, Pull}; +use esp_idf_hal::i2c::I2cDriver; +use esp_idf_hal::pcnt::{ + PcntChannel, PcntChannelConfig, PcntControlMode, PcntCountMode, PcntDriver, PinIndex, }; -use esp_idf_sys::{gpio_hold_dis, gpio_hold_en, EspError}; -use ina219::{address::Address, calibration::UnCalibrated, SyncIna219}; +use esp_idf_sys::{gpio_hold_dis, gpio_hold_en, vTaskDelay, EspError}; +use ina219::address::{Address, Pin}; +use ina219::calibration::{Calibration, UnCalibrated}; +use ina219::SyncIna219; +use measurements::{Current, Resistance, Voltage}; use one_wire_bus::OneWire; use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface}; use std::result::Result::Ok as OkStd; +use ina219::configuration::{Configuration, OperatingMode}; const MS0: u8 = 1_u8; const MS1: u8 = 0_u8; @@ -37,15 +39,78 @@ const MS3: u8 = 4_u8; const MS4: u8 = 2_u8; const SENSOR_ON: u8 = 5_u8; +pub enum Charger<'a> { + SolarMpptV1 { + mppt_ina: SyncIna219>, UnCalibrated>, + solar_is_day: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, esp_idf_hal::gpio::Input>, + charge_indicator: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, + }, +} + +impl<'a> Charger<'a> { + pub(crate) fn powersave(&mut self) { + match self { Charger::SolarMpptV1 { mppt_ina, .. } => { + let _ = mppt_ina.set_configuration(Configuration { + reset: Default::default(), + bus_voltage_range: Default::default(), + shunt_voltage_range: Default::default(), + bus_resolution: Default::default(), + shunt_resolution: Default::default(), + operating_mode: OperatingMode::PowerDown, + }).map_err(|e| { + println!( + "Error setting ina mppt configuration during deepsleep preparation{:?}", + e + ); + }); + } } + } + fn set_charge_indicator(&mut self, charging: bool) -> anyhow::Result<()> { + match self { + Self::SolarMpptV1 { + charge_indicator, .. + } => { + charge_indicator.set_state(charging.into())?; + } + } + Ok(()) + } + + fn is_day(&self) -> bool { + match self { + Charger::SolarMpptV1 { solar_is_day, .. } => solar_is_day.get_level().into(), + } + } + + fn get_mptt_voltage(&mut self) -> anyhow::Result { + let voltage = match self { + Charger::SolarMpptV1 { mppt_ina, .. } => mppt_ina + .bus_voltage() + .map(|v| Voltage::from_millivolts(v.voltage_mv() as f64))?, + }; + Ok(voltage) + } + + fn get_mptt_current(&mut self) -> anyhow::Result { + let current = match self { + Charger::SolarMpptV1 { mppt_ina, .. } => mppt_ina.shunt_voltage().map(|v| { + let shunt_voltage = Voltage::from_microvolts(v.shunt_voltage_uv().abs() as f64); + let shut_value = Resistance::from_ohms(0.05_f64); + let current = shunt_voltage.as_volts() / shut_value.as_ohms(); + Current::from_amperes(current) + })?, + }; + Ok(current) + } +} + pub struct V4<'a> { - _mppt_ina: SyncIna219>, UnCalibrated>, esp: ESP<'a>, + charger: Charger<'a>, battery_monitor: Box, config: PlantControllerConfig, tank_channel: AdcChannelDriver<'a, Gpio5, AdcDriver<'a, esp_idf_hal::adc::ADC1>>, - solar_is_day: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, esp_idf_hal::gpio::Input>, signal_counter: PcntDriver<'a>, - charge_indicator: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, awake: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, Output>, light: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, tank_power: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, @@ -63,12 +128,12 @@ pub struct V4<'a> { sensor_expander: Pca9535Immediate>>, } -pub(crate) fn create_v4<'a>( +pub(crate) fn create_v4( peripherals: FreePeripherals, esp: ESP<'static>, config: PlantControllerConfig, battery_monitor: Box, -) -> anyhow::Result + Send>> { +) -> anyhow::Result> { let mut awake = PinDriver::output(peripherals.gpio15.downgrade())?; awake.set_high()?; @@ -173,7 +238,21 @@ pub(crate) fn create_v4<'a>( let _ = sensor_expander.pin_set_low(GPIOBank::Bank1, pin); } - let mut mppt_ina = SyncIna219::new(MutexDevice::new(&I2C_DRIVER), Address::from_byte(68)?)?; + //TODO error handling is not done nicely here, should not break if ina is not responsive + let mut mppt_ina = SyncIna219::new( + MutexDevice::new(&I2C_DRIVER), + Address::from_pins(Pin::Vcc, Pin::Gnd), + )?; + + mppt_ina.set_configuration(Configuration{ + reset: Default::default(), + bus_voltage_range: Default::default(), + shunt_voltage_range: Default::default(), + bus_resolution: Default::default(), + shunt_resolution: ina219::configuration::Resolution::Avg128, + operating_mode: Default::default(), + })?; + //TODO this is probably laready done untill we are ready first time?, maybee add startup time comparison on access? esp.delay.delay_ms( mppt_ina .configuration()? @@ -181,18 +260,17 @@ pub(crate) fn create_v4<'a>( .unwrap() .as_millis() as u32, ); - println!("Bus Voltage: {}", mppt_ina.bus_voltage()?); - println!("Shunt Voltage: {}", mppt_ina.shunt_voltage()?); - let volt = (mppt_ina.shunt_voltage()?.shunt_voltage_mv()) as f32 / 1000_f32; - let current = volt / 0.05; - println!("Shunt Current: {}", current); + + let charger = Charger::SolarMpptV1 { + mppt_ina, + solar_is_day, + charge_indicator, + }; let v = V4 { - _mppt_ina: mppt_ina, esp, awake, tank_channel, - solar_is_day, signal_counter, light, tank_power, @@ -202,9 +280,9 @@ pub(crate) fn create_v4<'a>( general_fault, pump_expander, sensor_expander, - charge_indicator, config, battery_monitor, + charger, }; Ok(Box::new(v)) } @@ -223,14 +301,12 @@ impl<'a> BoardInteraction<'a> for V4<'a> { } fn set_charge_indicator(&mut self, charging: bool) -> anyhow::Result<()> { - self.charge_indicator - .set_state(charging.into()) - .expect("cannot fail"); - Ok(()) + self.charger.set_charge_indicator(charging) } fn deep_sleep(&mut self, duration_in_ms: u64) -> ! { self.awake.set_low().unwrap(); + self.charger.powersave(); deep_sleep(duration_in_ms); } @@ -331,7 +407,7 @@ impl<'a> BoardInteraction<'a> for V4<'a> { } fn is_day(&self) -> bool { - self.solar_is_day.get_level().into() + self.charger.is_day() } fn water_temperature_c(&mut self) -> anyhow::Result { @@ -568,4 +644,12 @@ impl<'a> BoardInteraction<'a> for V4<'a> { self.esp.save_config(&self.config)?; anyhow::Ok(()) } + + fn get_mptt_voltage(&mut self) -> anyhow::Result { + self.charger.get_mptt_voltage() + } + + fn get_mptt_current(&mut self) -> anyhow::Result { + self.charger.get_mptt_current() + } } diff --git a/rust/src/main.rs b/rust/src/main.rs index dfad2a7..5a9d60f 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -754,13 +754,24 @@ fn wait_infinity(wait_type: WaitType, reboot_now: Arc) -> ! { loop { unsafe { let mut board = BOARD_ACCESS.lock().unwrap(); - if let Ok(charging) = board + + //we have mppt controller, ask it for charging current + if let Ok(current) = board.board_hal.get_mptt_current() { + let _ = board.board_hal.set_charge_indicator(current.as_milliamperes() > 20_f64); + } + //fallback to battery controller and ask it instead + else if let Ok(charging) = board .board_hal .get_battery_monitor() .average_current_milli_ampere() { let _ = board.board_hal.set_charge_indicator(charging > 20); } + else { + //who knows + let _ = board.board_hal.set_charge_indicator(false); + } + match wait_type { WaitType::MissingConfig => { // Keep existing behavior: circular filling pattern diff --git a/rust/src/webserver/webserver.rs b/rust/src/webserver/webserver.rs index f52c713..93af359 100644 --- a/rust/src/webserver/webserver.rs +++ b/rust/src/webserver/webserver.rs @@ -40,6 +40,13 @@ struct Moistures { moisture_b: Vec, } +#[derive(Serialize, Debug)] +struct SolarState { + mppt_voltage: f32, + mppt_current: f32, + is_day: bool, +} + #[derive(Deserialize, Debug)] struct SetTime<'a> { time: &'a str, @@ -218,6 +225,18 @@ fn set_config( anyhow::Ok(Some("saved".to_owned())) } +fn get_solar_state( + _request: &mut Request<&mut EspHttpConnection>, +) -> Result, anyhow::Error> { + let mut board = BOARD_ACCESS.lock().expect("board access"); + let state = SolarState { + mppt_voltage: board.board_hal.get_mptt_voltage()?.as_volts() as f32, + mppt_current: board.board_hal.get_mptt_current()?.as_amperes() as f32, + is_day: board.board_hal.is_day(), + }; + anyhow::Ok(Some(serde_json::to_string(&state)?)) +} + fn get_battery_state( _request: &mut Request<&mut EspHttpConnection>, ) -> Result, anyhow::Error> { @@ -383,6 +402,11 @@ pub fn httpd(reboot_now: Arc) -> Box> { handle_error_to500(request, get_battery_state) }) .unwrap(); + server + .fn_handler("/solar", Method::Get, |request| { + handle_error_to500(request, get_solar_state) + }) + .unwrap(); server .fn_handler("/time", Method::Get, |request| { handle_error_to500(request, get_time) diff --git a/rust/src_webpack/src/api.ts b/rust/src_webpack/src/api.ts index faff2ca..712d639 100644 --- a/rust/src_webpack/src/api.ts +++ b/rust/src_webpack/src/api.ts @@ -38,6 +38,12 @@ export interface FileList { iter_error: string, } +export interface SolarState{ + mppt_voltage: number, + mppt_current: number, + is_day: boolean +} + export interface FileInfo{ filename: string, size: number, diff --git a/rust/src_webpack/src/hardware.html b/rust/src_webpack/src/hardware.html index 71a7f32..0b2a24b 100644 --- a/rust/src_webpack/src/hardware.html +++ b/rust/src_webpack/src/hardware.html @@ -13,7 +13,7 @@ -
+
BatteryMonitor
diff --git a/rust/src_webpack/src/main.html b/rust/src_webpack/src/main.html index a706228..d78b38a 100644 --- a/rust/src_webpack/src/main.html +++ b/rust/src_webpack/src/main.html @@ -149,6 +149,8 @@
+
+
diff --git a/rust/src_webpack/src/main.ts b/rust/src_webpack/src/main.ts index d34145c..8463eb2 100644 --- a/rust/src_webpack/src/main.ts +++ b/rust/src_webpack/src/main.ts @@ -28,8 +28,9 @@ import { SetTime, SSIDList, TankInfo, TestPump, VersionInfo, - FileList + FileList, SolarState } from "./api"; +import {SolarView} from "./solarview"; export class Controller { loadTankInfo() : Promise { @@ -160,7 +161,7 @@ export class Controller { console.log(error); }); } - updateBatteryData(): Promise { + updateBatteryData() { return fetch(PUBLIC_URL + "/battery") .then(response => response.json()) .then(json => json as BatteryState) @@ -172,6 +173,18 @@ export class Controller { console.log(error); }) } + updateSolarData() { + return fetch(PUBLIC_URL + "/solar") + .then(response => response.json()) + .then(json => json as SolarState) + .then(solar => { + controller.solarView.update(solar) + }) + .catch(error => { + controller.solarView.update(null) + console.log(error); + }) + } uploadNewFirmware(file: File) { var current = 0; var max = 100; @@ -244,6 +257,7 @@ export class Controller { //load from remote to be clean controller.downloadConfig() } + backupConfig(json: string, statusCallback: (status: string) => void) { controller.progressview.addIndeterminate("backup_config", "Backingup Config") fetch(PUBLIC_URL + "/backup_config", { @@ -465,6 +479,7 @@ export class Controller { readonly firmWareView: OTAView; readonly progressview: ProgressView; readonly batteryView: BatteryView; + readonly solarView: SolarView; readonly fileview: FileView; readonly logView: LogView constructor() { @@ -473,6 +488,7 @@ export class Controller { this.networkView = new NetworkConfigView(this, PUBLIC_URL) this.tankView = new TankConfigView(this) this.batteryView = new BatteryView(this) + this.solarView = new SolarView(this) this.nightLampView = new NightLampView(this) this.submitView = new SubmitView(this) this.firmWareView = new OTAView(this) @@ -489,21 +505,16 @@ export class Controller { controller.exit(); } } - - selftest() { - - } } const controller = new Controller(); controller.progressview.removeProgress("rebooting"); - - const tasks = [ { task: controller.populateTimezones, displayString: "Populating Timezones" }, { task: controller.updateRTCData, displayString: "Updating RTC Data" }, { task: controller.updateBatteryData, displayString: "Updating Battery Data" }, + { task: controller.updateSolarData, displayString: "Updating Solar Data" }, { task: controller.downloadConfig, displayString: "Downloading Configuration" }, { task: controller.version, displayString: "Fetching Version Information" }, { task: controller.updateFileList, displayString: "Updating File List" }, diff --git a/rust/src_webpack/src/nightlightview.html b/rust/src_webpack/src/nightlightview.html index fbcc0fb..562f3d7 100644 --- a/rust/src_webpack/src/nightlightview.html +++ b/rust/src_webpack/src/nightlightview.html @@ -20,7 +20,7 @@
Test Nightlight
-
+
Enable Nightlight
diff --git a/rust/src_webpack/src/ota.ts b/rust/src_webpack/src/ota.ts index 923e5fb..f596ff1 100644 --- a/rust/src_webpack/src/ota.ts +++ b/rust/src_webpack/src/ota.ts @@ -29,7 +29,7 @@ export class OTAView { }; test.onclick = () => { - controller.selftest(); + controller.selfTest(); } } diff --git a/rust/src_webpack/src/solarview.html b/rust/src_webpack/src/solarview.html new file mode 100644 index 0000000..da829a2 --- /dev/null +++ b/rust/src_webpack/src/solarview.html @@ -0,0 +1,29 @@ + + +
+
+ Mppt: +
+ ⟳ +
+ +
+ Mppt mV: + +
+
+ Mppt mA: + +
+
+ is Day: + +
\ No newline at end of file diff --git a/rust/src_webpack/src/solarview.ts b/rust/src_webpack/src/solarview.ts new file mode 100644 index 0000000..641f518 --- /dev/null +++ b/rust/src_webpack/src/solarview.ts @@ -0,0 +1,49 @@ +import { Controller } from "./main"; +import {BatteryState, SolarState} from "./api"; + +export class SolarView{ + solar_voltage_milli_volt: HTMLSpanElement; + solar_current_milli_ampere: HTMLSpanElement; + solar_is_day: HTMLSpanElement; + solar_auto_refresh: HTMLInputElement; + timer: NodeJS.Timeout | undefined; + controller: Controller; + + constructor (controller:Controller) { + (document.getElementById("solarview") as HTMLElement).innerHTML = require("./solarview.html") + this.solar_voltage_milli_volt = document.getElementById("solar_voltage_milli_volt") as HTMLSpanElement; + this.solar_current_milli_ampere = document.getElementById("solar_current_milli_ampere") as HTMLSpanElement; + this.solar_is_day = document.getElementById("solar_is_day") as HTMLSpanElement; + this.solar_auto_refresh = document.getElementById("solar_auto_refresh") as HTMLInputElement; + + this.controller = controller + this.solar_auto_refresh.onchange = () => { + if(this.timer){ + clearTimeout(this.timer) + } + if(this.solar_auto_refresh.checked){ + controller.updateSolarData() + } + } + } + + update(solarState: SolarState|null){ + if (solarState == null) { + this.solar_voltage_milli_volt.innerText = "N/A" + this.solar_current_milli_ampere.innerText = "N/A" + this.solar_is_day.innerText = "N/A" + } else { + this.solar_voltage_milli_volt.innerText = solarState.mppt_voltage.toFixed(2) + this.solar_current_milli_ampere.innerText = String(+solarState.mppt_current) + this.solar_is_day.innerText = solarState.is_day?"🌞":"🌙" + } + + if(this.solar_auto_refresh.checked){ + this.timer = setTimeout(this.controller.updateSolarData, 1000); + } else { + if(this.timer){ + clearTimeout(this.timer) + } + } + } + } \ No newline at end of file diff --git a/rust/src_webpack/src/submitview.html b/rust/src_webpack/src/submitview.html index 19e4842..6af8e96 100644 --- a/rust/src_webpack/src/submitview.html +++ b/rust/src_webpack/src/submitview.html @@ -7,14 +7,31 @@ word-wrap: break-word; overflow: scroll; } + .submitbutton{ + padding: 1em 1em; + background: #667eea; + color: white; + border: none; + border-radius: 8px; + font-size: 1.1em; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + letter-spacing: 1px; + margin: 1em 0; + } + .submitbutton:hover { + background: #1c4e63; + } + +
-
BackupStatus:
diff --git a/rust/src_webpack/webpack.config.js b/rust/src_webpack/webpack.config.js index fa36e6f..61466fa 100644 --- a/rust/src_webpack/webpack.config.js +++ b/rust/src_webpack/webpack.config.js @@ -9,7 +9,7 @@ console.log("Dev server is " + isDevServer); var host; if (isDevServer){ //ensure no trailing / - host = 'http://192.168.71.1'; + host = 'http://10.23.44.186'; } else { host = ''; }