cleanups
This commit is contained in:
203
Software/MainBoard/rust/src_webpack/src/api.ts
Normal file
203
Software/MainBoard/rust/src_webpack/src/api.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
export interface LogArray extends Array<LogEntry> {
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: string,
|
||||
message_id: number,
|
||||
a: number,
|
||||
b: number,
|
||||
txt_short: string,
|
||||
txt_long: string
|
||||
}
|
||||
|
||||
export interface LogLocalisation extends Array<LogLocalisationEntry> {
|
||||
}
|
||||
|
||||
export interface LogLocalisationEntry {
|
||||
msg_type: string,
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface BackupHeader {
|
||||
timestamp: string,
|
||||
size: number
|
||||
}
|
||||
|
||||
export interface NetworkConfig {
|
||||
ap_ssid: string,
|
||||
ssid: string,
|
||||
password: string,
|
||||
mqtt_url: string,
|
||||
base_topic: string,
|
||||
mqtt_user: string | null,
|
||||
mqtt_password: string | null,
|
||||
max_wait: number
|
||||
}
|
||||
|
||||
export interface FileList {
|
||||
total: number,
|
||||
used: number,
|
||||
files: FileInfo[],
|
||||
file_system_corrupt: string,
|
||||
iter_error: string,
|
||||
}
|
||||
|
||||
export interface SolarState {
|
||||
mppt_voltage: number,
|
||||
mppt_current: number,
|
||||
is_day: boolean
|
||||
}
|
||||
|
||||
export interface FileInfo {
|
||||
filename: string,
|
||||
size: number,
|
||||
}
|
||||
|
||||
export interface NightLampConfig {
|
||||
enabled: boolean,
|
||||
night_lamp_hour_start: number,
|
||||
night_lamp_hour_end: number,
|
||||
night_lamp_only_when_dark: boolean,
|
||||
low_soc_cutoff: number,
|
||||
low_soc_restore: number
|
||||
}
|
||||
|
||||
export interface NightLampCommand {
|
||||
active: boolean
|
||||
}
|
||||
|
||||
export interface TankConfig {
|
||||
tank_sensor_enabled: boolean,
|
||||
tank_allow_pumping_if_sensor_error: boolean,
|
||||
tank_useable_ml: number,
|
||||
tank_warn_percent: number,
|
||||
tank_empty_percent: number,
|
||||
tank_full_percent: number,
|
||||
ml_per_pulse: number
|
||||
}
|
||||
|
||||
|
||||
export enum BatteryBoardVersion {
|
||||
Disabled = "Disabled",
|
||||
BQ34Z100G1 = "BQ34Z100G1",
|
||||
WchI2cSlave = "WchI2cSlave"
|
||||
}
|
||||
|
||||
export enum BoardVersion {
|
||||
INITIAL = "INITIAL",
|
||||
V3 = "V3",
|
||||
V4 = "V4"
|
||||
}
|
||||
|
||||
export interface BoardHardware {
|
||||
board: BoardVersion,
|
||||
battery: BatteryBoardVersion,
|
||||
}
|
||||
|
||||
export interface PlantControllerConfig {
|
||||
hardware: BoardHardware,
|
||||
|
||||
network: NetworkConfig,
|
||||
tank: TankConfig,
|
||||
night_lamp: NightLampConfig,
|
||||
plants: PlantConfig[]
|
||||
timezone?: string,
|
||||
}
|
||||
|
||||
export interface PlantConfig {
|
||||
mode: string,
|
||||
target_moisture: number,
|
||||
min_moisture: number,
|
||||
pump_time_s: number,
|
||||
pump_cooldown_min: number,
|
||||
pump_hour_start: number,
|
||||
pump_hour_end: number,
|
||||
sensor_a: boolean,
|
||||
sensor_b: boolean,
|
||||
max_consecutive_pump_count: number,
|
||||
moisture_sensor_min_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,
|
||||
flow_value_ml: number,
|
||||
flow_value_count: number,
|
||||
pump_time_s: number,
|
||||
error: boolean,
|
||||
}
|
||||
|
||||
export interface SSIDList {
|
||||
ssids: [string]
|
||||
}
|
||||
|
||||
export interface TestPump {
|
||||
pump: number
|
||||
}
|
||||
|
||||
export interface SetTime {
|
||||
time: string
|
||||
}
|
||||
|
||||
export interface GetTime {
|
||||
rtc: string,
|
||||
native: string
|
||||
}
|
||||
|
||||
export interface Moistures {
|
||||
moisture_a: [string],
|
||||
moisture_b: [string],
|
||||
}
|
||||
|
||||
export interface VersionInfo {
|
||||
git_hash: string,
|
||||
build_time: string,
|
||||
current: string,
|
||||
slot0_state: string,
|
||||
slot1_state: string,
|
||||
}
|
||||
|
||||
export interface BatteryState {
|
||||
temperature: string
|
||||
voltage_milli_volt: string,
|
||||
current_milli_ampere: string,
|
||||
cycle_count: string,
|
||||
design_milli_ampere: string,
|
||||
remaining_milli_ampere: string,
|
||||
state_of_charge: string,
|
||||
state_of_health: string
|
||||
}
|
||||
|
||||
export interface DetectionPlant {
|
||||
a: boolean,
|
||||
b: boolean
|
||||
}
|
||||
|
||||
export interface DetectionResult {
|
||||
plants: DetectionPlant[]
|
||||
}
|
||||
|
||||
export interface TankInfo {
|
||||
/// is there enough water in the tank
|
||||
enough_water: boolean,
|
||||
/// warning that water needs to be refilled soon
|
||||
warn_level: boolean,
|
||||
/// estimation how many ml are still in tank
|
||||
left_ml: number | null,
|
||||
/// if there is was an issue with the water level sensor
|
||||
sensor_error: string | null,
|
||||
/// raw water sensor value
|
||||
raw: number | null,
|
||||
/// percent value
|
||||
percent: number | null,
|
||||
/// water in tank might be frozen
|
||||
water_frozen: boolean,
|
||||
/// water temperature
|
||||
water_temp: number | null,
|
||||
temp_sensor_error: string | null
|
||||
}
|
||||
49
Software/MainBoard/rust/src_webpack/src/batteryview.html
Normal file
49
Software/MainBoard/rust/src_webpack/src/batteryview.html
Normal file
@@ -0,0 +1,49 @@
|
||||
<style>
|
||||
.powerflexkey {
|
||||
min-width: 150px;
|
||||
}
|
||||
.powerflexvalue {
|
||||
text-wrap: nowrap;
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="flexcontainer">
|
||||
<div class="subtitle">
|
||||
Battery:
|
||||
</div>
|
||||
<input id="battery_auto_refresh" type="checkbox">⟳
|
||||
</div>
|
||||
|
||||
<div class="flexcontainer">
|
||||
<span class="powerflexkey">V:</span>
|
||||
<span class="powerflexvalue" id="battery_voltage_milli_volt"></span>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<span class="powerflexkey">mA:</span>
|
||||
<span class="powerflexvalue" id="battery_current_milli_ampere" ></span>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<span class="powerflexkey">Cycles:</span>
|
||||
<span class="powerflexvalue" id="battery_cycle_count" ></span>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<span class="powerflexkey">design mA:</span>
|
||||
<span class="powerflexvalue" id="battery_design_milli_ampere" ></span>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<span class="powerflexkey">remaining mA:</span>
|
||||
<span class="powerflexvalue" id="battery_remaining_milli_ampere" ></span>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<span class="powerflexkey">charge %:</span>
|
||||
<span class="powerflexvalue" id="battery_state_of_charge" ></span>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<span class="powerflexkey">health %:</span>
|
||||
<span class="powerflexvalue" id="battery_state_of_health" ></span>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<span class="powerflexkey">Temp °C:</span>
|
||||
<span class="powerflexvalue" id="battery_temperature" ></span>
|
||||
</div>
|
||||
70
Software/MainBoard/rust/src_webpack/src/batteryview.ts
Normal file
70
Software/MainBoard/rust/src_webpack/src/batteryview.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Controller } from "./main";
|
||||
import {BatteryState} from "./api";
|
||||
|
||||
export class BatteryView{
|
||||
voltage_milli_volt: HTMLSpanElement;
|
||||
current_milli_ampere: HTMLSpanElement;
|
||||
cycle_count: HTMLSpanElement;
|
||||
design_milli_ampere: HTMLSpanElement;
|
||||
remaining_milli_ampere: HTMLSpanElement;
|
||||
state_of_charge: HTMLSpanElement;
|
||||
state_of_health: HTMLSpanElement;
|
||||
temperature: HTMLSpanElement;
|
||||
auto_refresh: HTMLInputElement;
|
||||
timer: NodeJS.Timeout | undefined;
|
||||
controller: Controller;
|
||||
|
||||
constructor (controller:Controller) {
|
||||
(document.getElementById("batteryview") as HTMLElement).innerHTML = require("./batteryview.html")
|
||||
this.voltage_milli_volt = document.getElementById("battery_voltage_milli_volt") as HTMLSpanElement;
|
||||
this.current_milli_ampere = document.getElementById("battery_current_milli_ampere") as HTMLSpanElement;
|
||||
this.cycle_count = document.getElementById("battery_cycle_count") as HTMLSpanElement;
|
||||
this.design_milli_ampere = document.getElementById("battery_design_milli_ampere") as HTMLSpanElement;
|
||||
this.remaining_milli_ampere = document.getElementById("battery_remaining_milli_ampere") as HTMLSpanElement;
|
||||
this.state_of_charge = document.getElementById("battery_state_of_charge") as HTMLSpanElement;
|
||||
this.state_of_health = document.getElementById("battery_state_of_health") as HTMLSpanElement;
|
||||
this.temperature = document.getElementById("battery_temperature") as HTMLSpanElement;
|
||||
this.auto_refresh = document.getElementById("battery_auto_refresh") as HTMLInputElement;
|
||||
|
||||
this.controller = controller
|
||||
this.auto_refresh.onchange = () => {
|
||||
if(this.timer){
|
||||
clearTimeout(this.timer)
|
||||
}
|
||||
if(this.auto_refresh.checked){
|
||||
controller.updateBatteryData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update(batterystate: BatteryState|null){
|
||||
if (batterystate == null) {
|
||||
this.voltage_milli_volt.innerText = "N/A"
|
||||
this.current_milli_ampere.innerText = "N/A"
|
||||
this.cycle_count.innerText = "N/A"
|
||||
this.design_milli_ampere.innerText = "N/A"
|
||||
this.remaining_milli_ampere.innerText = "N/A"
|
||||
this.state_of_charge.innerText = "N/A"
|
||||
this.state_of_health.innerText = "N/A"
|
||||
this.temperature.innerText = "N/A"
|
||||
} else {
|
||||
this.voltage_milli_volt.innerText = String(+batterystate.voltage_milli_volt/1000)
|
||||
this.current_milli_ampere.innerText = batterystate.current_milli_ampere
|
||||
this.cycle_count.innerText = batterystate.cycle_count
|
||||
this.design_milli_ampere.innerText = batterystate.design_milli_ampere
|
||||
this.remaining_milli_ampere.innerText = batterystate.remaining_milli_ampere
|
||||
this.state_of_charge.innerText = batterystate.state_of_charge
|
||||
this.state_of_health.innerText = batterystate.state_of_health
|
||||
this.temperature.innerText = String(+batterystate.temperature / 100)
|
||||
}
|
||||
|
||||
|
||||
if(this.auto_refresh.checked){
|
||||
this.timer = setTimeout(this.controller.updateBatteryData, 1000);
|
||||
} else {
|
||||
if(this.timer){
|
||||
clearTimeout(this.timer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
72
Software/MainBoard/rust/src_webpack/src/fileview.html
Normal file
72
Software/MainBoard/rust/src_webpack/src/fileview.html
Normal file
@@ -0,0 +1,72 @@
|
||||
<style>
|
||||
.filecheckbox {
|
||||
margin: 0px;
|
||||
min-width: 20px
|
||||
}
|
||||
|
||||
.filekey {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.filevalue {
|
||||
flex-grow: 1;
|
||||
width: 25%;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.filenumberbox {
|
||||
min-width: 50px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.filetitle {
|
||||
border-top-style: dotted;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.fileentryouter {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="subtitle">Files:</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="filekey">Total Size</div>
|
||||
<div id="filetotalsize" class="filevalue"></div>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="filekey">Used Size</div>
|
||||
<div id="fileusedsize" class="filevalue"></div>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="filekey">Free Size</div>
|
||||
<div id="filefreesize" class="filevalue"></div>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<div class="flexcontainer" style="border-left-style: double; border-right-style: double; border-top-style: double;">
|
||||
<div class="subtitle" >Upload:</div>
|
||||
</div>
|
||||
<div class="flexcontainer" style="border-left-style: double; border-right-style: double;">
|
||||
<div class="flexcontainer">
|
||||
<div class="filekey">
|
||||
File:
|
||||
</div>
|
||||
<input id="fileuploadfile" class="filevalue" type="file">
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="filekey">
|
||||
Name:
|
||||
</div>
|
||||
<input id="fileuploadname" class="filevalue" type="text">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flexcontainer" style="border-left-style: double; border-right-style: double; border-bottom-style: double;">
|
||||
<button id="fileuploadbtn" class="subtitle">Upload</button>
|
||||
</div>
|
||||
<br>
|
||||
<div class="flexcontainer" style="border-left-style: double; border-right-style: double; border-top-style: double;">
|
||||
<div class="subtitle">List:</div>
|
||||
</div>
|
||||
<div id="fileList" class="flexcontainer" style="border-left-style: double; border-right-style: double; border-bottom-style: double;">
|
||||
</div>
|
||||
96
Software/MainBoard/rust/src_webpack/src/fileview.ts
Normal file
96
Software/MainBoard/rust/src_webpack/src/fileview.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import {Controller} from "./main";
|
||||
import {FileInfo, FileList} from "./api";
|
||||
const regex = /[^a-zA-Z0-9_.]/g;
|
||||
|
||||
function sanitize(str:string){
|
||||
return str.replaceAll(regex, '_')
|
||||
}
|
||||
|
||||
export class FileView {
|
||||
readonly fileListView: HTMLElement;
|
||||
readonly controller: Controller;
|
||||
readonly filefreesize: HTMLElement;
|
||||
readonly filetotalsize: HTMLElement;
|
||||
readonly fileusedsize: HTMLElement;
|
||||
|
||||
constructor(controller: Controller) {
|
||||
(document.getElementById("fileview") as HTMLElement).innerHTML = require('./fileview.html') as string;
|
||||
this.fileListView = document.getElementById("fileList") as HTMLElement
|
||||
this.filefreesize = document.getElementById("filefreesize") as HTMLElement
|
||||
this.filetotalsize = document.getElementById("filetotalsize") as HTMLElement
|
||||
this.fileusedsize = document.getElementById("fileusedsize") as HTMLElement
|
||||
|
||||
let fileuploadfile = document.getElementById("fileuploadfile") as HTMLInputElement
|
||||
let fileuploadname = document.getElementById("fileuploadname") as HTMLInputElement
|
||||
let fileuploadbtn = document.getElementById("fileuploadbtn") as HTMLInputElement
|
||||
fileuploadfile.onchange = () => {
|
||||
const selectedFile = fileuploadfile.files?.[0];
|
||||
if (selectedFile == null) {
|
||||
//TODO error dialog here
|
||||
return
|
||||
}
|
||||
|
||||
fileuploadname.value = sanitize(selectedFile.name)
|
||||
};
|
||||
|
||||
fileuploadname.onchange = () => {
|
||||
let input = fileuploadname.value
|
||||
let clean = sanitize(fileuploadname.value)
|
||||
if (input != clean){
|
||||
fileuploadname.value = clean
|
||||
}
|
||||
}
|
||||
|
||||
fileuploadbtn.onclick = () => {
|
||||
const selectedFile = fileuploadfile.files?.[0];
|
||||
if (selectedFile == null) {
|
||||
//TODO error dialog here
|
||||
return
|
||||
}
|
||||
controller.uploadFile(selectedFile, selectedFile.name)
|
||||
}
|
||||
|
||||
|
||||
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
setFileList(fileList: FileList, public_url: string) {
|
||||
this.filetotalsize.innerText = Math.floor(fileList.total / 1024) + "kB"
|
||||
this.fileusedsize.innerText = Math.ceil(fileList.used / 1024) + "kB"
|
||||
this.filefreesize.innerText = Math.ceil((fileList.total - fileList.used) / 1024) + "kB"
|
||||
|
||||
//fast clear
|
||||
this.fileListView.textContent = ""
|
||||
for (let i = 0; i < fileList.files.length; i++) {
|
||||
let file = fileList.files[i]
|
||||
new FileEntry(this.controller, i, file, this.fileListView, public_url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FileEntry {
|
||||
view: HTMLElement;
|
||||
constructor(controller: Controller, fileid: number, fileinfo: FileInfo, parent: HTMLElement, public_url: string) {
|
||||
this.view = document.createElement("div") as HTMLElement
|
||||
parent.appendChild(this.view)
|
||||
this.view.classList.add("fileentryouter")
|
||||
|
||||
const template = require('./fileviewentry.html') as string;
|
||||
this.view.innerHTML = template.replaceAll("${fileid}", String(fileid))
|
||||
|
||||
let name = document.getElementById("file_" + fileid + "_name") as HTMLElement;
|
||||
let size = document.getElementById("file_" + fileid + "_size") as HTMLElement;
|
||||
let deleteBtn = document.getElementById("file_" + fileid + "_delete") as HTMLButtonElement;
|
||||
deleteBtn.onclick = () => {
|
||||
controller.deleteFile(fileinfo.filename);
|
||||
}
|
||||
|
||||
let downloadBtn = document.getElementById("file_" + fileid + "_download") as HTMLAnchorElement;
|
||||
downloadBtn.href = public_url + "/file?filename=" + fileinfo.filename
|
||||
downloadBtn.download = fileinfo.filename
|
||||
|
||||
name.innerText = fileinfo.filename;
|
||||
size.innerText = fileinfo.size.toString()
|
||||
}
|
||||
}
|
||||
11
Software/MainBoard/rust/src_webpack/src/fileviewentry.html
Normal file
11
Software/MainBoard/rust/src_webpack/src/fileviewentry.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<div class="flexcontainer">
|
||||
<div id="file_${fileid}_name" class="filetitle">Name</div>
|
||||
</div>
|
||||
|
||||
<div class="flexcontainer">
|
||||
<div class="filekey">Size</div>
|
||||
<div id = "file_${fileid}_size" class="filevalue"></div>
|
||||
<a id = "file_${fileid}_download" class="filevalue" target="_blank">Download</a>
|
||||
<button id = "file_${fileid}_delete" class="filevalue">Delete</button>
|
||||
</div>
|
||||
|
||||
20
Software/MainBoard/rust/src_webpack/src/hardware.html
Normal file
20
Software/MainBoard/rust/src_webpack/src/hardware.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<style>
|
||||
.boardkey{
|
||||
min-width: 200px;
|
||||
}
|
||||
.boardvalue{
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="subtitle">Hardware:</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="boardkey">BoardRevision</div>
|
||||
<select class="boardvalue" id="hardware_board_value">
|
||||
</select>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="boardkey">BatteryMonitor</div>
|
||||
<select class="boardvalue" id="hardware_battery_value">
|
||||
</select>
|
||||
</div>
|
||||
45
Software/MainBoard/rust/src_webpack/src/hardware.ts
Normal file
45
Software/MainBoard/rust/src_webpack/src/hardware.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Controller } from "./main";
|
||||
import {BatteryBoardVersion, BoardHardware, BoardVersion} from "./api";
|
||||
|
||||
export class HardwareConfigView {
|
||||
private readonly hardware_board_value: HTMLSelectElement;
|
||||
private readonly hardware_battery_value: HTMLSelectElement;
|
||||
constructor(controller:Controller){
|
||||
(document.getElementById("hardwareview") as HTMLElement).innerHTML = require('./hardware.html') as string;
|
||||
|
||||
this.hardware_board_value = document.getElementById("hardware_board_value") as HTMLSelectElement;
|
||||
this.hardware_board_value.onchange = controller.configChanged
|
||||
|
||||
Object.keys(BoardVersion).forEach(version => {
|
||||
let option = document.createElement("option");
|
||||
if (version == BoardVersion.INITIAL.toString()){
|
||||
option.selected = true
|
||||
}
|
||||
option.innerText = version.toString();
|
||||
this.hardware_board_value.appendChild(option);
|
||||
})
|
||||
|
||||
this.hardware_battery_value = document.getElementById("hardware_battery_value") as HTMLSelectElement;
|
||||
this.hardware_battery_value.onchange = controller.configChanged
|
||||
Object.keys(BatteryBoardVersion).forEach(version => {
|
||||
let option = document.createElement("option");
|
||||
if (version == BatteryBoardVersion.Disabled.toString()){
|
||||
option.selected = true
|
||||
}
|
||||
option.innerText = version.toString();
|
||||
this.hardware_battery_value.appendChild(option);
|
||||
})
|
||||
}
|
||||
|
||||
setConfig(hardware: BoardHardware) {
|
||||
this.hardware_board_value.value = hardware.board.toString()
|
||||
this.hardware_battery_value.value = hardware.battery.toString()
|
||||
}
|
||||
|
||||
getConfig(): BoardHardware {
|
||||
return {
|
||||
board : BoardVersion[this.hardware_board_value.value as keyof typeof BoardVersion],
|
||||
battery : BatteryBoardVersion[this.hardware_battery_value.value as keyof typeof BatteryBoardVersion],
|
||||
}
|
||||
}
|
||||
}
|
||||
7
Software/MainBoard/rust/src_webpack/src/log.html
Normal file
7
Software/MainBoard/rust/src_webpack/src/log.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<style>
|
||||
|
||||
</style>
|
||||
<button id="loadLog">Load Logs</button>
|
||||
<div id="logpanel">
|
||||
|
||||
</div>
|
||||
46
Software/MainBoard/rust/src_webpack/src/log.ts
Normal file
46
Software/MainBoard/rust/src_webpack/src/log.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Controller } from "./main";
|
||||
import {LogArray, LogLocalisation} from "./api";
|
||||
|
||||
export class LogView {
|
||||
private readonly logpanel: HTMLElement;
|
||||
private readonly loadLog: HTMLButtonElement;
|
||||
loglocale: LogLocalisation | undefined;
|
||||
|
||||
constructor(controller: Controller) {
|
||||
(document.getElementById("logview") as HTMLElement).innerHTML = require('./log.html') as string;
|
||||
this.logpanel = document.getElementById("logpanel") as HTMLElement
|
||||
this.loadLog = document.getElementById("loadLog") as HTMLButtonElement
|
||||
|
||||
this.loadLog.onclick = () => {
|
||||
controller.loadLog();
|
||||
}
|
||||
}
|
||||
|
||||
setLogLocalisation(loglocale: LogLocalisation) {
|
||||
this.loglocale = loglocale;
|
||||
}
|
||||
|
||||
setLog(logs: LogArray) {
|
||||
this.logpanel.textContent = ""
|
||||
logs.forEach(entry => {
|
||||
let message = this.loglocale!![entry.message_id];
|
||||
let template = message.message
|
||||
template = template.replace("${number_a}", entry.a.toString());
|
||||
template = template.replace("${number_b}", entry.b.toString());
|
||||
template = template.replace("${txt_short}", entry.txt_short.toString());
|
||||
template = template.replace("${txt_long}", entry.txt_long.toString());
|
||||
|
||||
let ts = new Date(entry.timestamp);
|
||||
|
||||
let div = document.createElement("div")
|
||||
let timestampDiv = document.createElement("div")
|
||||
let messageDiv = document.createElement("div")
|
||||
timestampDiv.innerText = ts.toISOString();
|
||||
messageDiv.innerText = template;
|
||||
div.appendChild(timestampDiv)
|
||||
div.appendChild(messageDiv)
|
||||
this.logpanel.appendChild(div)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
194
Software/MainBoard/rust/src_webpack/src/main.html
Normal file
194
Software/MainBoard/rust/src_webpack/src/main.html
Normal file
@@ -0,0 +1,194 @@
|
||||
<style>
|
||||
.progressPane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: grey;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 2.5em;
|
||||
width: 100%;
|
||||
background-color: #555;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progressSpacer{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.progress:after {
|
||||
content: attr(data-label);
|
||||
font-size: 0.8em;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 10px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.progress .value {
|
||||
background-color: darkcyan;
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.progress .valueIndeterminate {
|
||||
background-color: darkcyan;
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
animation: indeterminateAnimation 1s infinite linear;
|
||||
transform-origin: 0 50%;
|
||||
}
|
||||
|
||||
|
||||
@keyframes indeterminateAnimation {
|
||||
0% {
|
||||
transform: translateX(0%) scaleX(0.5);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(50%) scaleX(0.5);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(0%) scaleX(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.flexcontainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.flexcontainer-rev{
|
||||
display: flex;
|
||||
flex-wrap: wrap-reverse;
|
||||
}
|
||||
.subcontainer {
|
||||
min-width: 300px;
|
||||
max-width: 900px;
|
||||
flex-grow: 1;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
padding: 8px;
|
||||
}
|
||||
.subcontainercontainer{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.plantcontainer {
|
||||
flex-grow: 1;
|
||||
min-width: 100%;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
padding: 8px;
|
||||
}
|
||||
@media (min-width: 350px) {
|
||||
.plantcontainer {
|
||||
flex-grow: 1;
|
||||
min-width: 40%;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1100px) {
|
||||
.plantcontainer {
|
||||
flex-grow: 1;
|
||||
min-width: 20%;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 2150px) {
|
||||
.plantcontainer {
|
||||
flex-grow: 1;
|
||||
min-width: 200px;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.plantlist {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
<div class="container-xl">
|
||||
<div style="display:flex; flex-wrap: wrap;">
|
||||
<div id="hardwareview" class="subcontainer"></div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; flex-wrap: wrap;">
|
||||
<div id="firmwareview" class="subcontainer">
|
||||
</div>
|
||||
<div id="timeview" class="subcontainer">
|
||||
</div>
|
||||
<div id="batteryview" class="subcontainer">
|
||||
</div>
|
||||
<div id="solarview" class="subcontainer">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flexcontainer">
|
||||
<div id="network_view" class="subcontainercontainer"></div>
|
||||
<div id="lightview" class="subcontainer">
|
||||
</div>
|
||||
<div id="tankview" class="subcontainer">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Plants:</h3>
|
||||
<button id="measure_moisture">Measure Moisture</button>
|
||||
<button id="detect_sensors" style="display:none">Detect/Test Sensors</button>
|
||||
<div id="plants" class="plantlist"></div>
|
||||
|
||||
<div class="flexcontainer-rev">
|
||||
<div id = "submitview" class="subcontainer">
|
||||
</div>
|
||||
<div id="fileview" class="subcontainer">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<button id="exit">Exit</button>
|
||||
<button id="reboot">Reboot</button>
|
||||
|
||||
<div class="flexcontainer">
|
||||
<div id="logview" class="subcontainercontainer"></div>
|
||||
</div>
|
||||
|
||||
<script src="bundle.js"></script>
|
||||
</div>
|
||||
|
||||
<div id="progressPane" class="progressPane">
|
||||
<div class="progressSpacer"></div>>
|
||||
<div id="progressPaneBar" class="progress" data-label="50% Complete">
|
||||
<span id="progressPaneSpan" class="value" style="width:100%;"></span>
|
||||
</div>
|
||||
<div class="progressSpacer"></div>>
|
||||
</div>
|
||||
619
Software/MainBoard/rust/src_webpack/src/main.ts
Normal file
619
Software/MainBoard/rust/src_webpack/src/main.ts
Normal file
@@ -0,0 +1,619 @@
|
||||
import {deepEqual} from 'fast-equals';
|
||||
|
||||
declare var PUBLIC_URL: string;
|
||||
console.log("Url is " + PUBLIC_URL);
|
||||
|
||||
document.body.innerHTML = require('./main.html') as string;
|
||||
|
||||
|
||||
import {TimeView} from "./timeview";
|
||||
import {PlantViews} from "./plant";
|
||||
import {NetworkConfigView} from "./network";
|
||||
import {NightLampView} from "./nightlightview";
|
||||
import {TankConfigView} from "./tankview";
|
||||
import {SubmitView} from "./submitView";
|
||||
import {ProgressView} from "./progress";
|
||||
import {OTAView} from "./ota";
|
||||
import {BatteryView} from "./batteryview";
|
||||
import {FileView} from './fileview';
|
||||
import {LogView} from './log';
|
||||
import {HardwareConfigView} from "./hardware";
|
||||
import {
|
||||
BackupHeader,
|
||||
BatteryState,
|
||||
GetTime, LogArray, LogLocalisation,
|
||||
Moistures,
|
||||
NightLampCommand,
|
||||
PlantControllerConfig,
|
||||
SetTime, SSIDList, TankInfo,
|
||||
TestPump,
|
||||
VersionInfo,
|
||||
FileList, SolarState, PumpTestResult
|
||||
} from "./api";
|
||||
import {SolarView} from "./solarview";
|
||||
import {toast} from "./toast";
|
||||
|
||||
export class Controller {
|
||||
loadTankInfo(): Promise<void> {
|
||||
return fetch(PUBLIC_URL + "/tank")
|
||||
.then(response => response.json())
|
||||
.then(json => json as TankInfo)
|
||||
.then(tankinfo => {
|
||||
controller.tankView.setTankInfo(tankinfo)
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
loadLogLocaleConfig() {
|
||||
return fetch(PUBLIC_URL + "/log_localization")
|
||||
.then(response => response.json())
|
||||
.then(json => json as LogLocalisation)
|
||||
.then(loglocale => {
|
||||
controller.logView.setLogLocalisation(loglocale)
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
loadLog() {
|
||||
return fetch(PUBLIC_URL + "/log")
|
||||
.then(response => response.json())
|
||||
.then(json => json as LogArray)
|
||||
.then(logs => {
|
||||
controller.logView.setLog(logs)
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
async getBackupInfo(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(PUBLIC_URL + "/backup_info");
|
||||
const json = await response.json();
|
||||
const header = json as BackupHeader;
|
||||
controller.submitView.setBackupInfo(header);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
async populateTimezones(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(PUBLIC_URL + '/timezones');
|
||||
const json = await response.json();
|
||||
const timezones = json as string[];
|
||||
controller.timeView.timezones(timezones);
|
||||
} catch (error) {
|
||||
return console.error('Error fetching timezones:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async updateFileList(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(PUBLIC_URL + "/files");
|
||||
const json = await response.json();
|
||||
const filelist = json as FileList;
|
||||
controller.fileview.setFileList(filelist, PUBLIC_URL);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
uploadFile(file: File, name: string) {
|
||||
let current = 0;
|
||||
let max = 100;
|
||||
controller.progressview.addProgress("file_upload", (current / max) * 100, "Uploading File " + name + "(" + current + "/" + max + ")")
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.upload.addEventListener("progress", event => {
|
||||
current = event.loaded / 1000;
|
||||
max = event.total / 1000;
|
||||
controller.progressview.addProgress("file_upload", (current / max) * 100, "Uploading File " + name + "(" + current + "/" + max + ")")
|
||||
}, false);
|
||||
ajax.addEventListener("load", () => {
|
||||
controller.progressview.removeProgress("file_upload")
|
||||
controller.updateFileList()
|
||||
}, false);
|
||||
ajax.addEventListener("error", () => {
|
||||
alert("Error upload")
|
||||
controller.progressview.removeProgress("file_upload")
|
||||
controller.updateFileList()
|
||||
}, false);
|
||||
ajax.addEventListener("abort", () => {
|
||||
alert("abort upload")
|
||||
controller.progressview.removeProgress("file_upload")
|
||||
controller.updateFileList()
|
||||
}, false);
|
||||
ajax.open("POST", PUBLIC_URL + "/file?filename=" + name);
|
||||
ajax.send(file);
|
||||
}
|
||||
|
||||
deleteFile(name: string) {
|
||||
controller.progressview.addIndeterminate("file_delete", "Deleting " + name);
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.open("DELETE", PUBLIC_URL + "/file?filename=" + name);
|
||||
ajax.send();
|
||||
ajax.addEventListener("error", () => {
|
||||
controller.progressview.removeProgress("file_delete")
|
||||
alert("Error delete")
|
||||
controller.updateFileList()
|
||||
}, false);
|
||||
ajax.addEventListener("abort", () => {
|
||||
controller.progressview.removeProgress("file_delete")
|
||||
alert("Error upload")
|
||||
controller.updateFileList()
|
||||
}, false);
|
||||
ajax.addEventListener("load", () => {
|
||||
controller.progressview.removeProgress("file_delete")
|
||||
controller.updateFileList()
|
||||
}, false);
|
||||
controller.updateFileList()
|
||||
}
|
||||
|
||||
async updateRTCData(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(PUBLIC_URL + "/time");
|
||||
const json = await response.json();
|
||||
const time = json as GetTime;
|
||||
controller.timeView.update(time.native, time.rtc);
|
||||
} catch (error) {
|
||||
controller.timeView.update("n/a", "n/a");
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
async updateBatteryData(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(PUBLIC_URL + "/battery");
|
||||
const json = await response.json();
|
||||
const battery = json as BatteryState;
|
||||
controller.batteryView.update(battery);
|
||||
} catch (error) {
|
||||
controller.batteryView.update(null);
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
async updateSolarData(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(PUBLIC_URL + "/solar");
|
||||
const json = await response.json();
|
||||
const solar = json as SolarState;
|
||||
controller.solarView.update(solar);
|
||||
} catch (error) {
|
||||
controller.solarView.update(null);
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
uploadNewFirmware(file: File) {
|
||||
let current = 0;
|
||||
let max = 100;
|
||||
controller.progressview.addProgress("ota_upload", (current / max) * 100, "Uploading firmeware (" + current + "/" + max + ")")
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.upload.addEventListener("progress", event => {
|
||||
current = event.loaded / 1000;
|
||||
max = event.total / 1000;
|
||||
controller.progressview.addProgress("ota_upload", (current / max) * 100, "Uploading firmeware (" + current + "/" + max + ")")
|
||||
}, false);
|
||||
ajax.addEventListener("load", () => {
|
||||
controller.progressview.removeProgress("ota_upload")
|
||||
const status = ajax.status;
|
||||
if (status >= 200 && status < 300) {
|
||||
controller.reboot();
|
||||
} else {
|
||||
const statusText = ajax.statusText || "";
|
||||
const body = ajax.responseText || "";
|
||||
toast.error(`OTA update error (${status}${statusText ? ' ' + statusText : ''}): ${body}`);
|
||||
}
|
||||
}, false);
|
||||
ajax.addEventListener("error", () => {
|
||||
controller.progressview.removeProgress("ota_upload")
|
||||
toast.error("OTA upload failed due to a network error.");
|
||||
}, false);
|
||||
ajax.addEventListener("abort", () => {
|
||||
controller.progressview.removeProgress("ota_upload")
|
||||
toast.error("OTA upload was aborted.");
|
||||
}, false);
|
||||
ajax.open("POST", PUBLIC_URL + "/ota");
|
||||
ajax.send(file);
|
||||
}
|
||||
|
||||
async version(): Promise<void> {
|
||||
controller.progressview.addIndeterminate("version", "Getting buildVersion")
|
||||
const response = await fetch(PUBLIC_URL + "/version");
|
||||
const json = await response.json();
|
||||
const versionInfo = json as VersionInfo;
|
||||
controller.progressview.removeProgress("version");
|
||||
controller.firmWareView.setVersion(versionInfo);
|
||||
}
|
||||
|
||||
getBackupConfig() {
|
||||
controller.progressview.addIndeterminate("get_backup_config", "Downloading Backup")
|
||||
fetch(PUBLIC_URL + "/get_backup_config")
|
||||
.then(response => response.text())
|
||||
.then(loaded => {
|
||||
controller.progressview.removeProgress("get_backup_config")
|
||||
controller.submitView.setBackupJson(loaded);
|
||||
})
|
||||
}
|
||||
|
||||
async downloadConfig(): Promise<void> {
|
||||
controller.progressview.addIndeterminate("get_config", "Downloading Config")
|
||||
const response = await fetch(PUBLIC_URL + "/get_config");
|
||||
const loaded = await response.json();
|
||||
const currentConfig = loaded as PlantControllerConfig;
|
||||
controller.setInitialConfig(currentConfig);
|
||||
controller.setConfig(currentConfig);
|
||||
//sync json view initially
|
||||
controller.configChanged();
|
||||
controller.progressview.removeProgress("get_config");
|
||||
}
|
||||
|
||||
setInitialConfig(currentConfig: PlantControllerConfig) {
|
||||
this.initialConfig = currentConfig
|
||||
}
|
||||
|
||||
uploadConfig(json: string, statusCallback: (status: string) => void) {
|
||||
controller.progressview.addIndeterminate("set_config", "Uploading Config")
|
||||
fetch(PUBLIC_URL + "/set_config", {
|
||||
method: "POST",
|
||||
body: json,
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(text => statusCallback(text))
|
||||
.then( _ => {
|
||||
controller.progressview.removeProgress("set_config");
|
||||
setTimeout(() => { controller.downloadConfig() }, 250)
|
||||
})
|
||||
}
|
||||
|
||||
async backupConfig(json: string): Promise<string> {
|
||||
const response = await fetch(PUBLIC_URL + "/backup_config", {
|
||||
method: "POST",
|
||||
body: json,
|
||||
});
|
||||
return await response.text();
|
||||
}
|
||||
|
||||
syncRTCFromBrowser() {
|
||||
controller.progressview.addIndeterminate("write_rtc", "Writing RTC")
|
||||
const value: SetTime = {
|
||||
time: new Date().toISOString()
|
||||
};
|
||||
const pretty = JSON.stringify(value, undefined, 1);
|
||||
fetch(PUBLIC_URL + "/time", {
|
||||
method: "POST",
|
||||
body: pretty
|
||||
}).then(
|
||||
_ => controller.progressview.removeProgress("write_rtc")
|
||||
)
|
||||
}
|
||||
|
||||
configChanged() {
|
||||
const current = controller.getConfig();
|
||||
var pretty = JSON.stringify(current, undefined, 0);
|
||||
controller.submitView.setJson(pretty);
|
||||
|
||||
|
||||
if (deepEqual(current, controller.initialConfig)) {
|
||||
document.title = "PlantCtrl"
|
||||
} else {
|
||||
document.title = "*PlantCtrl"
|
||||
}
|
||||
}
|
||||
|
||||
selfTest() {
|
||||
fetch(PUBLIC_URL + "/boardtest", {
|
||||
method: "POST"
|
||||
})
|
||||
}
|
||||
|
||||
testNightLamp(active: boolean) {
|
||||
const body: NightLampCommand = {
|
||||
active: active
|
||||
};
|
||||
var pretty = JSON.stringify(body, undefined, 1);
|
||||
fetch(PUBLIC_URL + "/lamptest", {
|
||||
method: "POST",
|
||||
body: pretty
|
||||
})
|
||||
}
|
||||
|
||||
testPlant(plantId: number) {
|
||||
let counter = 0
|
||||
let limit = 30
|
||||
controller.progressview.addProgress("test_pump", counter / limit * 100, "Testing pump " + (plantId + 1) + " for " + (limit - counter) + "s")
|
||||
|
||||
let timerId: string | number | NodeJS.Timeout | undefined
|
||||
|
||||
function updateProgress() {
|
||||
counter++;
|
||||
controller.progressview.addProgress("test_pump", counter / limit * 100, "Testing pump " + (plantId + 1) + " for " + (limit - counter) + "s")
|
||||
timerId = setTimeout(updateProgress, 1000);
|
||||
|
||||
}
|
||||
|
||||
timerId = setTimeout(updateProgress, 1000);
|
||||
|
||||
var body: TestPump = {
|
||||
pump: plantId
|
||||
}
|
||||
var pretty = JSON.stringify(body, undefined, 1);
|
||||
|
||||
fetch(PUBLIC_URL + "/pumptest", {
|
||||
method: "POST",
|
||||
body: pretty
|
||||
})
|
||||
.then(response => response.json() as Promise<PumpTestResult>)
|
||||
.then(
|
||||
response => {
|
||||
controller.plantViews.setPumpTestCurrent(plantId, response);
|
||||
clearTimeout(timerId);
|
||||
controller.progressview.removeProgress("test_pump");
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async detectSensors() {
|
||||
let counter = 0
|
||||
let limit = 5
|
||||
controller.progressview.addProgress("detect_sensors", counter / limit * 100, "Detecting sensors " + (limit - counter) + "s")
|
||||
|
||||
let timerId: string | number | NodeJS.Timeout | undefined
|
||||
|
||||
function updateProgress() {
|
||||
counter++;
|
||||
controller.progressview.addProgress("detect_sensors", counter / limit * 100, "Detecting sensors " + (limit - counter) + "s")
|
||||
timerId = setTimeout(updateProgress, 1000);
|
||||
}
|
||||
|
||||
timerId = setTimeout(updateProgress, 1000);
|
||||
|
||||
fetch(PUBLIC_URL + "/detect_sensors", { method: "POST" })
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
clearTimeout(timerId);
|
||||
controller.progressview.removeProgress("detect_sensors");
|
||||
const pretty = JSON.stringify(json);
|
||||
toast.info("Detection result: " + pretty);
|
||||
})
|
||||
.catch(error => {
|
||||
clearTimeout(timerId);
|
||||
controller.progressview.removeProgress("detect_sensors");
|
||||
toast.error("Autodetect failed: " + error);
|
||||
});
|
||||
}
|
||||
|
||||
getConfig(): PlantControllerConfig {
|
||||
return {
|
||||
hardware: controller.hardwareView.getConfig(),
|
||||
network: controller.networkView.getConfig(),
|
||||
tank: controller.tankView.getConfig(),
|
||||
night_lamp: controller.nightLampView.getConfig(),
|
||||
plants: controller.plantViews.getConfig(),
|
||||
timezone: controller.timeView.getTimeZone()
|
||||
}
|
||||
}
|
||||
|
||||
scanWifi() {
|
||||
let counter = 0
|
||||
let limit = 5
|
||||
controller.progressview.addProgress("scan_ssid", counter / limit * 100, "Scanning for SSIDs for " + (limit - counter) + "s")
|
||||
|
||||
let timerId: string | number | NodeJS.Timeout | undefined
|
||||
|
||||
function updateProgress() {
|
||||
counter++;
|
||||
controller.progressview.addProgress("scan_ssid", counter / limit * 100, "Scanning for SSIDs for " + (limit - counter) + "s")
|
||||
timerId = setTimeout(updateProgress, 1000);
|
||||
|
||||
}
|
||||
|
||||
timerId = setTimeout(updateProgress, 1000);
|
||||
|
||||
|
||||
var ajax = new XMLHttpRequest();
|
||||
ajax.responseType = 'json';
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState === 4) {
|
||||
clearTimeout(timerId);
|
||||
controller.progressview.removeProgress("scan_ssid");
|
||||
this.networkView.setScanResult(ajax.response as SSIDList)
|
||||
}
|
||||
};
|
||||
ajax.onerror = (evt) => {
|
||||
clearTimeout(timerId);
|
||||
controller.progressview.removeProgress("scan_ssid");
|
||||
alert("Failed to start see console")
|
||||
}
|
||||
ajax.open("POST", PUBLIC_URL + "/wifiscan");
|
||||
ajax.send();
|
||||
}
|
||||
|
||||
setConfig(current: PlantControllerConfig) {
|
||||
// Show Detect/Test button only for V4 HAL
|
||||
if (current.hardware && (current.hardware as any).board === "V4") {
|
||||
this.detectBtn.style.display = "inline-block";
|
||||
} else {
|
||||
this.detectBtn.style.display = "none";
|
||||
}
|
||||
this.tankView.setConfig(current.tank);
|
||||
this.networkView.setConfig(current.network);
|
||||
this.nightLampView.setConfig(current.night_lamp);
|
||||
this.plantViews.setConfig(current.plants);
|
||||
this.timeView.setTimeZone(current.timezone);
|
||||
this.hardwareView.setConfig(current.hardware);
|
||||
}
|
||||
|
||||
measure_moisture() {
|
||||
let counter = 0
|
||||
let limit = 2
|
||||
controller.progressview.addProgress("measure_moisture", counter / limit * 100, "Measure Moisture " + (limit - counter) + "s")
|
||||
|
||||
let timerId: string | number | NodeJS.Timeout | undefined
|
||||
|
||||
function updateProgress() {
|
||||
counter++;
|
||||
controller.progressview.addProgress("measure_moisture", counter / limit * 100, "Measure Moisture " + (limit - counter) + "s")
|
||||
timerId = setTimeout(updateProgress, 1000);
|
||||
|
||||
}
|
||||
|
||||
timerId = setTimeout(updateProgress, 1000);
|
||||
|
||||
|
||||
fetch(PUBLIC_URL + "/moisture")
|
||||
.then(response => response.json())
|
||||
.then(json => json as Moistures)
|
||||
.then(time => {
|
||||
controller.plantViews.update(time.moisture_a, time.moisture_b)
|
||||
clearTimeout(timerId);
|
||||
controller.progressview.removeProgress("measure_moisture");
|
||||
})
|
||||
.catch(error => {
|
||||
clearTimeout(timerId);
|
||||
controller.progressview.removeProgress("measure_moisture");
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
exit() {
|
||||
fetch(PUBLIC_URL + "/exit", {
|
||||
method: "POST",
|
||||
})
|
||||
controller.progressview.addIndeterminate("rebooting", "Returned to normal mode, you can close this site now")
|
||||
|
||||
}
|
||||
|
||||
waitForReboot() {
|
||||
console.log("Check if controller online again")
|
||||
fetch(PUBLIC_URL + "/version", {
|
||||
method: "GET",
|
||||
signal: AbortSignal.timeout(5000)
|
||||
}).then(response => {
|
||||
if (response.status != 200) {
|
||||
console.log("Not reached yet, retrying")
|
||||
setTimeout(controller.waitForReboot, 1000)
|
||||
} else {
|
||||
console.log("Reached controller, reloading")
|
||||
controller.progressview.addIndeterminate("rebooting", "Reached Controller, reloading")
|
||||
setTimeout(function () {
|
||||
window.location.reload()
|
||||
}, 2000);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log("Not reached yet, retrying")
|
||||
setTimeout(controller.waitForReboot, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
reboot() {
|
||||
fetch(PUBLIC_URL + "/reboot", {
|
||||
method: "POST",
|
||||
})
|
||||
controller.progressview.addIndeterminate("rebooting", "Rebooting")
|
||||
setTimeout(this.waitForReboot, 1000)
|
||||
}
|
||||
|
||||
initialConfig: PlantControllerConfig | null = null
|
||||
readonly rebootBtn: HTMLButtonElement
|
||||
readonly exitBtn: HTMLButtonElement
|
||||
readonly timeView: TimeView;
|
||||
readonly plantViews: PlantViews;
|
||||
readonly networkView: NetworkConfigView;
|
||||
readonly hardwareView: HardwareConfigView;
|
||||
readonly tankView: TankConfigView;
|
||||
readonly nightLampView: NightLampView;
|
||||
readonly submitView: SubmitView;
|
||||
readonly firmWareView: OTAView;
|
||||
readonly progressview: ProgressView;
|
||||
readonly batteryView: BatteryView;
|
||||
readonly solarView: SolarView;
|
||||
readonly fileview: FileView;
|
||||
readonly logView: LogView
|
||||
readonly detectBtn: HTMLButtonElement
|
||||
|
||||
constructor() {
|
||||
this.timeView = new TimeView(this)
|
||||
this.plantViews = new PlantViews(this)
|
||||
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)
|
||||
this.progressview = new ProgressView(this)
|
||||
this.fileview = new FileView(this)
|
||||
this.logView = new LogView(this)
|
||||
this.hardwareView = new HardwareConfigView(this)
|
||||
this.detectBtn = document.getElementById("detect_sensors") as HTMLButtonElement
|
||||
this.detectBtn.onclick = () => { controller.detectSensors(); }
|
||||
this.rebootBtn = document.getElementById("reboot") as HTMLButtonElement
|
||||
this.rebootBtn.onclick = () => {
|
||||
controller.reboot();
|
||||
}
|
||||
this.exitBtn = document.getElementById("exit") as HTMLButtonElement
|
||||
this.exitBtn.onclick = () => {
|
||||
controller.exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"},
|
||||
{task: controller.getBackupInfo, displayString: "Fetching Backup Information"},
|
||||
{task: controller.loadLogLocaleConfig, displayString: "Loading Log Localization Config"},
|
||||
{task: controller.loadTankInfo, displayString: "Loading Tank Information"},
|
||||
];
|
||||
|
||||
async function executeTasksSequentially() {
|
||||
let current = 0;
|
||||
for (const {task, displayString} of tasks) {
|
||||
current++;
|
||||
let ratio = current / tasks.length;
|
||||
controller.progressview.addProgress("initial", ratio * 100, displayString);
|
||||
try {
|
||||
await task();
|
||||
} catch (error) {
|
||||
console.error(`Error executing task '${displayString}':`, error);
|
||||
// Optionally, you can decide whether to continue or break on errors
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
executeTasksSequentially().then(r => {
|
||||
controller.progressview.removeProgress("initial")
|
||||
});
|
||||
|
||||
controller.progressview.removeProgress("rebooting");
|
||||
|
||||
window.addEventListener("beforeunload", (event) => {
|
||||
const currentConfig = controller.getConfig();
|
||||
|
||||
// Check if the current state differs from the initial configuration
|
||||
if (!deepEqual(currentConfig, controller.initialConfig)) {
|
||||
const confirmationMessage = "You have unsaved changes. Are you sure you want to leave this page?";
|
||||
|
||||
// Standard behavior for displaying the confirmation dialog
|
||||
event.preventDefault();
|
||||
event.returnValue = confirmationMessage; // This will trigger the browser's default dialog
|
||||
return confirmationMessage;
|
||||
}
|
||||
});
|
||||
91
Software/MainBoard/rust/src_webpack/src/network.html
Normal file
91
Software/MainBoard/rust/src_webpack/src/network.html
Normal file
@@ -0,0 +1,91 @@
|
||||
<style>
|
||||
.basicnetworkkey{
|
||||
min-width: 200px;
|
||||
}
|
||||
.basicnetworkvalue{
|
||||
flex-grow: 1;
|
||||
}
|
||||
.basicnetworkkeyssid1{
|
||||
flex-grow: 1;
|
||||
}
|
||||
.basicnetworkkeyssid2{
|
||||
min-width: 50px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.mqttkey{
|
||||
min-width: 100px;
|
||||
}
|
||||
.mqttvalue{
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
<div>
|
||||
<div class="flexcontainer">
|
||||
<div class="subcontainer">
|
||||
<div class="subtitle">Basic network</div>
|
||||
<div class="flexcontainer">
|
||||
<span class="basicnetworkkey">Api Redirection to:</span>
|
||||
<span class="basicnetworkvalue" id="remote_ip">remote ip</span>
|
||||
</div>
|
||||
|
||||
<div class="flexcontainer">
|
||||
<label class="basicnetworkkey" for="ap_ssid">AP SSID:</label>
|
||||
<input class="basicnetworkvalue" type="text" id="ap_ssid" list="ssidlist">
|
||||
</div>
|
||||
|
||||
<div class="flexcontainer">
|
||||
<label class="basicnetworkkey" for="ssid">Station Mode:</label>
|
||||
<input class="basicnetworkkeyssid1" type="search" id="ssid" list="ssidlist">
|
||||
<datalist id="ssidlist">
|
||||
<option value="Not scanned yet">
|
||||
</datalist>
|
||||
<input class="basicnetworkkeyssid2" type="button" id="scan" value="Scan">
|
||||
</div>
|
||||
|
||||
<div class="flexcontainer">
|
||||
<label class="basicnetworkkey" for="max_wait">Max wait:</label>
|
||||
<input class="basicnetworkvalue" type="number" id="max_wait">
|
||||
</div>
|
||||
|
||||
<div class="flexcontainer">
|
||||
<label class="basicnetworkkey" for="ssid">Password:</label>
|
||||
<input class="basicnetworkvalue" type="text" id="password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="subcontainer">
|
||||
<div class="flexcontainer">
|
||||
<div class="subtitle">
|
||||
Mqtt Reporting
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flexcontainer">
|
||||
<div class="mqttkey">
|
||||
MQTT Url
|
||||
</div>
|
||||
<input class="mqttvalue" type="text" id="mqtt_url" placeholder="mqtt://192.168.1.1:1883">
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="mqttkey">
|
||||
Base Topic
|
||||
</div>
|
||||
<input class="mqttvalue" type="text" id="base_topic" placeholder="plants/one">
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="mqttkey">
|
||||
MQTT User
|
||||
</div>
|
||||
<input class="mqttvalue" type="text" id="mqtt_user" placeholder="">
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="mqttkey">
|
||||
MQTT Password
|
||||
</div>
|
||||
<input class="mqttvalue" type="text" id="mqtt_password" placeholder="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
78
Software/MainBoard/rust/src_webpack/src/network.ts
Normal file
78
Software/MainBoard/rust/src_webpack/src/network.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Controller } from "./main";
|
||||
import {NetworkConfig, SSIDList} from "./api";
|
||||
|
||||
export class NetworkConfigView {
|
||||
setScanResult(ssidList: SSIDList) {
|
||||
this.ssidlist.innerHTML = ''
|
||||
for (const ssid of ssidList.ssids) {
|
||||
const wi = document.createElement("option");
|
||||
wi.value = ssid;
|
||||
this.ssidlist.appendChild(wi);
|
||||
}
|
||||
}
|
||||
private readonly ap_ssid: HTMLInputElement;
|
||||
private readonly ssid: HTMLInputElement;
|
||||
private readonly password: HTMLInputElement;
|
||||
private readonly mqtt_url: HTMLInputElement;
|
||||
private readonly base_topic: HTMLInputElement;
|
||||
private readonly max_wait: HTMLInputElement;
|
||||
private readonly mqtt_user: HTMLInputElement;
|
||||
private readonly mqtt_password: HTMLInputElement;
|
||||
private readonly ssidlist: HTMLElement;
|
||||
|
||||
constructor(controller: Controller, publicIp: string) {
|
||||
(document.getElementById("network_view") as HTMLElement).innerHTML = require('./network.html') as string;
|
||||
|
||||
(document.getElementById("remote_ip") as HTMLElement).innerText = publicIp;
|
||||
|
||||
this.ap_ssid = (document.getElementById("ap_ssid") as HTMLInputElement);
|
||||
this.ap_ssid.onchange = controller.configChanged
|
||||
|
||||
this.ssid = (document.getElementById("ssid") as HTMLInputElement);
|
||||
this.ssid.onchange = controller.configChanged
|
||||
this.password = (document.getElementById("password") as HTMLInputElement);
|
||||
this.password.onchange = controller.configChanged
|
||||
this.max_wait = (document.getElementById("max_wait") as HTMLInputElement);
|
||||
this.max_wait.onchange = controller.configChanged
|
||||
|
||||
this.mqtt_url = document.getElementById("mqtt_url") as HTMLInputElement;
|
||||
this.mqtt_url.onchange = controller.configChanged
|
||||
this.base_topic = document.getElementById("base_topic") as HTMLInputElement;
|
||||
this.base_topic.onchange = controller.configChanged
|
||||
this.mqtt_user = document.getElementById("mqtt_user") as HTMLInputElement;
|
||||
this.mqtt_user.onchange = controller.configChanged
|
||||
this.mqtt_password = document.getElementById("mqtt_password") as HTMLInputElement;
|
||||
this.mqtt_password.onchange = controller.configChanged
|
||||
|
||||
this.ssidlist = document.getElementById("ssidlist") as HTMLElement
|
||||
|
||||
let scanWifiBtn = document.getElementById("scan") as HTMLButtonElement;
|
||||
scanWifiBtn.onclick = function (){
|
||||
controller.scanWifi();
|
||||
}
|
||||
}
|
||||
|
||||
setConfig(network: NetworkConfig) {
|
||||
this.ap_ssid.value = network.ap_ssid;
|
||||
this.ssid.value = network.ssid;
|
||||
this.password.value = network.password;
|
||||
this.mqtt_url.value = network.mqtt_url;
|
||||
this.base_topic.value = network.base_topic;
|
||||
this.mqtt_user.value = network.mqtt_user ?? "";
|
||||
this.mqtt_password.value = network.mqtt_password ?? "";
|
||||
this.max_wait.value = network.max_wait.toString();
|
||||
}
|
||||
|
||||
getConfig(): NetworkConfig {
|
||||
return {
|
||||
max_wait: +this.max_wait.value,
|
||||
ap_ssid: this.ap_ssid.value,
|
||||
ssid: this.ssid.value ?? null,
|
||||
password: this.password.value ?? null,
|
||||
mqtt_url: this.mqtt_url.value ?? null,
|
||||
mqtt_user: this.mqtt_user.value ? this.mqtt_user.value : null,
|
||||
mqtt_password: this.mqtt_password.value ? this.mqtt_password.value : null,
|
||||
base_topic: this.base_topic.value ?? null
|
||||
}
|
||||
}
|
||||
}
|
||||
48
Software/MainBoard/rust/src_webpack/src/nightlightview.html
Normal file
48
Software/MainBoard/rust/src_webpack/src/nightlightview.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<style>
|
||||
.lightcheckbox{
|
||||
margin: 0px;
|
||||
min-width: 20px
|
||||
}
|
||||
.lightkey{
|
||||
min-width: 200px;
|
||||
}
|
||||
.lightvalue{
|
||||
flex-grow: 1;
|
||||
}
|
||||
.lightnumberbox{
|
||||
min-width: 50px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="subtitle">Light:</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="lightkey">Test Nightlight</div>
|
||||
<input class="lightcheckbox" type="checkbox" id="night_lamp_test">
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="lightkey">Enable Nightlight</div>
|
||||
<input class="lightcheckbox" type="checkbox" id="night_lamp_enabled">
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="lightkey">Light only when dark</div>
|
||||
<input class="lightcheckbox" type="checkbox" id="night_lamp_only_when_dark">
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="lightkey">Start</div>
|
||||
<select class="lightnumberbox" type="time" id="night_lamp_time_start">
|
||||
</select>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="lightkey">Stop</div>
|
||||
<select class="lightnumberbox" type="time" id="night_lamp_time_end">
|
||||
</select>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="lightkey">Disable if Battery below %</div>
|
||||
<input class="lightcheckbox" type="number" id="night_lamp_soc_low" min="0" max="100">
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="lightkey">Reenable if Battery higher %</div>
|
||||
<input class="lightcheckbox" type="number" id="night_lamp_soc_restore" min="0" max="100">
|
||||
</div>
|
||||
76
Software/MainBoard/rust/src_webpack/src/nightlightview.ts
Normal file
76
Software/MainBoard/rust/src_webpack/src/nightlightview.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Controller } from "./main";
|
||||
import {NightLampConfig} from "./api";
|
||||
|
||||
export class NightLampView {
|
||||
private readonly night_lamp_only_when_dark: HTMLInputElement;
|
||||
private readonly night_lamp_time_start: HTMLSelectElement;
|
||||
private readonly night_lamp_time_end: HTMLSelectElement;
|
||||
private readonly night_lamp_test: HTMLInputElement;
|
||||
private readonly night_lamp_enabled: HTMLInputElement;
|
||||
private readonly night_lamp_soc_low: HTMLInputElement;
|
||||
private readonly night_lamp_soc_restore: HTMLInputElement;
|
||||
|
||||
constructor(controller:Controller){
|
||||
(document.getElementById("lightview") as HTMLElement).innerHTML = require('./nightlightview.html') as string;
|
||||
|
||||
this.night_lamp_only_when_dark = document.getElementById("night_lamp_only_when_dark") as HTMLInputElement;
|
||||
this.night_lamp_only_when_dark.onchange = controller.configChanged
|
||||
|
||||
this.night_lamp_enabled = document.getElementById("night_lamp_enabled") as HTMLInputElement;
|
||||
this.night_lamp_enabled.onchange = controller.configChanged
|
||||
|
||||
this.night_lamp_soc_low = document.getElementById("night_lamp_soc_low") as HTMLInputElement;
|
||||
this.night_lamp_soc_low.onchange = controller.configChanged
|
||||
|
||||
this.night_lamp_soc_restore = document.getElementById("night_lamp_soc_restore") as HTMLInputElement;
|
||||
this.night_lamp_soc_restore.onchange = controller.configChanged
|
||||
|
||||
this.night_lamp_time_start = document.getElementById("night_lamp_time_start") as HTMLSelectElement;
|
||||
this.night_lamp_time_start.onchange = controller.configChanged
|
||||
for (let i = 0; i < 24; i++) {
|
||||
let option = document.createElement("option");
|
||||
if (i == 20){
|
||||
option.selected = true
|
||||
}
|
||||
option.innerText = i.toString();
|
||||
this.night_lamp_time_start.appendChild(option);
|
||||
}
|
||||
this.night_lamp_time_end = document.getElementById("night_lamp_time_end") as HTMLSelectElement;
|
||||
this.night_lamp_time_end.onchange = controller.configChanged
|
||||
|
||||
for (let i = 0; i < 24; i++) {
|
||||
let option = document.createElement("option");
|
||||
if (i == 1){
|
||||
option.selected = true
|
||||
}
|
||||
option.innerText = i.toString();
|
||||
this.night_lamp_time_end.appendChild(option);
|
||||
}
|
||||
|
||||
let night_lamp_test = document.getElementById("night_lamp_test") as HTMLInputElement;
|
||||
this.night_lamp_test = night_lamp_test
|
||||
this.night_lamp_test.onchange = () => {
|
||||
controller.testNightLamp(night_lamp_test.checked)
|
||||
}
|
||||
}
|
||||
|
||||
setConfig(nightLamp: NightLampConfig) {
|
||||
this.night_lamp_only_when_dark.checked = nightLamp.night_lamp_only_when_dark
|
||||
this.night_lamp_time_start.value = nightLamp.night_lamp_hour_start.toString();
|
||||
this.night_lamp_time_end.value = nightLamp.night_lamp_hour_end.toString();
|
||||
this.night_lamp_enabled.checked = nightLamp.enabled;
|
||||
this.night_lamp_soc_low.value = nightLamp.low_soc_cutoff.toString();
|
||||
this.night_lamp_soc_restore.value = nightLamp.low_soc_restore.toString();
|
||||
}
|
||||
|
||||
getConfig(): NightLampConfig {
|
||||
return {
|
||||
night_lamp_hour_start: +this.night_lamp_time_start.value,
|
||||
night_lamp_hour_end: +this.night_lamp_time_end.value,
|
||||
night_lamp_only_when_dark: this.night_lamp_only_when_dark.checked,
|
||||
enabled: this.night_lamp_enabled.checked,
|
||||
low_soc_cutoff: +this.night_lamp_soc_low.value,
|
||||
low_soc_restore: +this.night_lamp_soc_restore.value
|
||||
}
|
||||
}
|
||||
}
|
||||
53
Software/MainBoard/rust/src_webpack/src/ota.html
Normal file
53
Software/MainBoard/rust/src_webpack/src/ota.html
Normal file
@@ -0,0 +1,53 @@
|
||||
<style>
|
||||
.otakey {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.otavalue {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.otaform {
|
||||
min-width: 100px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.otachooser {
|
||||
min-width: 100px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<div class="flexcontainer">
|
||||
<div class="subtitle">
|
||||
Current Firmware
|
||||
</div>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<span class="otakey">Buildtime:</span>
|
||||
<span class="otavalue" id="firmware_buildtime"></span>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<span class="otakey">Buildhash:</span>
|
||||
<span class="otavalue" id="firmware_githash"></span>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<span class="otakey">Partition:</span>
|
||||
<span class="otavalue" id="firmware_partition"></span>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<span class="otakey">State0:</span>
|
||||
<span class="otavalue" id="firmware_state0"></span>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<span class="otakey">State1:</span>
|
||||
<span class="otavalue" id="firmware_state1"></span>
|
||||
</div>
|
||||
|
||||
<div class="flexcontainer">
|
||||
<form class="otaform" id="upload_form" method="post">
|
||||
<input class="otachooser" type="file" name="file1" id="firmware_file"><br>
|
||||
</form>
|
||||
</div>
|
||||
<div class="display:flex">
|
||||
<button style="margin-left: 16px; margin-top: 8px;" class="col-6" type="button" id="test">Self-Test</button>
|
||||
</div>
|
||||
48
Software/MainBoard/rust/src_webpack/src/ota.ts
Normal file
48
Software/MainBoard/rust/src_webpack/src/ota.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import {Controller} from "./main";
|
||||
import {VersionInfo} from "./api";
|
||||
|
||||
export class OTAView {
|
||||
readonly file1Upload: HTMLInputElement;
|
||||
readonly firmware_buildtime: HTMLDivElement;
|
||||
readonly firmware_githash: HTMLDivElement;
|
||||
readonly firmware_partition: HTMLDivElement;
|
||||
readonly firmware_state0: HTMLDivElement;
|
||||
readonly firmware_state1: HTMLDivElement;
|
||||
|
||||
constructor(controller: Controller) {
|
||||
(document.getElementById("firmwareview") as HTMLElement).innerHTML = require("./ota.html")
|
||||
|
||||
let test = document.getElementById("test") as HTMLButtonElement;
|
||||
|
||||
this.firmware_buildtime = document.getElementById("firmware_buildtime") as HTMLDivElement;
|
||||
this.firmware_githash = document.getElementById("firmware_githash") as HTMLDivElement;
|
||||
this.firmware_partition = document.getElementById("firmware_partition") as HTMLDivElement;
|
||||
|
||||
this.firmware_state0 = document.getElementById("firmware_state0") as HTMLDivElement;
|
||||
this.firmware_state1 = document.getElementById("firmware_state1") as HTMLDivElement;
|
||||
|
||||
|
||||
const file = document.getElementById("firmware_file") as HTMLInputElement;
|
||||
this.file1Upload = file
|
||||
this.file1Upload.onchange = () => {
|
||||
const selectedFile = file.files?.[0];
|
||||
if (selectedFile == null) {
|
||||
//TODO error dialog here
|
||||
return
|
||||
}
|
||||
controller.uploadNewFirmware(selectedFile);
|
||||
};
|
||||
|
||||
test.onclick = () => {
|
||||
controller.selfTest();
|
||||
}
|
||||
}
|
||||
|
||||
setVersion(versionInfo: VersionInfo) {
|
||||
this.firmware_buildtime.innerText = versionInfo.build_time;
|
||||
this.firmware_githash.innerText = versionInfo.git_hash;
|
||||
this.firmware_partition.innerText = versionInfo.current;
|
||||
this.firmware_state0.innerText = versionInfo.slot0_state;
|
||||
this.firmware_state1.innerText = versionInfo.slot1_state;
|
||||
}
|
||||
}
|
||||
162
Software/MainBoard/rust/src_webpack/src/plant.html
Normal file
162
Software/MainBoard/rust/src_webpack/src/plant.html
Normal file
@@ -0,0 +1,162 @@
|
||||
<style>
|
||||
.plantsensorkey {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.plantsensorvalue {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.plantkey {
|
||||
min-width: 195px;
|
||||
}
|
||||
|
||||
.plantvalue {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.plantcheckbox {
|
||||
min-width: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.plantTargetEnabledOnly_ ${plantId} {
|
||||
}
|
||||
|
||||
.plantPumpEnabledOnly_ ${plantId} {
|
||||
}
|
||||
|
||||
.plantSensorEnabledOnly_ ${plantId} {
|
||||
}
|
||||
|
||||
.plantHidden_ ${plantId} {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<div>
|
||||
<div class="subtitle"
|
||||
id="plant_${plantId}_header">
|
||||
Plant ${plantId}
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="plantkey">Sensor A installed:</div>
|
||||
<input class="plantcheckbox" id="plant_${plantId}_sensor_a" type="checkbox">
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="plantkey">Sensor B installed:</div>
|
||||
<input class="plantcheckbox" id="plant_${plantId}_sensor_b" type="checkbox">
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="plantkey">
|
||||
Mode:
|
||||
</div>
|
||||
<select class="plantvalue" id="plant_${plantId}_mode">
|
||||
<option value="OFF">Off</option>
|
||||
<option value="TargetMoisture">Target</option>
|
||||
<option value="MinMoisture">Min Moisture</option>
|
||||
<option value="TimerOnly">Timer</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
<div class="flexcontainer plantTargetEnabledOnly_${plantId}">
|
||||
<div class="plantkey">Target Moisture:</div>
|
||||
<input class="plantvalue" id="plant_${plantId}_target_moisture" type="number" min="0" max="100" placeholder="0">
|
||||
</div>
|
||||
<div class="flexcontainer plantMinEnabledOnly_${plantId}">
|
||||
<div class="plantkey">Minimum Moisture:</div>
|
||||
<input class="plantvalue" id="plant_${plantId}_min_moisture" type="number" min="0" max="100" placeholder="0">
|
||||
</div>
|
||||
<div class="flexcontainer plantPumpEnabledOnly_${plantId}">
|
||||
<div class="plantkey">Pump Time (s):</div>
|
||||
<input class="plantvalue" id="plant_${plantId}_pump_time_s" type="number" min="0" max="600" placeholder="30">
|
||||
</div>
|
||||
|
||||
<div class="flexcontainer plantPumpEnabledOnly_${plantId}">
|
||||
<div class="plantkey">Pump Cooldown (m):</div>
|
||||
<input class="plantvalue" id="plant_${plantId}_pump_cooldown_min" type="number" min="0" max="600"
|
||||
placeholder="30">
|
||||
</div>
|
||||
<div class="flexcontainer plantPumpEnabledOnly_${plantId}">
|
||||
<div class="plantkey">"Pump Hour Start":</div>
|
||||
<select class="plantvalue" id="plant_${plantId}_pump_hour_start">10</select>
|
||||
</div>
|
||||
<div class="flexcontainer plantPumpEnabledOnly_${plantId}">
|
||||
<div class="plantkey">"Pump Hour End":</div>
|
||||
<select class="plantvalue" id="plant_${plantId}_pump_hour_end">19</select>
|
||||
</div>
|
||||
<div class="flexcontainer plantTargetEnabledOnly_${plantId}">
|
||||
<div class="plantkey">Warn Pump Count:</div>
|
||||
<input class="plantvalue" id="plant_${plantId}_max_consecutive_pump_count" type="number" min="1" max="50"
|
||||
placeholder="10">
|
||||
</div>
|
||||
<div class="flexcontainer plantSensorEnabledOnly_${plantId}">
|
||||
<div class="plantkey">Min Frequency Override</div>
|
||||
<input class="plantvalue" id="plant_${plantId}_min_frequency" type="number" min="1000" max="25000">
|
||||
</div>
|
||||
<div class="flexcontainer plantSensorEnabledOnly_${plantId}">
|
||||
<div class="plantkey">Max Frequency Override</div>
|
||||
<input class="plantvalue" id="plant_${plantId}_max_frequency" type="number" min="1000" max="25000">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flexcontainer plantPumpEnabledOnly_${plantId}">
|
||||
<h2 class="plantkey">Current config:</h2>
|
||||
</div>
|
||||
<div class="flexcontainer plantPumpEnabledOnly_${plantId}">
|
||||
<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 plantPumpEnabledOnly_${plantId}">
|
||||
<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 plantPumpEnabledOnly_${plantId}">
|
||||
<div class="plantkey">Ignore current sensor error</div>
|
||||
<input class="plantcheckbox" id="plant_${plantId}_ignore_current_error" type="checkbox">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flexcontainer plantPumpEnabledOnly_${plantId}">
|
||||
<button class="subtitle" id="plant_${plantId}_test">Test Pump</button>
|
||||
</div>
|
||||
|
||||
<div class="flexcontainer plantSensorEnabledOnly_${plantId}">
|
||||
<div class="subtitle">Live:</div>
|
||||
</div>
|
||||
<div class="flexcontainer plantSensorEnabledOnly_${plantId}">
|
||||
<span class="plantsensorkey">Sensor A:</span>
|
||||
<span class="plantsensorvalue" id="plant_${plantId}_moisture_a">not measured</span>
|
||||
</div>
|
||||
<div class="flexcontainer plantSensorEnabledOnly_${plantId}">
|
||||
<div class="plantsensorkey">Sensor B:</div>
|
||||
<span class="plantsensorvalue" id="plant_${plantId}_moisture_b">not measured</span>
|
||||
</div>
|
||||
<div class="flexcontainer plantPumpEnabledOnly_${plantId}">
|
||||
<div class="plantsensorkey">Max Current</div>
|
||||
<span class="plantsensorvalue" id="plant_${plantId}_pump_test_current_max">not_tested</span>
|
||||
</div>
|
||||
<div class="flexcontainer plantPumpEnabledOnly_${plantId}">
|
||||
<div class="plantsensorkey">Min Current</div>
|
||||
<span class="plantsensorvalue" id="plant_${plantId}_pump_test_current_min">not_tested</span>
|
||||
</div>
|
||||
<div class="flexcontainer plantPumpEnabledOnly_${plantId}">
|
||||
<div class="plantsensorkey">Average</div>
|
||||
<span class="plantsensorvalue" id="plant_${plantId}_pump_test_current_average">not_tested</span>
|
||||
</div>
|
||||
<div class="flexcontainer plantPumpEnabledOnly_${plantId}">
|
||||
<div class="plantsensorkey">Pump Time</div>
|
||||
<span class="plantsensorvalue" id="plant_${plantId}_pump_test_pump_time">not_tested</span>
|
||||
</div>
|
||||
<div class="flexcontainer plantPumpEnabledOnly_${plantId}">
|
||||
<div class="plantsensorkey">Flow ml</div>
|
||||
<span class="plantsensorvalue" id="plant_${plantId}_pump_test_flow_ml">not_tested</span>
|
||||
</div>
|
||||
<div class="flexcontainer plantPumpEnabledOnly_${plantId}">
|
||||
<div class="plantsensorkey">Flow raw</div>
|
||||
<span class="plantsensorvalue" id="plant_${plantId}_pump_test_flow_raw">not_tested</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
317
Software/MainBoard/rust/src_webpack/src/plant.ts
Normal file
317
Software/MainBoard/rust/src_webpack/src/plant.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
import {PlantConfig, PumpTestResult} from "./api";
|
||||
|
||||
const PLANT_COUNT = 8;
|
||||
|
||||
|
||||
import {Controller} from "./main";
|
||||
|
||||
export class PlantViews {
|
||||
private readonly measure_moisture: HTMLButtonElement;
|
||||
private readonly plants: PlantView[] = []
|
||||
private readonly plantsDiv: HTMLDivElement
|
||||
|
||||
constructor(syncConfig: Controller) {
|
||||
this.measure_moisture = document.getElementById("measure_moisture") as HTMLButtonElement
|
||||
this.measure_moisture.onclick = syncConfig.measure_moisture
|
||||
this.plantsDiv = document.getElementById("plants") as HTMLDivElement;
|
||||
for (let plantId = 0; plantId < PLANT_COUNT; plantId++) {
|
||||
this.plants[plantId] = new PlantView(plantId, this.plantsDiv, syncConfig);
|
||||
}
|
||||
}
|
||||
|
||||
getConfig(): PlantConfig[] {
|
||||
const rv: PlantConfig[] = [];
|
||||
for (let i = 0; i < PLANT_COUNT; i++) {
|
||||
rv[i] = this.plants[i].getConfig();
|
||||
}
|
||||
return rv
|
||||
}
|
||||
|
||||
update(moisture_a: [string], moisture_b: [string]) {
|
||||
for (let plantId = 0; plantId < PLANT_COUNT; plantId++) {
|
||||
const a = moisture_a[plantId]
|
||||
const b = moisture_b[plantId]
|
||||
this.plants[plantId].setMeasurementResult(a, b)
|
||||
}
|
||||
}
|
||||
|
||||
setConfig(plants: PlantConfig[]) {
|
||||
for (let plantId = 0; plantId < PLANT_COUNT; plantId++) {
|
||||
const plantConfig = plants[plantId];
|
||||
const plantView = this.plants[plantId];
|
||||
plantView.setConfig(plantConfig)
|
||||
}
|
||||
}
|
||||
|
||||
setPumpTestCurrent(plantId: number, response: PumpTestResult) {
|
||||
const plantView = this.plants[plantId];
|
||||
plantView.setTestResult(response)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class PlantView {
|
||||
private readonly moistureSensorMinFrequency: HTMLInputElement;
|
||||
private readonly moistureSensorMaxFrequency: HTMLInputElement;
|
||||
private readonly plantId: number;
|
||||
private readonly plantDiv: HTMLDivElement;
|
||||
private readonly header: HTMLElement;
|
||||
private readonly testButton: HTMLButtonElement;
|
||||
private readonly targetMoisture: HTMLInputElement;
|
||||
private readonly minMoisture: HTMLInputElement;
|
||||
private readonly pumpTimeS: HTMLInputElement;
|
||||
private readonly pumpCooldown: HTMLInputElement;
|
||||
private readonly pumpHourStart: HTMLSelectElement;
|
||||
private readonly pumpHourEnd: HTMLSelectElement;
|
||||
private readonly sensorAInstalled: HTMLInputElement;
|
||||
private readonly sensorBInstalled: HTMLInputElement;
|
||||
private readonly mode: HTMLSelectElement;
|
||||
private readonly moistureA: HTMLElement;
|
||||
private readonly moistureB: HTMLElement;
|
||||
private readonly maxConsecutivePumpCount: HTMLInputElement;
|
||||
private readonly minPumpCurrentMa: HTMLInputElement;
|
||||
private readonly maxPumpCurrentMa: HTMLInputElement;
|
||||
private readonly ignoreCurrentError: HTMLInputElement;
|
||||
|
||||
private readonly pump_test_current_max: HTMLElement;
|
||||
private readonly pump_test_current_min: HTMLElement;
|
||||
private readonly pump_test_current_average: HTMLElement;
|
||||
private readonly pump_test_pump_time: HTMLElement;
|
||||
private readonly pump_test_flow_ml: HTMLElement;
|
||||
private readonly pump_test_flow_raw: HTMLElement;
|
||||
|
||||
|
||||
constructor(plantId: number, parent: HTMLDivElement, controller: Controller) {
|
||||
this.plantId = plantId;
|
||||
this.plantDiv = document.createElement("div")! as HTMLDivElement
|
||||
const template = require('./plant.html') as string;
|
||||
this.plantDiv.innerHTML = template.replaceAll("${plantId}", String(plantId))
|
||||
|
||||
this.plantDiv.classList.add("plantcontainer")
|
||||
parent.appendChild(this.plantDiv)
|
||||
|
||||
this.header = document.getElementById("plant_" + plantId + "_header")!
|
||||
this.header.innerText = "Plant " + (this.plantId + 1)
|
||||
|
||||
this.moistureA = document.getElementById("plant_" + plantId + "_moisture_a")! as HTMLElement;
|
||||
this.moistureB = document.getElementById("plant_" + plantId + "_moisture_b")! as HTMLElement;
|
||||
|
||||
this.pump_test_current_max = document.getElementById("plant_" + plantId + "_pump_test_current_max")! as HTMLElement;
|
||||
this.pump_test_current_min = document.getElementById("plant_" + plantId + "_pump_test_current_min")! as HTMLElement;
|
||||
this.pump_test_current_average = document.getElementById("plant_" + plantId + "_pump_test_current_average")! as HTMLElement;
|
||||
this.pump_test_pump_time = document.getElementById("plant_" + plantId + "_pump_test_pump_time")! as HTMLElement;
|
||||
this.pump_test_flow_ml = document.getElementById("plant_" + plantId + "_pump_test_flow_ml")! as HTMLElement;
|
||||
this.pump_test_flow_raw = document.getElementById("plant_" + plantId + "_pump_test_flow_raw")! as HTMLElement;
|
||||
|
||||
this.testButton = document.getElementById("plant_" + plantId + "_test")! as HTMLButtonElement;
|
||||
this.testButton.onclick = function () {
|
||||
controller.testPlant(plantId)
|
||||
}
|
||||
|
||||
this.mode = document.getElementById("plant_" + plantId + "_mode") as HTMLSelectElement
|
||||
this.mode.onchange = function () {
|
||||
controller.configChanged()
|
||||
}
|
||||
|
||||
this.targetMoisture = document.getElementById("plant_" + plantId + "_target_moisture")! as HTMLInputElement;
|
||||
this.targetMoisture.onchange = function () {
|
||||
controller.configChanged()
|
||||
}
|
||||
|
||||
this.minMoisture = document.getElementById("plant_" + plantId + "_min_moisture")! as HTMLInputElement;
|
||||
this.minMoisture.onchange = function () {
|
||||
controller.configChanged()
|
||||
}
|
||||
|
||||
this.pumpTimeS = document.getElementById("plant_" + plantId + "_pump_time_s") as HTMLInputElement;
|
||||
this.pumpTimeS.onchange = function () {
|
||||
controller.configChanged()
|
||||
}
|
||||
|
||||
this.pumpCooldown = document.getElementById("plant_" + plantId + "_pump_cooldown_min") as HTMLInputElement;
|
||||
this.pumpCooldown.onchange = function () {
|
||||
controller.configChanged()
|
||||
}
|
||||
|
||||
this.pumpHourStart = document.getElementById("plant_" + plantId + "_pump_hour_start") as HTMLSelectElement;
|
||||
this.pumpHourStart.onchange = function () {
|
||||
controller.configChanged()
|
||||
}
|
||||
for (let i = 0; i < 24; i++) {
|
||||
let option = document.createElement("option");
|
||||
if (i == 10) {
|
||||
option.selected = true
|
||||
}
|
||||
option.innerText = i.toString();
|
||||
this.pumpHourStart.appendChild(option);
|
||||
}
|
||||
|
||||
this.pumpHourEnd = document.getElementById("plant_" + plantId + "_pump_hour_end") as HTMLSelectElement;
|
||||
this.pumpHourEnd.onchange = function () {
|
||||
controller.configChanged()
|
||||
}
|
||||
for (let i = 0; i < 24; i++) {
|
||||
let option = document.createElement("option");
|
||||
if (i == 19) {
|
||||
option.selected = true
|
||||
}
|
||||
option.innerText = i.toString();
|
||||
this.pumpHourEnd.appendChild(option);
|
||||
}
|
||||
|
||||
this.sensorAInstalled = document.getElementById("plant_" + plantId + "_sensor_a") as HTMLInputElement;
|
||||
this.sensorAInstalled.onchange = function () {
|
||||
controller.configChanged()
|
||||
}
|
||||
|
||||
this.sensorBInstalled = document.getElementById("plant_" + plantId + "_sensor_b") as HTMLInputElement;
|
||||
this.sensorBInstalled.onchange = function () {
|
||||
controller.configChanged()
|
||||
}
|
||||
|
||||
this.minPumpCurrentMa = document.getElementById("plant_" + plantId + "_min_pump_current_ma") as HTMLInputElement;
|
||||
this.minPumpCurrentMa.onchange = function () {
|
||||
controller.configChanged()
|
||||
}
|
||||
|
||||
this.maxPumpCurrentMa = document.getElementById("plant_" + plantId + "_max_pump_current_ma") as HTMLInputElement;
|
||||
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()
|
||||
}
|
||||
this.moistureSensorMinFrequency.onchange = () => {
|
||||
controller.configChanged();
|
||||
};
|
||||
|
||||
this.moistureSensorMaxFrequency = document.getElementById("plant_" + plantId + "_max_frequency") as HTMLInputElement;
|
||||
this.moistureSensorMaxFrequency.onchange = () => {
|
||||
controller.configChanged();
|
||||
};
|
||||
}
|
||||
|
||||
updateVisibility(plantConfig: PlantConfig) {
|
||||
let sensorOnly = document.getElementsByClassName("plantSensorEnabledOnly_"+ this.plantId)
|
||||
let pumpOnly = document.getElementsByClassName("plantPumpEnabledOnly_"+ this.plantId)
|
||||
let targetOnly = document.getElementsByClassName("plantTargetEnabledOnly_"+ this.plantId)
|
||||
let minOnly = document.getElementsByClassName("plantMinEnabledOnly_"+ this.plantId)
|
||||
|
||||
console.log("updateVisibility plantConfig: " + plantConfig.mode)
|
||||
let showSensor = plantConfig.sensor_a || plantConfig.sensor_b
|
||||
let showPump = plantConfig.mode !== "OFF"
|
||||
let showTarget = plantConfig.mode === "TargetMoisture"
|
||||
let showMin = plantConfig.mode === "MinMoisture"
|
||||
|
||||
console.log("updateVisibility showsensor: " + showSensor + " pump " + showPump + " target " +showTarget + " min " + showMin)
|
||||
|
||||
for (const element of Array.from(sensorOnly)) {
|
||||
if (showSensor) {
|
||||
element.classList.remove("plantHidden_" + this.plantId)
|
||||
} else {
|
||||
element.classList.add("plantHidden_" + this.plantId)
|
||||
}
|
||||
}
|
||||
|
||||
for (const element of Array.from(pumpOnly)) {
|
||||
if (showPump) {
|
||||
element.classList.remove("plantHidden_" + this.plantId)
|
||||
} else {
|
||||
element.classList.add("plantHidden_" + this.plantId)
|
||||
}
|
||||
}
|
||||
|
||||
for (const element of Array.from(targetOnly)) {
|
||||
if (showTarget) {
|
||||
element.classList.remove("plantHidden_" + this.plantId)
|
||||
} else {
|
||||
element.classList.add("plantHidden_" + this.plantId)
|
||||
}
|
||||
}
|
||||
|
||||
for (const element of Array.from(minOnly)) {
|
||||
if (showMin) {
|
||||
element.classList.remove("plantHidden_" + this.plantId)
|
||||
} else {
|
||||
element.classList.add("plantHidden_" + this.plantId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setTestResult(result: PumpTestResult) {
|
||||
this.pump_test_current_max.innerText = result.max_current_ma.toString()
|
||||
this.pump_test_current_min.innerText = result.min_current_ma.toString()
|
||||
this.pump_test_current_average.innerText = result.median_current_ma.toString()
|
||||
|
||||
this.pump_test_flow_raw.innerText = result.flow_value_count.toString()
|
||||
this.pump_test_flow_ml.innerText = result.flow_value_ml.toString()
|
||||
|
||||
this.pump_test_pump_time.innerText = result.pump_time_s.toString()
|
||||
}
|
||||
|
||||
setMeasurementResult(a: string, b: string) {
|
||||
this.moistureA.innerText = a
|
||||
this.moistureB.innerText = b
|
||||
}
|
||||
|
||||
setConfig(plantConfig: PlantConfig) {
|
||||
this.mode.value = plantConfig.mode;
|
||||
this.targetMoisture.value = plantConfig.target_moisture.toString();
|
||||
this.minMoisture.value = plantConfig.min_moisture?.toString() || "";
|
||||
this.pumpTimeS.value = plantConfig.pump_time_s.toString();
|
||||
this.pumpCooldown.value = plantConfig.pump_cooldown_min.toString();
|
||||
this.pumpHourStart.value = plantConfig.pump_hour_start.toString();
|
||||
this.pumpHourEnd.value = plantConfig.pump_hour_end.toString();
|
||||
this.sensorBInstalled.checked = plantConfig.sensor_b;
|
||||
this.sensorAInstalled.checked = plantConfig.sensor_a;
|
||||
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
|
||||
this.moistureSensorMinFrequency.value =
|
||||
plantConfig.moisture_sensor_min_frequency?.toString() || "";
|
||||
this.moistureSensorMaxFrequency.value =
|
||||
plantConfig.moisture_sensor_max_frequency?.toString() || "";
|
||||
|
||||
this.updateVisibility(plantConfig);
|
||||
}
|
||||
|
||||
getConfig(): PlantConfig {
|
||||
|
||||
let conv: PlantConfig = {
|
||||
mode: this.mode.value,
|
||||
target_moisture: this.targetMoisture.valueAsNumber,
|
||||
min_moisture: this.minMoisture.valueAsNumber,
|
||||
pump_time_s: this.pumpTimeS.valueAsNumber,
|
||||
pump_cooldown_min: this.pumpCooldown.valueAsNumber,
|
||||
pump_hour_start: +this.pumpHourStart.value,
|
||||
pump_hour_end: +this.pumpHourEnd.value,
|
||||
sensor_b: this.sensorBInstalled.checked,
|
||||
sensor_a: this.sensorAInstalled.checked,
|
||||
max_consecutive_pump_count: this.maxConsecutivePumpCount.valueAsNumber,
|
||||
moisture_sensor_min_frequency: this.moistureSensorMinFrequency.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,
|
||||
};
|
||||
this.updateVisibility(conv);
|
||||
return conv;
|
||||
}
|
||||
}
|
||||
62
Software/MainBoard/rust/src_webpack/src/progress.ts
Normal file
62
Software/MainBoard/rust/src_webpack/src/progress.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Controller } from "./main";
|
||||
|
||||
class ProgressInfo{
|
||||
displayText:string;
|
||||
percentValue:number;
|
||||
indeterminate:boolean;
|
||||
constructor(displayText:string, percentValue: number, indeterminate:boolean ){
|
||||
this.displayText = displayText
|
||||
this.percentValue = percentValue <0 ? 0 : percentValue > 100? 100: percentValue
|
||||
this.indeterminate = indeterminate
|
||||
}
|
||||
}
|
||||
|
||||
export class ProgressView{
|
||||
progressPane: HTMLElement;
|
||||
progress: HTMLElement;
|
||||
progressPaneSpan: HTMLSpanElement;
|
||||
progresses: Map<string,ProgressInfo> = new Map;
|
||||
progressPaneBar: HTMLDivElement;
|
||||
constructor(controller:Controller){
|
||||
this.progressPane = document.getElementById("progressPane") as HTMLElement;
|
||||
this.progress = document.getElementById("progress") as HTMLElement;
|
||||
this.progressPaneSpan = document.getElementById("progressPaneSpan") as HTMLSpanElement;
|
||||
this.progressPaneBar = document.getElementById("progressPaneBar") as HTMLDivElement;
|
||||
|
||||
}
|
||||
|
||||
updateView() {
|
||||
if (this.progresses.size == 0){
|
||||
this.progressPane.style.display = "none"
|
||||
} else{
|
||||
const first = this.progresses.entries().next().value![1]
|
||||
this.progressPaneBar.setAttribute("data-label", first.displayText)
|
||||
if (first.indeterminate){
|
||||
this.progressPaneSpan.className = "valueIndeterminate"
|
||||
this.progressPaneSpan.style.width = "100%"
|
||||
|
||||
} else {
|
||||
this.progressPaneSpan.className = "value"
|
||||
this.progressPaneSpan.style.width = first.percentValue+"%"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addIndeterminate(id:string, displayText:string){
|
||||
this.progresses.set(id, new ProgressInfo(displayText,0,true))
|
||||
this.progressPane.style.display = "flex"
|
||||
this.updateView();
|
||||
|
||||
}
|
||||
|
||||
addProgress(id:string, value:number, displayText:string) {
|
||||
this.progresses.set(id, new ProgressInfo(displayText,value, false))
|
||||
this.progressPane.style.display = "flex"
|
||||
this.updateView();
|
||||
}
|
||||
removeProgress(id:string){
|
||||
this.progresses.delete(id)
|
||||
this.updateView();
|
||||
|
||||
}
|
||||
}
|
||||
29
Software/MainBoard/rust/src_webpack/src/solarview.html
Normal file
29
Software/MainBoard/rust/src_webpack/src/solarview.html
Normal 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>
|
||||
49
Software/MainBoard/rust/src_webpack/src/solarview.ts
Normal file
49
Software/MainBoard/rust/src_webpack/src/solarview.ts
Normal 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(0)
|
||||
this.solar_current_milli_ampere.innerText = solarState.mppt_current.toFixed(0)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
74
Software/MainBoard/rust/src_webpack/src/submitView.ts
Normal file
74
Software/MainBoard/rust/src_webpack/src/submitView.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import {Controller} from "./main";
|
||||
import {BackupHeader} from "./api";
|
||||
|
||||
export class SubmitView {
|
||||
json: HTMLDivElement;
|
||||
submitFormBtn: HTMLButtonElement;
|
||||
submit_status: HTMLElement;
|
||||
backupBtn: HTMLButtonElement;
|
||||
restoreBackupBtn: HTMLButtonElement;
|
||||
backuptimestamp: HTMLElement;
|
||||
backupsize: HTMLElement;
|
||||
backupjson: HTMLElement;
|
||||
|
||||
constructor(controller: Controller) {
|
||||
(document.getElementById("submitview") as HTMLElement).innerHTML = require("./submitview.html")
|
||||
|
||||
let showJson = document.getElementById('showJson') as HTMLButtonElement
|
||||
let rawdata = document.getElementById('rawdata') as HTMLElement
|
||||
this.json = document.getElementById('json') as HTMLDivElement
|
||||
this.backupjson = document.getElementById('backupjson') as HTMLDivElement
|
||||
this.submitFormBtn = document.getElementById("submit") as HTMLButtonElement
|
||||
this.backupBtn = document.getElementById("backup") as HTMLButtonElement
|
||||
this.restoreBackupBtn = document.getElementById("restorebackup") as HTMLButtonElement
|
||||
this.backuptimestamp = document.getElementById("backuptimestamp") as HTMLElement
|
||||
this.backupsize = document.getElementById("backupsize") as HTMLElement
|
||||
this.submit_status = document.getElementById("submit_status") as HTMLElement
|
||||
this.submitFormBtn.onclick = () => {
|
||||
controller.uploadConfig(this.json.textContent as string, (status: string) => {
|
||||
if (status != "OK") {
|
||||
// Show error toast (click to dismiss only)
|
||||
const { toast } = require('./toast');
|
||||
toast.error(status);
|
||||
} else {
|
||||
// Show info toast (auto hides after 5s, or click to dismiss sooner)
|
||||
const { toast } = require('./toast');
|
||||
toast.info('Config uploaded successfully');
|
||||
}
|
||||
this.submit_status.innerHTML = status;
|
||||
});
|
||||
}
|
||||
this.backupBtn.onclick = () => {
|
||||
controller.progressview.addIndeterminate("backup", "Backup to EEPROM running")
|
||||
controller.backupConfig(this.json.textContent as string).then(saveStatus => {
|
||||
controller.getBackupInfo().then(r => {
|
||||
controller.progressview.removeProgress("backup")
|
||||
this.submit_status.innerHTML = saveStatus;
|
||||
});
|
||||
});
|
||||
}
|
||||
this.restoreBackupBtn.onclick = () => {
|
||||
controller.getBackupConfig();
|
||||
}
|
||||
showJson.onclick = () => {
|
||||
if (rawdata.style.display == "none") {
|
||||
rawdata.style.display = "flex";
|
||||
} else {
|
||||
rawdata.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setBackupInfo(header: BackupHeader) {
|
||||
this.backuptimestamp.innerText = header.timestamp
|
||||
this.backupsize.innerText = header.size.toString()
|
||||
}
|
||||
|
||||
setJson(pretty: string) {
|
||||
this.json.textContent = pretty
|
||||
}
|
||||
|
||||
setBackupJson(pretty: string) {
|
||||
this.backupjson.textContent = pretty
|
||||
}
|
||||
}
|
||||
40
Software/MainBoard/rust/src_webpack/src/submitview.html
Normal file
40
Software/MainBoard/rust/src_webpack/src/submitview.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<style>
|
||||
.submitarea{
|
||||
flex-grow: 1;
|
||||
border-style: groove;
|
||||
border-width: 1px;
|
||||
overflow-wrap: break-word;
|
||||
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>
|
||||
<div>BackupStatus:</div>
|
||||
<div id="backuptimestamp"></div>
|
||||
<div id="backupsize"></div>
|
||||
<button id="backup">Backup</button>
|
||||
<button id="restorebackup">Restore</button>
|
||||
<div id="submit_status"></div>
|
||||
89
Software/MainBoard/rust/src_webpack/src/tankview.html
Normal file
89
Software/MainBoard/rust/src_webpack/src/tankview.html
Normal file
@@ -0,0 +1,89 @@
|
||||
<style>
|
||||
.tankcheckbox {
|
||||
min-width: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
.tankkey{
|
||||
min-width: 250px;
|
||||
}
|
||||
.tankvalue{
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<div class="flexcontainer">
|
||||
<span style="flex-grow: 1; text-align: center; font-weight: bold;">
|
||||
Tank:
|
||||
</span>
|
||||
<input id="tankview_auto_refresh" type="checkbox">⟳
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<span class="tankkey">Enable Tank Sensor</span>
|
||||
<input class="tankcheckbox" type="checkbox" id="tank_sensor_enabled">
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="tankkey">Ignore Sensor Error</div>
|
||||
<input class="tankcheckbox" type="checkbox" id="tank_allow_pumping_if_sensor_error">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flexcontainer">
|
||||
<div class="tankkey">Useable ml full% to empty%</div>
|
||||
<input class="tankvalue" type="number" min="2" max="500000" id="tank_useable_ml">
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="tankkey">Warn below %</div>
|
||||
<input class="tankvalue" type="number" min="1" max="500000" id="tank_warn_percent">
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="tankkey">Empty at %</div>
|
||||
<input class="tankvalue" type="number" min="0" max="100" id="tank_empty_percent">
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="tankkey">Full at %</div>
|
||||
<input class="tankvalue" type="number" min="0" max="100" id="tank_full_percent">
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="tankkey">Flow Sensor ml per pulse</div>
|
||||
<input class="tankvalue" type="number" min="0" max="1000" step="0.01" id="ml_per_pulse">
|
||||
</div>
|
||||
<button id="tank_update">Update Tank</button>
|
||||
|
||||
|
||||
<div id="tank_measure_error_container" class="flexcontainer hidden">
|
||||
<div class="tankkey">Sensor Error</div>
|
||||
<label class="tankvalue" id="tank_measure_error"></label>
|
||||
</div>
|
||||
<div id="tank_measure_ml_container" class="flexcontainer">
|
||||
<div class="tankkey">Left ml</div>
|
||||
<label class="tankvalue" id="tank_measure_ml"></label>
|
||||
</div>
|
||||
<div id="tank_measure_percent_container" class="flexcontainer">
|
||||
<div class="tankkey">Current %</div>
|
||||
<label class="tankvalue" id="tank_measure_percent"></label>
|
||||
</div>
|
||||
<div id="tank_measure_temperature_container" class="flexcontainer">
|
||||
<div class="tankkey">Temperature °C</div>
|
||||
<label class="tankvalue" id="tank_measure_temperature"></label>
|
||||
</div>
|
||||
<div id="tank_measure_rawvolt_container" class="flexcontainer">
|
||||
<div class="tankkey">Probe Voltage</div>
|
||||
<label class="tankvalue" id="tank_measure_rawvolt"></label>
|
||||
</div>
|
||||
<div id="tank_measure_temperature_error_container" class="flexcontainer">
|
||||
<div class="tankkey">Temperature Error</div>
|
||||
<label class="tankvalue" id="tank_measure_temperature_error"></label>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="tankkey">Enough Water</div>
|
||||
<label class="tankvalue" id="tank_measure_enoughwater"></label>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="tankkey">Warn Level</div>
|
||||
<label class="tankvalue" id="tank_measure_warnlevel"></label>
|
||||
</div>
|
||||
160
Software/MainBoard/rust/src_webpack/src/tankview.ts
Normal file
160
Software/MainBoard/rust/src_webpack/src/tankview.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { Controller } from "./main";
|
||||
import {TankConfig, TankInfo} from "./api";
|
||||
|
||||
export class TankConfigView {
|
||||
private readonly tank_useable_ml: HTMLInputElement;
|
||||
private readonly tank_empty_percent: HTMLInputElement;
|
||||
private readonly tank_full_percent: HTMLInputElement;
|
||||
private readonly tank_warn_percent: HTMLInputElement;
|
||||
private readonly tank_sensor_enabled: HTMLInputElement;
|
||||
private readonly tank_allow_pumping_if_sensor_error: HTMLInputElement;
|
||||
private readonly ml_per_pulse: HTMLInputElement;
|
||||
private readonly tank_measure_error: HTMLLabelElement;
|
||||
private readonly tank_measure_ml: HTMLLabelElement;
|
||||
private readonly tank_measure_percent: HTMLLabelElement;
|
||||
private readonly tank_measure_temperature: HTMLLabelElement;
|
||||
private readonly tank_measure_rawvolt: HTMLLabelElement;
|
||||
private readonly tank_measure_enoughwater: HTMLLabelElement;
|
||||
private readonly tank_measure_warnlevel: HTMLLabelElement;
|
||||
private readonly tank_measure_temperature_error: HTMLLabelElement;
|
||||
private readonly tank_measure_error_container: HTMLDivElement;
|
||||
private readonly tank_measure_ml_container: HTMLDivElement;
|
||||
private readonly tank_measure_percent_container: HTMLDivElement;
|
||||
private readonly tank_measure_temperature_container: HTMLDivElement;
|
||||
private readonly tank_measure_rawvolt_container: HTMLDivElement;
|
||||
private readonly tank_measure_temperature_error_container: HTMLDivElement;
|
||||
|
||||
private readonly auto_refresh: HTMLInputElement;
|
||||
private timer: NodeJS.Timeout | undefined;
|
||||
private readonly controller: Controller;
|
||||
|
||||
constructor(controller:Controller){
|
||||
(document.getElementById("tankview") as HTMLElement).innerHTML = require("./tankview.html")
|
||||
this.controller = controller;
|
||||
|
||||
this.auto_refresh = document.getElementById("tankview_auto_refresh") as HTMLInputElement;
|
||||
|
||||
this.auto_refresh.onchange = () => {
|
||||
if(this.timer){
|
||||
clearTimeout(this.timer)
|
||||
}
|
||||
if(this.auto_refresh.checked){
|
||||
controller.loadTankInfo()
|
||||
}
|
||||
}
|
||||
|
||||
this.tank_useable_ml = document.getElementById("tank_useable_ml") as HTMLInputElement;
|
||||
this.tank_useable_ml.onchange = controller.configChanged
|
||||
this.tank_empty_percent = document.getElementById("tank_empty_percent") as HTMLInputElement;
|
||||
this.tank_empty_percent.onchange = controller.configChanged
|
||||
this.tank_full_percent = document.getElementById("tank_full_percent") as HTMLInputElement;
|
||||
this.tank_full_percent.onchange = controller.configChanged
|
||||
this.tank_warn_percent = document.getElementById("tank_warn_percent") as HTMLInputElement;
|
||||
this.tank_warn_percent.onchange = controller.configChanged
|
||||
this.tank_sensor_enabled = document.getElementById("tank_sensor_enabled") as HTMLInputElement;
|
||||
this.tank_sensor_enabled.onchange = controller.configChanged
|
||||
this.tank_allow_pumping_if_sensor_error = document.getElementById("tank_allow_pumping_if_sensor_error") as HTMLInputElement;
|
||||
this.tank_allow_pumping_if_sensor_error.onchange = controller.configChanged
|
||||
this.ml_per_pulse = document.getElementById("ml_per_pulse") as HTMLInputElement;
|
||||
this.ml_per_pulse.onchange = controller.configChanged
|
||||
|
||||
let tank_update = document.getElementById("tank_update") as HTMLInputElement;
|
||||
tank_update.onclick = () => {
|
||||
controller.loadTankInfo()
|
||||
}
|
||||
|
||||
|
||||
this.tank_measure_error = document.getElementById("tank_measure_error") as HTMLLabelElement;
|
||||
this.tank_measure_error_container = document.getElementById("tank_measure_error_container") as HTMLDivElement;
|
||||
|
||||
this.tank_measure_ml = document.getElementById("tank_measure_ml") as HTMLLabelElement;
|
||||
this.tank_measure_ml_container = document.getElementById("tank_measure_ml_container") as HTMLDivElement;
|
||||
|
||||
this.tank_measure_percent = document.getElementById("tank_measure_percent") as HTMLLabelElement;
|
||||
this.tank_measure_percent_container = document.getElementById("tank_measure_percent_container") as HTMLDivElement;
|
||||
|
||||
this.tank_measure_temperature = document.getElementById("tank_measure_temperature") as HTMLLabelElement;
|
||||
this.tank_measure_temperature_container = document.getElementById("tank_measure_temperature_container") as HTMLDivElement;
|
||||
|
||||
this.tank_measure_rawvolt = document.getElementById("tank_measure_rawvolt") as HTMLLabelElement;
|
||||
this.tank_measure_rawvolt_container = document.getElementById("tank_measure_rawvolt_container") as HTMLDivElement;
|
||||
|
||||
this.tank_measure_temperature_error = document.getElementById("tank_measure_temperature_error") as HTMLLabelElement;
|
||||
this.tank_measure_temperature_error_container = document.getElementById("tank_measure_temperature_error_container") as HTMLDivElement;
|
||||
|
||||
this.tank_measure_enoughwater = document.getElementById("tank_measure_enoughwater") as HTMLLabelElement;
|
||||
this.tank_measure_warnlevel = document.getElementById("tank_measure_warnlevel") as HTMLLabelElement;
|
||||
}
|
||||
|
||||
setTankInfo(tankinfo: TankInfo) {
|
||||
if (tankinfo.sensor_error == null){
|
||||
this.tank_measure_error_container.classList.add("hidden")
|
||||
} else {
|
||||
this.tank_measure_error.innerText = JSON.stringify(tankinfo.sensor_error) ;
|
||||
this.tank_measure_error_container.classList.remove("hidden")
|
||||
}
|
||||
if (tankinfo.left_ml == null){
|
||||
this.tank_measure_ml_container.classList.add("hidden")
|
||||
} else {
|
||||
this.tank_measure_ml.innerText = tankinfo.left_ml.toString();
|
||||
this.tank_measure_ml_container.classList.remove("hidden")
|
||||
}
|
||||
if (tankinfo.percent == null){
|
||||
this.tank_measure_percent_container.classList.add("hidden")
|
||||
} else {
|
||||
this.tank_measure_percent.innerText = tankinfo.percent.toString();
|
||||
this.tank_measure_percent_container.classList.remove("hidden")
|
||||
}
|
||||
if (tankinfo.water_temp == null){
|
||||
this.tank_measure_temperature_container.classList.add("hidden")
|
||||
} else {
|
||||
this.tank_measure_temperature.innerText = tankinfo.water_temp.toString();
|
||||
this.tank_measure_temperature_container.classList.remove("hidden")
|
||||
}
|
||||
if (tankinfo.raw == null){
|
||||
this.tank_measure_rawvolt_container.classList.add("hidden")
|
||||
} else {
|
||||
this.tank_measure_rawvolt.innerText = tankinfo.raw.toString();
|
||||
this.tank_measure_rawvolt_container.classList.remove("hidden")
|
||||
}
|
||||
|
||||
if (tankinfo.temp_sensor_error == null){
|
||||
this.tank_measure_temperature_error_container.classList.add("hidden")
|
||||
} else {
|
||||
this.tank_measure_temperature_error.innerText = tankinfo.temp_sensor_error;
|
||||
this.tank_measure_temperature_error_container.classList.remove("hidden")
|
||||
}
|
||||
|
||||
this.tank_measure_enoughwater.innerText = tankinfo.enough_water.toString()
|
||||
this.tank_measure_warnlevel.innerText = tankinfo.warn_level.toString()
|
||||
|
||||
if(this.auto_refresh.checked){
|
||||
this.timer = setTimeout(this.controller.loadTankInfo, 1000);
|
||||
} else {
|
||||
if(this.timer){
|
||||
clearTimeout(this.timer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setConfig(tank: TankConfig) {
|
||||
this.tank_allow_pumping_if_sensor_error.checked = tank.tank_allow_pumping_if_sensor_error;
|
||||
this.tank_empty_percent.value = String(tank.tank_empty_percent)
|
||||
this.tank_warn_percent.value = String(tank.tank_warn_percent)
|
||||
this.tank_full_percent.value = String(tank.tank_full_percent)
|
||||
this.tank_sensor_enabled.checked = tank.tank_sensor_enabled
|
||||
this.tank_useable_ml.value = String(tank.tank_useable_ml)
|
||||
this.ml_per_pulse.value = String(tank.ml_per_pulse)
|
||||
}
|
||||
getConfig(): TankConfig {
|
||||
return {
|
||||
tank_allow_pumping_if_sensor_error: this.tank_allow_pumping_if_sensor_error.checked,
|
||||
tank_empty_percent : this.tank_empty_percent.valueAsNumber,
|
||||
tank_full_percent: this.tank_full_percent.valueAsNumber,
|
||||
tank_sensor_enabled: this.tank_sensor_enabled.checked,
|
||||
tank_useable_ml: this.tank_useable_ml.valueAsNumber,
|
||||
tank_warn_percent: this.tank_warn_percent.valueAsNumber,
|
||||
ml_per_pulse: this.ml_per_pulse.valueAsNumber
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Software/MainBoard/rust/src_webpack/src/timeview.html
Normal file
28
Software/MainBoard/rust/src_webpack/src/timeview.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<div style="display:flex">
|
||||
<span style="flex-grow: 1; text-align: center; font-weight: bold;">
|
||||
Time:
|
||||
</span>
|
||||
<input id="timeview_auto_refresh" type="checkbox">⟳
|
||||
</div>
|
||||
|
||||
<div style="display:flex">
|
||||
<span style="min-width: 50px;">MCU:</span>
|
||||
<div id="timeview_esp_time" style="text-wrap: nowrap; flex-grow: 1;">Esp time</div>
|
||||
</div>
|
||||
<div style="display:flex">
|
||||
<span style="min-width: 50px;">RTC:</span>
|
||||
<div id="timeview_rtc_time" style="text-wrap: nowrap; flex-grow: 1;">Rtc time</div>
|
||||
</div>
|
||||
<div style="display:flex">
|
||||
<span style="min-width: 50px;">Local:</span>
|
||||
<div id="timeview_browser_time" style="text-wrap: nowrap; flex-grow: 1;">Local time</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex">
|
||||
<span style="min-width: 50px;">Timezone:</span>
|
||||
<select id="timezone_select" style="text-wrap: nowrap; flex-grow: 1;">
|
||||
<option value="" disabled selected>Select Timezone</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button id="timeview_time_upload">Store Browser time into esp and rtc</button>
|
||||
74
Software/MainBoard/rust/src_webpack/src/timeview.ts
Normal file
74
Software/MainBoard/rust/src_webpack/src/timeview.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Controller } from "./main";
|
||||
|
||||
export class TimeView {
|
||||
esp_time: HTMLDivElement
|
||||
rtc_time: HTMLDivElement
|
||||
browser_time: HTMLDivElement
|
||||
sync: HTMLButtonElement
|
||||
auto_refresh: HTMLInputElement;
|
||||
controller: Controller;
|
||||
timer: NodeJS.Timeout | undefined;
|
||||
timezoneSelect: HTMLSelectElement;
|
||||
|
||||
constructor(controller:Controller) {
|
||||
(document.getElementById("timeview") as HTMLElement).innerHTML = require("./timeview.html")
|
||||
this.timezoneSelect = document.getElementById('timezone_select') as HTMLSelectElement;
|
||||
this.timezoneSelect.onchange = function(){
|
||||
controller.configChanged()
|
||||
}
|
||||
|
||||
this.auto_refresh = document.getElementById("timeview_auto_refresh") as HTMLInputElement;
|
||||
this.esp_time = document.getElementById("timeview_esp_time") as HTMLDivElement;
|
||||
this.rtc_time = document.getElementById("timeview_rtc_time") as HTMLDivElement;
|
||||
this.browser_time = document.getElementById("timeview_browser_time") as HTMLDivElement;
|
||||
this.sync = document.getElementById("timeview_time_upload") as HTMLButtonElement;
|
||||
this.sync.onclick = controller.syncRTCFromBrowser;
|
||||
this.controller = controller;
|
||||
|
||||
this.auto_refresh.onchange = () => {
|
||||
if(this.timer){
|
||||
clearTimeout(this.timer)
|
||||
}
|
||||
if(this.auto_refresh.checked){
|
||||
controller.updateRTCData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update(native: string, rtc: string) {
|
||||
this.esp_time.innerText = native;
|
||||
this.rtc_time.innerText = rtc;
|
||||
const date = new Date();
|
||||
this.browser_time.innerText = date.toISOString();
|
||||
if(this.auto_refresh.checked){
|
||||
this.timer = setTimeout(this.controller.updateRTCData, 1000);
|
||||
} else {
|
||||
if(this.timer){
|
||||
clearTimeout(this.timer)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
timezones(timezones: string[]) {
|
||||
timezones.forEach(tz => {
|
||||
const option = document.createElement('option');
|
||||
option.value = tz;
|
||||
option.textContent = tz;
|
||||
this.timezoneSelect.appendChild(option);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
getTimeZone() {
|
||||
return this.timezoneSelect.value;
|
||||
}
|
||||
|
||||
setTimeZone(timezone: string | undefined) {
|
||||
if (timezone != undefined) {
|
||||
this.timezoneSelect.value = timezone;
|
||||
} else {
|
||||
this.timezoneSelect.value = "UTC";
|
||||
}
|
||||
}
|
||||
}
|
||||
93
Software/MainBoard/rust/src_webpack/src/toast.ts
Normal file
93
Software/MainBoard/rust/src_webpack/src/toast.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
class ToastService {
|
||||
private container: HTMLElement;
|
||||
private stylesInjected = false;
|
||||
|
||||
constructor() {
|
||||
this.container = this.ensureContainer();
|
||||
this.injectStyles();
|
||||
}
|
||||
|
||||
info(message: string, timeoutMs: number = 5000) {
|
||||
const el = this.createToast(message, 'info');
|
||||
this.container.appendChild(el);
|
||||
// Auto-dismiss after timeout
|
||||
const timer = window.setTimeout(() => this.dismiss(el), timeoutMs);
|
||||
// Dismiss on click immediately
|
||||
el.addEventListener('click', () => {
|
||||
window.clearTimeout(timer);
|
||||
this.dismiss(el);
|
||||
});
|
||||
}
|
||||
|
||||
error(message: string) {
|
||||
const el = this.createToast(message, 'error');
|
||||
this.container.appendChild(el);
|
||||
// Only dismiss on click
|
||||
el.addEventListener('click', () => this.dismiss(el));
|
||||
}
|
||||
|
||||
private dismiss(el: HTMLElement) {
|
||||
if (!el.parentElement) return;
|
||||
el.parentElement.removeChild(el);
|
||||
}
|
||||
|
||||
private createToast(message: string, type: 'info' | 'error'): HTMLElement {
|
||||
const div = document.createElement('div');
|
||||
div.className = `toast ${type}`;
|
||||
div.textContent = message;
|
||||
div.setAttribute('role', 'status');
|
||||
div.setAttribute('aria-live', 'polite');
|
||||
return div;
|
||||
}
|
||||
|
||||
private ensureContainer(): HTMLElement {
|
||||
let container = document.getElementById('toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = 'toast-container';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
private injectStyles() {
|
||||
if (this.stylesInjected) return;
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
#toast-container {
|
||||
position: fixed;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
z-index: 9999;
|
||||
}
|
||||
.toast {
|
||||
max-width: 320px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.toast.info {
|
||||
background-color: #d4edda; /* green-ish */
|
||||
color: #155724;
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
.toast.error {
|
||||
background-color: #f8d7da; /* red-ish */
|
||||
color: #721c24;
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
this.stylesInjected = true;
|
||||
}
|
||||
}
|
||||
|
||||
export const toast = new ToastService();
|
||||
Reference in New Issue
Block a user