From 04849162cd071996bae4ca845aa4a5bb56bd6917 Mon Sep 17 00:00:00 2001 From: Empire Date: Fri, 20 Jun 2025 23:29:44 +0200 Subject: [PATCH] added solar ina handling, adjusted website --- rust/src/hal/initial_hal.rs | 9 ++ rust/src/hal/mod.rs | 8 +- rust/src/hal/v3_hal.rs | 29 ++++-- rust/src/hal/v4_hal.rs | 120 +++++++++++++++++++---- rust/src/main.rs | 13 ++- rust/src/webserver/webserver.rs | 24 +++++ rust/src_webpack/src/api.ts | 6 ++ rust/src_webpack/src/hardware.html | 2 +- rust/src_webpack/src/main.html | 2 + rust/src_webpack/src/main.ts | 27 +++-- rust/src_webpack/src/nightlightview.html | 2 +- rust/src_webpack/src/ota.ts | 2 +- rust/src_webpack/src/solarview.html | 29 ++++++ rust/src_webpack/src/solarview.ts | 49 +++++++++ rust/src_webpack/src/submitview.html | 19 +++- rust/src_webpack/webpack.config.js | 2 +- 16 files changed, 301 insertions(+), 42 deletions(-) create mode 100644 rust/src_webpack/src/solarview.html create mode 100644 rust/src_webpack/src/solarview.ts diff --git a/rust/src/hal/initial_hal.rs b/rust/src/hal/initial_hal.rs index f11e416..c32b65c 100644 --- a/rust/src/hal/initial_hal.rs +++ b/rust/src/hal/initial_hal.rs @@ -7,6 +7,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>, @@ -123,4 +124,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 232664c..51b8354 100644 --- a/rust/src/hal/mod.rs +++ b/rust/src/hal/mod.rs @@ -50,6 +50,7 @@ use esp_idf_hal::gpio::{ use esp_idf_hal::pcnt::PCNT0; use esp_idf_hal::prelude::Peripherals; use esp_idf_hal::reset::ResetReason; +use measurements::{Current, Voltage}; use pca9535::StandardExpanderInterface; //Only support for 8 right now! @@ -115,15 +116,18 @@ impl Default for BackupHeader { } pub trait BoardInteraction<'a> { + fn set_charge_indicator(&mut self, charging: bool) -> Result<()>; + fn is_day(&self) -> bool; + fn get_mptt_voltage(&mut self) -> Result; + fn get_mptt_current(&mut self) -> Result; + fn get_esp(&mut self) -> &mut ESP<'a>; fn get_config(&mut self) -> &PlantControllerConfig; fn get_battery_monitor(&mut self) -> &mut Box; - fn set_charge_indicator(&mut self, charging: bool) -> Result<()>; fn deep_sleep(&mut self, duration_in_ms: u64) -> !; fn get_backup_info(&mut self) -> Result; fn get_backup_config(&mut self) -> Result>; fn backup_config(&mut self, bytes: &[u8]) -> Result<()>; - fn is_day(&self) -> bool; //should be multsampled fn water_temperature_c(&mut self) -> Result; /// return median tank sensor value in milli volt diff --git a/rust/src/hal/v3_hal.rs b/rust/src/hal/v3_hal.rs index ee2fbfe..8eb74e7 100644 --- a/rust/src/hal/v3_hal.rs +++ b/rust/src/hal/v3_hal.rs @@ -23,6 +23,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; @@ -247,6 +248,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 } @@ -259,10 +280,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) @@ -364,10 +381,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 b63141a..e3b121b 100644 --- a/rust/src/hal/v4_hal.rs +++ b/rust/src/hal/v4_hal.rs @@ -23,12 +23,14 @@ use esp_idf_hal::pcnt::{ PcntChannel, PcntChannelConfig, PcntControlMode, PcntCountMode, PcntDriver, PinIndex, }; use esp_idf_sys::{gpio_hold_dis, gpio_hold_en, vTaskDelay, EspError}; -use ina219::address::Address; +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>, @@ -173,7 +238,21 @@ pub(crate) fn create_v4( 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( .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, esp, awake, tank_channel, - solar_is_day, signal_counter, light, tank_power, @@ -202,9 +280,9 @@ pub(crate) fn create_v4( 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 d2a63d1..54dc69b 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -756,13 +756,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 53ca85d..aa79e51 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 00e4ebc..2caa51d 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 = ''; }