add pump current configuration

This commit is contained in:
Empire Phoenix 2025-06-26 02:39:32 +02:00
parent 577c38d026
commit 5d91daf23d
12 changed files with 492 additions and 275 deletions

View File

@ -118,6 +118,9 @@ pub struct PlantConfig {
pub max_consecutive_pump_count: u8, pub max_consecutive_pump_count: u8,
pub moisture_sensor_min_frequency: Option<f32>, // Optional min frequency pub moisture_sensor_min_frequency: Option<f32>, // Optional min frequency
pub moisture_sensor_max_frequency: Option<f32>, // Optional max frequency pub moisture_sensor_max_frequency: Option<f32>, // Optional max frequency
pub min_pump_current_ma: u16,
pub max_pump_current_ma: u16,
pub ignore_current_error: bool,
} }
impl Default for PlantConfig { impl Default for PlantConfig {
@ -134,6 +137,9 @@ impl Default for PlantConfig {
max_consecutive_pump_count: 10, max_consecutive_pump_count: 10,
moisture_sensor_min_frequency: None, // No override by default moisture_sensor_min_frequency: None, // No override by default
moisture_sensor_max_frequency: None, // No override by default moisture_sensor_max_frequency: None, // No override by default
min_pump_current_ma: 10,
max_pump_current_ma: 3000,
ignore_current_error: true,
} }
} }
} }

View File

@ -107,6 +107,10 @@ impl<'a> BoardInteraction<'a> for Initial<'a> {
bail!("Please configure board revision") bail!("Please configure board revision")
} }
fn pump_current(&mut self, _plant: usize) -> Result<Current> {
bail!("Please configure board revision")
}
fn fault(&mut self, _plant: usize, _enable: bool) -> Result<()> { fn fault(&mut self, _plant: usize, _enable: bool) -> Result<()> {
bail!("Please configure board revision") bail!("Please configure board revision")
} }
@ -119,10 +123,6 @@ impl<'a> BoardInteraction<'a> for Initial<'a> {
let _ = self.general_fault.set_state(enable.into()); let _ = self.general_fault.set_state(enable.into());
} }
fn test_pump(&mut self, _plant: usize) -> Result<()> {
bail!("Please configure board revision")
}
fn test(&mut self) -> Result<()> { fn test(&mut self) -> Result<()> {
bail!("Please configure board revision") bail!("Please configure board revision")
} }

View File

@ -99,11 +99,10 @@ pub trait BoardInteraction<'a> {
//should be multsampled //should be multsampled
fn light(&mut self, enable: bool) -> Result<()>; fn light(&mut self, enable: bool) -> Result<()>;
fn pump(&mut self, plant: usize, enable: bool) -> Result<()>; fn pump(&mut self, plant: usize, enable: bool) -> Result<()>;
fn pump_current(&mut self, plant: usize) -> Result<Current>;
fn fault(&mut self, plant: usize, enable: bool) -> Result<()>; fn fault(&mut self, plant: usize, enable: bool) -> Result<()>;
fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<f32>; fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<f32>;
fn general_fault(&mut self, enable: bool); fn general_fault(&mut self, enable: bool);
fn test_pump(&mut self, plant: usize) -> Result<()>;
fn test(&mut self) -> Result<()>; fn test(&mut self) -> Result<()>;
fn set_config(&mut self, config: PlantControllerConfig) -> Result<()>; fn set_config(&mut self, config: PlantControllerConfig) -> Result<()>;
fn get_mptt_voltage(&mut self) -> anyhow::Result<Voltage>; fn get_mptt_voltage(&mut self) -> anyhow::Result<Voltage>;

View File

@ -14,7 +14,7 @@ use esp_idf_hal::{
gpio::{AnyInputPin, IOPin, InputOutput, PinDriver, Pull}, gpio::{AnyInputPin, IOPin, InputOutput, PinDriver, Pull},
pcnt::{PcntChannel, PcntChannelConfig, PcntControlMode, PcntCountMode, PcntDriver, PinIndex}, pcnt::{PcntChannel, PcntChannelConfig, PcntControlMode, PcntCountMode, PcntDriver, PinIndex},
}; };
use esp_idf_sys::{gpio_hold_dis, gpio_hold_en, vTaskDelay}; use esp_idf_sys::{gpio_hold_dis, gpio_hold_en};
use measurements::{Current, Voltage}; use measurements::{Current, Voltage};
use plant_ctrl2::sipo::ShiftRegister40; use plant_ctrl2::sipo::ShiftRegister40;
use std::result::Result::Ok as OkStd; use std::result::Result::Ok as OkStd;
@ -252,6 +252,10 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
Ok(()) Ok(())
} }
fn pump_current(&mut self, _plant: usize) -> Result<Current> {
bail!("Not implemented in v3")
}
fn fault(&mut self, plant: usize, enable: bool) -> Result<()> { fn fault(&mut self, plant: usize, enable: bool) -> Result<()> {
let index = match plant { let index = match plant {
0 => FAULT_1, 0 => FAULT_1,
@ -365,13 +369,6 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
unsafe { gpio_hold_en(self.general_fault.pin()) }; unsafe { gpio_hold_en(self.general_fault.pin()) };
} }
fn test_pump(&mut self, plant: usize) -> Result<()> {
self.pump(plant, true)?;
unsafe { vTaskDelay(30000) };
self.pump(plant, false)?;
Ok(())
}
fn test(&mut self) -> Result<()> { fn test(&mut self) -> Result<()> {
self.general_fault(true); self.general_fault(true);
self.esp.delay.delay_ms(100); self.esp.delay.delay_ms(100);

View File

@ -13,7 +13,7 @@ use embedded_hal::digital::OutputPin;
use embedded_hal_bus::i2c::MutexDevice; use embedded_hal_bus::i2c::MutexDevice;
use esp_idf_hal::delay::Delay; use esp_idf_hal::delay::Delay;
use esp_idf_hal::gpio::{AnyInputPin, IOPin, InputOutput, Output, PinDriver, Pull}; use esp_idf_hal::gpio::{AnyInputPin, IOPin, InputOutput, Output, PinDriver, Pull};
use esp_idf_hal::i2c::{I2cDriver, I2cError}; use esp_idf_hal::i2c::I2cDriver;
use esp_idf_hal::pcnt::{ use esp_idf_hal::pcnt::{
PcntChannel, PcntChannelConfig, PcntControlMode, PcntCountMode, PcntDriver, PinIndex, PcntChannel, PcntChannelConfig, PcntControlMode, PcntCountMode, PcntDriver, PinIndex,
}; };
@ -21,7 +21,6 @@ use esp_idf_sys::{gpio_hold_dis, gpio_hold_en};
use ina219::address::{Address, Pin}; use ina219::address::{Address, Pin};
use ina219::calibration::UnCalibrated; use ina219::calibration::UnCalibrated;
use ina219::configuration::{Configuration, OperatingMode}; use ina219::configuration::{Configuration, OperatingMode};
use ina219::errors::InitializationError;
use ina219::SyncIna219; use ina219::SyncIna219;
use measurements::{Current, Resistance, Voltage}; use measurements::{Current, Resistance, Voltage};
use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface}; use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface};
@ -125,6 +124,7 @@ pub struct V4<'a> {
light: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, light: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>,
general_fault: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, general_fault: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>,
pump_expander: Pca9535Immediate<MutexDevice<'a, I2cDriver<'a>>>, pump_expander: Pca9535Immediate<MutexDevice<'a, I2cDriver<'a>>>,
pump_ina: Option<SyncIna219<MutexDevice<'a, I2cDriver<'a>>, UnCalibrated>>,
sensor_expander: Pca9535Immediate<MutexDevice<'a, I2cDriver<'a>>>, sensor_expander: Pca9535Immediate<MutexDevice<'a, I2cDriver<'a>>>,
extra1: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, Output>, extra1: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, Output>,
extra2: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, Output>, extra2: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, Output>,
@ -208,7 +208,7 @@ pub(crate) fn create_v4(
let _ = sensor_expander.pin_set_low(GPIOBank::Bank1, pin); let _ = sensor_expander.pin_set_low(GPIOBank::Bank1, pin);
} }
let mut mppt_ina = SyncIna219::new( let mppt_ina = SyncIna219::new(
MutexDevice::new(&I2C_DRIVER), MutexDevice::new(&I2C_DRIVER),
Address::from_pins(Pin::Vcc, Pin::Gnd), Address::from_pins(Pin::Vcc, Pin::Gnd),
); );
@ -224,15 +224,6 @@ pub(crate) fn create_v4(
operating_mode: Default::default(), operating_mode: Default::default(),
})?; })?;
//TODO this is probably already done until we are ready first time?, maybe add startup time comparison on access?
esp.delay.delay_ms(
mppt_ina
.configuration()?
.conversion_time()
.unwrap()
.as_millis() as u32,
);
Charger::SolarMpptV1 { Charger::SolarMpptV1 {
mppt_ina, mppt_ina,
solar_is_day, solar_is_day,
@ -242,6 +233,17 @@ pub(crate) fn create_v4(
Err(_) => Charger::ErrorInit {}, Err(_) => Charger::ErrorInit {},
}; };
let pump_ina = match SyncIna219::new(
MutexDevice::new(&I2C_DRIVER),
Address::from_pins(Pin::Gnd, Pin::Sda),
) {
Ok(pump_ina) => Some(pump_ina),
Err(err) => {
println!("Error creating pump ina: {:?}", err);
None
}
};
let v = V4 { let v = V4 {
rtc_module, rtc_module,
esp, esp,
@ -250,6 +252,7 @@ pub(crate) fn create_v4(
signal_counter, signal_counter,
light, light,
general_fault, general_fault,
pump_ina,
pump_expander, pump_expander,
sensor_expander, sensor_expander,
config, config,
@ -314,6 +317,24 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
anyhow::Ok(()) anyhow::Ok(())
} }
fn pump_current(&mut self, _plant: usize) -> anyhow::Result<Current> {
//sensore is shared for all pumps, ignore plant id
match self.pump_ina.as_mut() {
None => {
bail!("pump current sensor not available");
}
Some(pump_ina) => {
let v = pump_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(v)
}
}
}
fn fault(&mut self, plant: usize, enable: bool) -> anyhow::Result<()> { fn fault(&mut self, plant: usize, enable: bool) -> anyhow::Result<()> {
if enable { if enable {
self.pump_expander self.pump_expander
@ -406,13 +427,6 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
unsafe { gpio_hold_en(self.general_fault.pin()) }; unsafe { gpio_hold_en(self.general_fault.pin()) };
} }
fn test_pump(&mut self, plant: usize) -> anyhow::Result<()> {
self.pump(plant, true)?;
self.esp.delay.delay_ms(30000);
self.pump(plant, false)?;
anyhow::Ok(())
}
fn test(&mut self) -> anyhow::Result<()> { fn test(&mut self) -> anyhow::Result<()> {
self.general_fault(true); self.general_fault(true);
self.esp.delay.delay_ms(100); self.esp.delay.delay_ms(100);

View File

@ -93,6 +93,7 @@ pub fn log(message_key: LogMessage, number_a: u32, number_b: u32, txt_short: &st
let serial_entry = template.fill_in(&values); let serial_entry = template.fill_in(&values);
println!("{serial_entry}"); println!("{serial_entry}");
//TODO push to mqtt?
let entry = LogEntry { let entry = LogEntry {
timestamp: time, timestamp: time,
@ -193,6 +194,16 @@ pub enum LogMessage {
serialize = "Pumped multiple times, but plant is still to try attempt: ${number_a} limit :: ${number_b} plant: ${txt_short}" serialize = "Pumped multiple times, but plant is still to try attempt: ${number_a} limit :: ${number_b} plant: ${txt_short}"
)] )]
ConsecutivePumpCountLimit, ConsecutivePumpCountLimit,
#[strum(
serialize = "Pump Overcurrent error, pump: ${number_a} tripped overcurrent ${number_b} limit was ${txt_short} @s ${txt_long}"
)]
PumpOverCurrent,
#[strum(
serialize = "Pump Open loop error, pump: ${number_a} is low, ${number_b} limit was ${txt_short} @s ${txt_long}"
)]
PumpOpenLoopCurrent,
#[strum(serialize = "Pump Open current sensor required but did not work: ${number_a}")]
PumpMissingSensorCurrent,
} }
#[derive(Serialize)] #[derive(Serialize)]

View File

@ -1,3 +1,4 @@
use crate::config::PlantConfig;
use crate::{ use crate::{
config::BoardVersion::INITIAL, config::BoardVersion::INITIAL,
hal::{PlantHal, HAL, PLANT_COUNT}, hal::{PlantHal, HAL, PLANT_COUNT},
@ -76,6 +77,17 @@ struct LightState {
struct PumpInfo { struct PumpInfo {
enabled: bool, enabled: bool,
pump_ineffective: bool, pump_ineffective: bool,
median_current_ma: u16,
max_current_ma: u16,
min_current_ma: u16,
}
#[derive(Serialize)]
pub struct PumpResult {
median_current_ma: u16,
max_current_ma: u16,
min_current_ma: u16,
error: bool,
} }
#[derive(Serialize, Deserialize, Debug, PartialEq)] #[derive(Serialize, Deserialize, Debug, PartialEq)]
@ -225,6 +237,8 @@ fn safe_main() -> anyhow::Result<()> {
try_connect_wifi_sntp_mqtt(&mut board) try_connect_wifi_sntp_mqtt(&mut board)
} else { } else {
println!("No wifi configured"); println!("No wifi configured");
//the current sensors require this amount to stabilize, in case of wifi this is already handles for sure;
board.board_hal.get_esp().delay.delay_ms(100);
NetworkMode::OFFLINE NetworkMode::OFFLINE
}; };
@ -395,14 +409,20 @@ fn safe_main() -> anyhow::Result<()> {
board.board_hal.get_esp().last_pump_time(plant_id); board.board_hal.get_esp().last_pump_time(plant_id);
//state.active = true; //state.active = true;
pump_info(&mut board, plant_id, true, pump_ineffective); pump_info(&mut board, plant_id, true, pump_ineffective, 0, 0, 0, false);
if !dry_run { let result = do_secure_pump(&mut board, plant_id, plant_config, dry_run)?;
board.board_hal.pump(plant_id, true)?;
Delay::new_default().delay_ms(1000 * plant_config.pump_time_s as u32);
board.board_hal.pump(plant_id, false)?; board.board_hal.pump(plant_id, false)?;
} pump_info(
pump_info(&mut board, plant_id, false, pump_ineffective); &mut board,
plant_id,
false,
pump_ineffective,
result.median_current_ma,
result.max_current_ma,
result.min_current_ma,
result.error,
);
} else if !state.pump_in_timeout(plant_config, &timezone_time) { } else if !state.pump_in_timeout(plant_config, &timezone_time) {
// plant does not need to be watered and is not in timeout // plant does not need to be watered and is not in timeout
// -> reset consecutive pump count // -> reset consecutive pump count
@ -556,6 +576,92 @@ fn safe_main() -> anyhow::Result<()> {
.deep_sleep(1000 * 1000 * 60 * deep_sleep_duration_minutes as u64); .deep_sleep(1000 * 1000 * 60 * deep_sleep_duration_minutes as u64);
} }
pub fn do_secure_pump(
board: &mut MutexGuard<HAL>,
plant_id: usize,
plant_config: &PlantConfig,
dry_run: bool,
) -> anyhow::Result<PumpResult> {
let mut current_collector = vec![0_u16; plant_config.pump_time_s.into()];
let mut error = false;
let mut first_error = true;
if !dry_run {
board.board_hal.pump(plant_id, true)?;
Delay::new_default().delay_ms(2);
for step in 0..plant_config.pump_time_s as usize {
let current = board.board_hal.pump_current(plant_id);
match current {
Ok(current) => {
let current_ma = current.as_milliamperes() as u16;
current_collector[step] = current_ma;
let high_current = current_ma > plant_config.max_pump_current_ma;
if high_current {
if first_error {
log(
LogMessage::PumpOverCurrent,
plant_id as u32 + 1,
current_ma as u32,
plant_config.max_pump_current_ma.to_string().as_str(),
step.to_string().as_str(),
);
board.board_hal.general_fault(true);
board.board_hal.fault(plant_id, true)?;
if !plant_config.ignore_current_error {
error = true;
break;
}
first_error = false;
}
}
let low_current = current_ma < plant_config.min_pump_current_ma;
if low_current {
if first_error {
log(
LogMessage::PumpOpenLoopCurrent,
plant_id as u32 + 1,
current_ma as u32,
plant_config.min_pump_current_ma.to_string().as_str(),
step.to_string().as_str(),
);
board.board_hal.general_fault(true);
board.board_hal.fault(plant_id, true)?;
if !plant_config.ignore_current_error {
error = true;
break;
}
first_error = false;
}
}
}
Err(err) => {
if !plant_config.ignore_current_error {
println!("Error getting pump current: {}", err);
log(
LogMessage::PumpMissingSensorCurrent,
plant_id as u32,
0,
"",
"",
);
error = true;
break;
} else {
//eg v3 without a sensor ends here, do not spam
}
}
}
Delay::new_default().delay_ms(1000);
}
}
current_collector.sort();
Ok(PumpResult {
median_current_ma: current_collector[current_collector.len() / 2],
max_current_ma: current_collector[current_collector.len() - 1],
min_current_ma: current_collector[0],
error,
})
}
fn update_charge_indicator(board: &mut MutexGuard<HAL>) { fn update_charge_indicator(board: &mut MutexGuard<HAL>) {
//we have mppt controller, ask it for charging current //we have mppt controller, ask it for charging current
if let Ok(current) = board.board_hal.get_mptt_current() { if let Ok(current) = board.board_hal.get_mptt_current() {
@ -711,10 +817,17 @@ fn pump_info(
plant_id: usize, plant_id: usize,
pump_active: bool, pump_active: bool,
pump_ineffective: bool, pump_ineffective: bool,
median_current_ma: u16,
max_current_ma: u16,
min_current_ma: u16,
error: bool,
) { ) {
let pump_info = PumpInfo { let pump_info = PumpInfo {
enabled: pump_active, enabled: pump_active,
pump_ineffective, pump_ineffective,
median_current_ma: median_current_ma,
max_current_ma: max_current_ma,
min_current_ma: min_current_ma,
}; };
let pump_topic = format!("/pump{}", plant_id + 1); let pump_topic = format!("/pump{}", plant_id + 1);
match serde_json::to_string(&pump_info) { match serde_json::to_string(&pump_info) {

View File

@ -2,7 +2,7 @@
use crate::{ use crate::{
config::PlantControllerConfig, config::PlantControllerConfig,
determine_tank_state, get_version, determine_tank_state, do_secure_pump, get_version,
hal::PLANT_COUNT, hal::PLANT_COUNT,
log::LogMessage, log::LogMessage,
plant_state::{MoistureSensorState, PlantState}, plant_state::{MoistureSensorState, PlantState},
@ -222,7 +222,7 @@ fn backup_info(
fn set_config( fn set_config(
request: &mut Request<&mut EspHttpConnection>, request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> { ) -> Result<Option<std::string::String>, anyhow::Error> {
let all = read_up_to_bytes_from_request(request, Some(3072))?; let all = read_up_to_bytes_from_request(request, Some(4096))?;
let config: PlantControllerConfig = serde_json::from_slice(&all)?; let config: PlantControllerConfig = serde_json::from_slice(&all)?;
let mut board = BOARD_ACCESS.lock().expect("board access"); let mut board = BOARD_ACCESS.lock().expect("board access");
@ -275,8 +275,11 @@ fn pump_test(
let actual_data = read_up_to_bytes_from_request(request, None)?; let actual_data = read_up_to_bytes_from_request(request, None)?;
let pump_test: TestPump = serde_json::from_slice(&actual_data)?; let pump_test: TestPump = serde_json::from_slice(&actual_data)?;
let mut board = BOARD_ACCESS.lock().unwrap(); let mut board = BOARD_ACCESS.lock().unwrap();
board.board_hal.test_pump(pump_test.pump)?;
anyhow::Ok(None) let config = &board.board_hal.get_config().plants[pump_test.pump].clone();
let pump_result = do_secure_pump(&mut board, pump_test.pump, config, false)?;
board.board_hal.pump(pump_test.pump, false)?;
anyhow::Ok(Some(serde_json::to_string(&pump_result)?))
} }
fn tank_info( fn tank_info(

View File

@ -1,4 +1,5 @@
export interface LogArray extends Array<LogEntry>{} export interface LogArray extends Array<LogEntry> {
}
export interface LogEntry { export interface LogEntry {
timestamp: string, timestamp: string,
@ -9,7 +10,8 @@ export interface LogEntry {
txt_long: string txt_long: string
} }
export interface LogLocalisation extends Array<LogLocalisationEntry>{} export interface LogLocalisation extends Array<LogLocalisationEntry> {
}
export interface LogLocalisationEntry { export interface LogLocalisationEntry {
msg_type: string, msg_type: string,
@ -38,13 +40,13 @@ export interface FileList {
iter_error: string, iter_error: string,
} }
export interface SolarState{ export interface SolarState {
mppt_voltage: number, mppt_voltage: number,
mppt_current: number, mppt_current: number,
is_day: boolean is_day: boolean
} }
export interface FileInfo{ export interface FileInfo {
filename: string, filename: string,
size: number, size: number,
} }
@ -77,7 +79,8 @@ export enum BatteryBoardVersion {
BQ34Z100G1 = "BQ34Z100G1", BQ34Z100G1 = "BQ34Z100G1",
WchI2cSlave = "WchI2cSlave" WchI2cSlave = "WchI2cSlave"
} }
export enum BoardVersion{
export enum BoardVersion {
INITIAL = "INITIAL", INITIAL = "INITIAL",
V3 = "V3", V3 = "V3",
V4 = "V4" V4 = "V4"
@ -110,9 +113,17 @@ export interface PlantConfig {
max_consecutive_pump_count: number, max_consecutive_pump_count: number,
moisture_sensor_min_frequency: number | null; moisture_sensor_min_frequency: number | null;
moisture_sensor_max_frequency: number | null; moisture_sensor_max_frequency: number | null;
min_pump_current_ma: number,
max_pump_current_ma: number,
ignore_current_error: boolean,
} }
export interface PumpTestResult {
median_current_ma: number,
max_current_ma: number,
min_current_ma: number,
error: boolean,
}
export interface SSIDList { export interface SSIDList {
ssids: [string] ssids: [string]

View File

@ -28,7 +28,7 @@ import {
SetTime, SSIDList, TankInfo, SetTime, SSIDList, TankInfo,
TestPump, TestPump,
VersionInfo, VersionInfo,
FileList, SolarState FileList, SolarState, PumpTestResult
} from "./api"; } from "./api";
import {SolarView} from "./solarview"; import {SolarView} from "./solarview";
@ -345,9 +345,10 @@ export class Controller {
method: "POST", method: "POST",
body: pretty body: pretty
}) })
.then(response => response.text()) .then(response => response.json() as Promise<PumpTestResult>)
.then( .then(
_ => { response => {
controller.plantViews.setPumpTestCurrent(plantId, response);
clearTimeout(timerId); clearTimeout(timerId);
controller.progressview.removeProgress("test_pump"); controller.progressview.removeProgress("test_pump");
} }

View File

@ -1,18 +1,21 @@
<style> <style>
.plantsensorkey{ .plantsensorkey {
min-width: 100px; min-width: 100px;
} }
.plantsensorvalue{
.plantsensorvalue {
flex-grow: 1; flex-grow: 1;
} }
.plantkey{ .plantkey {
min-width: 175px; min-width: 175px;
} }
.plantvalue{
.plantvalue {
flex-grow: 1; flex-grow: 1;
} }
.plantcheckbox{
.plantcheckbox {
min-width: 20px; min-width: 20px;
margin: 0; margin: 0;
} }
@ -47,7 +50,8 @@
<div class="flexcontainer"> <div class="flexcontainer">
<div class="plantkey">Pump Cooldown (m):</div> <div class="plantkey">Pump Cooldown (m):</div>
<input class="plantvalue" id="plant_${plantId}_pump_cooldown_min" type="number" min="0" max="600" placeholder="30"> <input class="plantvalue" id="plant_${plantId}_pump_cooldown_min" type="number" min="0" max="600"
placeholder="30">
</div> </div>
<div class="flexcontainer"> <div class="flexcontainer">
<div class="plantkey">"Pump Hour Start":</div> <div class="plantkey">"Pump Hour Start":</div>
@ -68,7 +72,7 @@
</div> </div>
<div class="flexcontainer"> <div class="flexcontainer">
<div class="plantkey">Max Frequency Override</div> <div class="plantkey">Max Frequency Override</div>
<input class="plantvalue" id="plant_${plantId}_max_frequency" type="number" min="1000" max="25000" > <input class="plantvalue" id="plant_${plantId}_max_frequency" type="number" min="1000" max="25000">
</div> </div>
<div class="flexcontainer"> <div class="flexcontainer">
@ -79,6 +83,22 @@
<div class="plantkey">Sensor B installed:</div> <div class="plantkey">Sensor B installed:</div>
<input class="plantcheckbox" id="plant_${plantId}_sensor_b" type="checkbox"> <input class="plantcheckbox" id="plant_${plantId}_sensor_b" type="checkbox">
</div> </div>
<div class="flexcontainer">
<h2 class="plantkey">Current config:</h2>
</div>
<div class="flexcontainer">
<div class="plantkey">Min current</div>
<input class="plantvalue" id="plant_${plantId}_min_pump_current_ma" type="number" min="0" max="4500">
</div>
<div class="flexcontainer">
<div class="plantkey">Max current</div>
<input class="plantvalue" id="plant_${plantId}_max_pump_current_ma" type="number" min="0" max="4500">
</div>
<div class="flexcontainer">
<div class="plantkey">Ignore current sensor error</div>
<input class="plantcheckbox" id="plant_${plantId}_ignore_current_error" type="checkbox">
</div>
<div class="flexcontainer"> <div class="flexcontainer">
<button class="subtitle" id="plant_${plantId}_test">Test Pump</button> <button class="subtitle" id="plant_${plantId}_test">Test Pump</button>
@ -89,11 +109,15 @@
</div> </div>
<div class="flexcontainer"> <div class="flexcontainer">
<span class="plantsensorkey">Sensor A:</span> <span class="plantsensorkey">Sensor A:</span>
<span class="plantsensorvalue" id="plant_${plantId}_moisture_a">loading</span> <span class="plantsensorvalue" id="plant_${plantId}_moisture_a">not measured</span>
</div> </div>
<div class="flexcontainer"> <div class="flexcontainer">
<div class="plantsensorkey">Sensor B:</div> <div class="plantsensorkey">Sensor B:</div>
<span class="plantsensorvalue" id="plant_${plantId}_moisture_b">loading</span> <span class="plantsensorvalue" id="plant_${plantId}_moisture_b">not measured</span>
</div>
<div class="flexcontainer">
<div class="plantsensorkey">Test Current</div>
<span class="plantsensorvalue" id="plant_${plantId}_pump_current_result">not_tested</span>
</div> </div>

View File

@ -1,4 +1,4 @@
import {PlantConfig} from "./api"; import {PlantConfig, PumpTestResult} from "./api";
const PLANT_COUNT = 8; const PLANT_COUNT = 8;
@ -10,7 +10,7 @@ export class PlantViews {
private readonly plants: PlantView[] = [] private readonly plants: PlantView[] = []
private readonly plantsDiv: HTMLDivElement private readonly plantsDiv: HTMLDivElement
constructor(syncConfig:Controller) { constructor(syncConfig: Controller) {
this.measure_moisture = document.getElementById("measure_moisture") as HTMLButtonElement this.measure_moisture = document.getElementById("measure_moisture") as HTMLButtonElement
this.measure_moisture.onclick = syncConfig.measure_moisture this.measure_moisture.onclick = syncConfig.measure_moisture
this.plantsDiv = document.getElementById("plants") as HTMLDivElement; this.plantsDiv = document.getElementById("plants") as HTMLDivElement;
@ -26,11 +26,12 @@ export class PlantViews {
} }
return rv return rv
} }
update(moisture_a: [string], moisture_b: [string]) { update(moisture_a: [string], moisture_b: [string]) {
for (let plantId = 0; plantId < PLANT_COUNT; plantId++) { for (let plantId = 0; plantId < PLANT_COUNT; plantId++) {
const a = moisture_a[plantId] const a = moisture_a[plantId]
const b = moisture_b[plantId] const b = moisture_b[plantId]
this.plants[plantId].update(a,b) this.plants[plantId].setMeasurementResult(a, b)
} }
} }
@ -41,7 +42,12 @@ export class PlantViews {
plantView.setConfig(plantConfig) plantView.setConfig(plantConfig)
} }
} }
setPumpTestCurrent(plantId: number, response: PumpTestResult) {
const plantView = this.plants[plantId];
plantView.setTestResult(response)
} }
}
export class PlantView { export class PlantView {
private readonly moistureSensorMinFrequency: HTMLInputElement; private readonly moistureSensorMinFrequency: HTMLInputElement;
@ -60,10 +66,14 @@ export class PlantView {
private readonly mode: HTMLSelectElement; private readonly mode: HTMLSelectElement;
private readonly moistureA: HTMLElement; private readonly moistureA: HTMLElement;
private readonly moistureB: HTMLElement; private readonly moistureB: HTMLElement;
private readonly pump_current_result: HTMLElement
private readonly maxConsecutivePumpCount: HTMLInputElement; private readonly maxConsecutivePumpCount: HTMLInputElement;
private readonly minPumpCurrentMa: HTMLInputElement;
private readonly maxPumpCurrentMa: HTMLInputElement;
private readonly ignoreCurrentError: HTMLInputElement;
constructor(plantId: number, parent:HTMLDivElement, controller:Controller) { constructor(plantId: number, parent: HTMLDivElement, controller: Controller) {
this.plantId = plantId; this.plantId = plantId;
this.plantDiv = document.createElement("div")! as HTMLDivElement this.plantDiv = document.createElement("div")! as HTMLDivElement
const template = require('./plant.html') as string; const template = require('./plant.html') as string;
@ -72,93 +82,115 @@ export class PlantView {
this.plantDiv.classList.add("plantcontainer") this.plantDiv.classList.add("plantcontainer")
parent.appendChild(this.plantDiv) parent.appendChild(this.plantDiv)
this.header = document.getElementById("plant_"+plantId+"_header")! this.header = document.getElementById("plant_" + plantId + "_header")!
this.header.innerText = "Plant "+ (this.plantId+1) this.header.innerText = "Plant " + (this.plantId + 1)
this.moistureA = document.getElementById("plant_"+plantId+"_moisture_a")! as HTMLElement; this.moistureA = document.getElementById("plant_" + plantId + "_moisture_a")! as HTMLElement;
this.moistureB = document.getElementById("plant_"+plantId+"_moisture_b")! as HTMLElement; this.moistureB = document.getElementById("plant_" + plantId + "_moisture_b")! as HTMLElement;
this.pump_current_result = document.getElementById("plant_" + plantId + "_pump_current_result")! as HTMLElement;
this.testButton = document.getElementById("plant_"+plantId+"_test")! as HTMLButtonElement;
this.testButton.onclick = function(){ this.testButton = document.getElementById("plant_" + plantId + "_test")! as HTMLButtonElement;
this.testButton.onclick = function () {
controller.testPlant(plantId) controller.testPlant(plantId)
} }
this.mode = document.getElementById("plant_"+plantId+"_mode") as HTMLSelectElement this.mode = document.getElementById("plant_" + plantId + "_mode") as HTMLSelectElement
this.mode.onchange = function(){ this.mode.onchange = function () {
controller.configChanged() controller.configChanged()
} }
this.targetMoisture = document.getElementById("plant_"+plantId+"_target_moisture")! as HTMLInputElement; this.targetMoisture = document.getElementById("plant_" + plantId + "_target_moisture")! as HTMLInputElement;
this.targetMoisture.onchange = function(){ this.targetMoisture.onchange = function () {
controller.configChanged() controller.configChanged()
} }
this.pumpTimeS = document.getElementById("plant_"+plantId+"_pump_time_s") as HTMLInputElement; this.pumpTimeS = document.getElementById("plant_" + plantId + "_pump_time_s") as HTMLInputElement;
this.pumpTimeS.onchange = function(){ this.pumpTimeS.onchange = function () {
controller.configChanged() controller.configChanged()
} }
this.pumpCooldown = document.getElementById("plant_"+plantId+"_pump_cooldown_min") as HTMLInputElement; this.pumpCooldown = document.getElementById("plant_" + plantId + "_pump_cooldown_min") as HTMLInputElement;
this.pumpCooldown.onchange = function(){ this.pumpCooldown.onchange = function () {
controller.configChanged() controller.configChanged()
} }
this.pumpHourStart = document.getElementById("plant_"+plantId+"_pump_hour_start") as HTMLSelectElement; this.pumpHourStart = document.getElementById("plant_" + plantId + "_pump_hour_start") as HTMLSelectElement;
this.pumpHourStart.onchange = function(){ this.pumpHourStart.onchange = function () {
controller.configChanged() controller.configChanged()
} }
for (let i = 0; i < 24; i++) { for (let i = 0; i < 24; i++) {
let option = document.createElement("option"); let option = document.createElement("option");
if (i == 10){ if (i == 10) {
option.selected = true option.selected = true
} }
option.innerText = i.toString(); option.innerText = i.toString();
this.pumpHourStart.appendChild(option); this.pumpHourStart.appendChild(option);
} }
this.pumpHourEnd = document.getElementById("plant_"+plantId+"_pump_hour_end") as HTMLSelectElement; this.pumpHourEnd = document.getElementById("plant_" + plantId + "_pump_hour_end") as HTMLSelectElement;
this.pumpHourEnd.onchange = function(){ this.pumpHourEnd.onchange = function () {
controller.configChanged() controller.configChanged()
} }
for (let i = 0; i < 24; i++) { for (let i = 0; i < 24; i++) {
let option = document.createElement("option"); let option = document.createElement("option");
if (i == 19){ if (i == 19) {
option.selected = true option.selected = true
} }
option.innerText = i.toString(); option.innerText = i.toString();
this.pumpHourEnd.appendChild(option); this.pumpHourEnd.appendChild(option);
} }
this.sensorAInstalled = document.getElementById("plant_"+plantId+"_sensor_a") as HTMLInputElement; this.sensorAInstalled = document.getElementById("plant_" + plantId + "_sensor_a") as HTMLInputElement;
this.sensorAInstalled.onchange = function(){ this.sensorAInstalled.onchange = function () {
controller.configChanged() controller.configChanged()
} }
this.sensorBInstalled = document.getElementById("plant_"+plantId+"_sensor_b") as HTMLInputElement; this.sensorBInstalled = document.getElementById("plant_" + plantId + "_sensor_b") as HTMLInputElement;
this.sensorBInstalled.onchange = function(){ this.sensorBInstalled.onchange = function () {
controller.configChanged() controller.configChanged()
} }
this.maxConsecutivePumpCount = document.getElementById("plant_"+plantId+"_max_consecutive_pump_count") as HTMLInputElement; this.minPumpCurrentMa = document.getElementById("plant_" + plantId + "_min_pump_current_ma") as HTMLInputElement;
this.maxConsecutivePumpCount.onchange = function(){ this.minPumpCurrentMa.onchange = function () {
controller.configChanged() controller.configChanged()
} }
this.moistureSensorMinFrequency = document.getElementById("plant_"+plantId+"_min_frequency") as HTMLInputElement; this.maxPumpCurrentMa = document.getElementById("plant_" + plantId + "_max_pump_current_ma") as HTMLInputElement;
this.moistureSensorMinFrequency.onchange = function(){ this.maxPumpCurrentMa.onchange = function () {
controller.configChanged()
}
this.ignoreCurrentError = document.getElementById("plant_" + plantId + "_ignore_current_error") as HTMLInputElement;
this.ignoreCurrentError.onchange = function () {
controller.configChanged()
}
this.maxConsecutivePumpCount = document.getElementById("plant_" + plantId + "_max_consecutive_pump_count") as HTMLInputElement;
this.maxConsecutivePumpCount.onchange = function () {
controller.configChanged()
}
this.moistureSensorMinFrequency = document.getElementById("plant_" + plantId + "_min_frequency") as HTMLInputElement;
this.moistureSensorMinFrequency.onchange = function () {
controller.configChanged() controller.configChanged()
} }
this.moistureSensorMinFrequency.onchange = () => { this.moistureSensorMinFrequency.onchange = () => {
controller.configChanged(); controller.configChanged();
}; };
this.moistureSensorMaxFrequency = document.getElementById("plant_"+plantId+"_max_frequency") as HTMLInputElement; this.moistureSensorMaxFrequency = document.getElementById("plant_" + plantId + "_max_frequency") as HTMLInputElement;
this.moistureSensorMaxFrequency.onchange = () => { this.moistureSensorMaxFrequency.onchange = () => {
controller.configChanged(); controller.configChanged();
}; };
} }
update(a: string, b: string) { setTestResult(result: PumpTestResult) {
this.pump_current_result.innerText = "Did abort " + result.error + " median current " + result.median_current_ma + " max current " + result.max_current_ma + " min current " + result.min_current_ma
}
setMeasurementResult(a: string, b: string) {
this.moistureA.innerText = a this.moistureA.innerText = a
this.moistureB.innerText = b this.moistureB.innerText = b
} }
@ -173,6 +205,9 @@ export class PlantView {
this.sensorBInstalled.checked = plantConfig.sensor_b; this.sensorBInstalled.checked = plantConfig.sensor_b;
this.sensorAInstalled.checked = plantConfig.sensor_a; this.sensorAInstalled.checked = plantConfig.sensor_a;
this.maxConsecutivePumpCount.value = plantConfig.max_consecutive_pump_count.toString(); this.maxConsecutivePumpCount.value = plantConfig.max_consecutive_pump_count.toString();
this.minPumpCurrentMa.value = plantConfig.min_pump_current_ma.toString();
this.maxPumpCurrentMa.value = plantConfig.max_pump_current_ma.toString();
this.ignoreCurrentError.checked = plantConfig.ignore_current_error;
// Set new fields // Set new fields
this.moistureSensorMinFrequency.value = this.moistureSensorMinFrequency.value =
@ -193,7 +228,10 @@ export class PlantView {
sensor_a: this.sensorAInstalled.checked, sensor_a: this.sensorAInstalled.checked,
max_consecutive_pump_count: this.maxConsecutivePumpCount.valueAsNumber, max_consecutive_pump_count: this.maxConsecutivePumpCount.valueAsNumber,
moisture_sensor_min_frequency: this.moistureSensorMinFrequency.valueAsNumber || null, moisture_sensor_min_frequency: this.moistureSensorMinFrequency.valueAsNumber || null,
moisture_sensor_max_frequency: this.moistureSensorMaxFrequency.valueAsNumber || null moisture_sensor_max_frequency: this.moistureSensorMaxFrequency.valueAsNumber || null,
min_pump_current_ma: this.minPumpCurrentMa.valueAsNumber,
max_pump_current_ma: this.maxPumpCurrentMa.valueAsNumber,
ignore_current_error: this.ignoreCurrentError.checked,
}; };
} }
} }