added solar ina handling, adjusted website

This commit is contained in:
Empire 2025-06-20 23:29:44 +02:00
parent 34b20b1f8f
commit 04849162cd
16 changed files with 301 additions and 42 deletions

View File

@ -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<Voltage> {
bail!("Please configure board revision")
}
fn get_mptt_current(&mut self) -> Result<Current> {
bail!("Please configure board revision")
}
}

View File

@ -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<Voltage>;
fn get_mptt_current(&mut self) -> Result<Current>;
fn get_esp(&mut self) -> &mut ESP<'a>;
fn get_config(&mut self) -> &PlantControllerConfig;
fn get_battery_monitor(&mut self) -> &mut Box<dyn BatteryInteraction + Send>;
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<BackupHeader>;
fn get_backup_config(&mut self) -> Result<Vec<u8>>;
fn backup_config(&mut self, bytes: &[u8]) -> Result<()>;
fn is_day(&self) -> bool;
//should be multsampled
fn water_temperature_c(&mut self) -> Result<f32>;
/// return median tank sensor value in milli volt

View File

@ -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<Voltage> {
//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<Current> {
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<f32> {
self.one_wire_bus
.reset(&mut self.esp.delay)

View File

@ -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 struct V4<'a> {
pub enum Charger<'a> {
SolarMpptV1 {
mppt_ina: SyncIna219<MutexDevice<'a, I2cDriver<'a>>, 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<Voltage> {
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<Current> {
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> {
esp: ESP<'a>,
charger: Charger<'a>,
battery_monitor: Box<dyn BatteryInteraction + Send>,
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<f32> {
@ -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<Voltage> {
self.charger.get_mptt_voltage()
}
fn get_mptt_current(&mut self) -> anyhow::Result<Current> {
self.charger.get_mptt_current()
}
}

View File

@ -756,13 +756,24 @@ fn wait_infinity(wait_type: WaitType, reboot_now: Arc<AtomicBool>) -> ! {
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

View File

@ -40,6 +40,13 @@ struct Moistures {
moisture_b: Vec<std::string::String>,
}
#[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<Option<std::string::String>, 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<Option<std::string::String>, anyhow::Error> {
@ -383,6 +402,11 @@ pub fn httpd(reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
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)

View File

@ -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,

View File

@ -13,7 +13,7 @@
<select class="boardvalue" id="hardware_board_value">
</select>
</div>
<div class="flexcontainer" style="text-decoration-line: line-through;">
<div class="flexcontainer">
<div class="boardkey">BatteryMonitor</div>
<select class="boardvalue" id="hardware_battery_value">
</select>

View File

@ -149,6 +149,8 @@
</div>
<div id="batteryview" class="subcontainer">
</div>
<div id="solarview" class="subcontainer">
</div>
</div>
<div class="flexcontainer">

View File

@ -28,8 +28,9 @@ import {
SetTime, SSIDList, TankInfo,
TestPump,
VersionInfo,
FileList
FileList, SolarState
} from "./api";
import {SolarView} from "./solarview";
export class Controller {
loadTankInfo() : Promise<void> {
@ -160,7 +161,7 @@ export class Controller {
console.log(error);
});
}
updateBatteryData(): Promise<void> {
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" },

View File

@ -20,7 +20,7 @@
<div class="lightkey">Test Nightlight</div>
<input class="lightcheckbox" type="checkbox" id="night_lamp_test">
</div>
<div class="flexcontainer" style="text-decoration-line: line-through;">
<div class="flexcontainer">
<div class="lightkey">Enable Nightlight</div>
<input class="lightcheckbox" type="checkbox" id="night_lamp_enabled">
</div>

View File

@ -29,7 +29,7 @@ export class OTAView {
};
test.onclick = () => {
controller.selftest();
controller.selfTest();
}
}

View File

@ -0,0 +1,29 @@
<style>
.solarflexkey {
min-width: 150px;
}
.solarflexvalue {
text-wrap: nowrap;
flex-grow: 1;
}
</style>
<div class="flexcontainer">
<div class="subtitle">
Mppt:
</div>
<input id="solar_auto_refresh" type="checkbox">
</div>
<div class="flexcontainer">
<span class="solarflexkey">Mppt mV:</span>
<span class="solarflexvalue" id="solar_voltage_milli_volt"></span>
</div>
<div class="flexcontainer">
<span class="solarflexkey">Mppt mA:</span>
<span class="solarflexvalue" id="solar_current_milli_ampere" ></span>
</div>
<div class="flexcontainer">
<span class="solarflexkey">is Day:</span>
<span class="solarflexvalue" id="solar_is_day" ></span>
</div>

View File

@ -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)
}
}
}
}

View File

@ -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;
}
</style>
<button class="submitbutton" id="submit">Submit</button>
<br>
<button id="showJson">Show Json</button>
<div id="rawdata" class="flexcontainer" style="display: none;">
<div class="submitarea" id="json" contenteditable="true"></div>
<div class="submitarea" id="backupjson">backup will be here</div>
</div>
<button id="submit">Submit</button>
<div>BackupStatus:</div>
<div id="backuptimestamp"></div>
<div id="backupsize"></div>

View File

@ -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 = '';
}