549 lines
18 KiB
TypeScript
549 lines
18 KiB
TypeScript
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
|
|
} from "./api";
|
|
|
|
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);
|
|
});
|
|
}
|
|
getBackupInfo() : Promise<void> {
|
|
return fetch(PUBLIC_URL + "/backup_info")
|
|
.then(response => response.json())
|
|
.then(json => json as BackupHeader)
|
|
.then(header => {
|
|
controller.submitView.setBackupInfo(header)
|
|
})
|
|
.catch(error => {
|
|
console.log(error);
|
|
});
|
|
}
|
|
|
|
populateTimezones(): Promise<void> {
|
|
return fetch(PUBLIC_URL+'/timezones')
|
|
.then(response => response.json())
|
|
.then(json => json as string[])
|
|
.then(timezones => {
|
|
controller.timeView.timezones(timezones)
|
|
})
|
|
.catch(error => console.error('Error fetching timezones:', error));
|
|
}
|
|
|
|
updateFileList() : Promise<void> {
|
|
return fetch(PUBLIC_URL + "/files")
|
|
.then(response => response.json())
|
|
.then(json => json as FileList)
|
|
.then(filelist => {
|
|
controller.fileview.setFileList(filelist, PUBLIC_URL)
|
|
})
|
|
.catch(error => {
|
|
console.log(error);
|
|
});
|
|
}
|
|
uploadFile(file: File, name:string) {
|
|
var current = 0;
|
|
var max = 100;
|
|
controller.progressview.addProgress("file_upload", (current / max) * 100, "Uploading File " + name + "(" + current + "/" + max + ")")
|
|
var 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);
|
|
var 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()
|
|
}
|
|
|
|
updateRTCData() : Promise<void> {
|
|
return fetch(PUBLIC_URL + "/time")
|
|
.then(response => response.json())
|
|
.then(json => json as GetTime)
|
|
.then(time => {
|
|
controller.timeView.update(time.native, time.rtc)
|
|
})
|
|
.catch(error => {
|
|
controller.timeView.update("n/a", "n/a")
|
|
console.log(error);
|
|
});
|
|
}
|
|
updateBatteryData(): Promise<void> {
|
|
return fetch(PUBLIC_URL + "/battery")
|
|
.then(response => response.json())
|
|
.then(json => json as BatteryState)
|
|
.then(battery => {
|
|
controller.batteryView.update(battery)
|
|
})
|
|
.catch(error => {
|
|
controller.batteryView.update(null)
|
|
console.log(error);
|
|
})
|
|
}
|
|
uploadNewFirmware(file: File) {
|
|
var current = 0;
|
|
var max = 100;
|
|
controller.progressview.addProgress("ota_upload", (current / max) * 100, "Uploading firmeware (" + current + "/" + max + ")")
|
|
var 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")
|
|
controller.reboot();
|
|
}, false);
|
|
ajax.addEventListener("error", () => {
|
|
alert("Error ota")
|
|
controller.progressview.removeProgress("ota_upload")
|
|
}, false);
|
|
ajax.addEventListener("abort", () => {
|
|
alert("abort ota")
|
|
controller.progressview.removeProgress("ota_upload")
|
|
}, false);
|
|
ajax.open("POST", PUBLIC_URL + "/ota");
|
|
ajax.send(file);
|
|
}
|
|
version() : Promise<void> {
|
|
controller.progressview.addIndeterminate("version", "Getting buildVersion")
|
|
return fetch(PUBLIC_URL + "/version")
|
|
.then(response => response.json())
|
|
.then(json => json as VersionInfo)
|
|
.then(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();
|
|
var 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))
|
|
controller.progressview.removeProgress("set_config")
|
|
//load from remote to be clean
|
|
controller.downloadConfig()
|
|
}
|
|
backupConfig(json: string, statusCallback: (status: string) => void) {
|
|
controller.progressview.addIndeterminate("backup_config", "Backingup Config")
|
|
fetch(PUBLIC_URL + "/backup_config", {
|
|
method: "POST",
|
|
body: json,
|
|
})
|
|
.then(response => response.text())
|
|
.then(text => statusCallback(text))
|
|
controller.progressview.removeProgress("backup_config")
|
|
}
|
|
syncRTCFromBrowser() {
|
|
controller.progressview.addIndeterminate("write_rtc", "Writing RTC")
|
|
var value: SetTime = {
|
|
time: new Date().toISOString()
|
|
}
|
|
var 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){
|
|
var 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.text())
|
|
.then(
|
|
_ => {
|
|
clearTimeout(timerId);
|
|
controller.progressview.removeProgress("test_pump");
|
|
}
|
|
)
|
|
}
|
|
|
|
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) {
|
|
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 fileview: FileView;
|
|
readonly logView: LogView
|
|
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.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.rebootBtn = document.getElementById("reboot") as HTMLButtonElement
|
|
this.rebootBtn.onclick = () => {
|
|
controller.reboot();
|
|
}
|
|
this.exitBtn = document.getElementById("exit") as HTMLButtonElement
|
|
this.exitBtn.onclick = () => {
|
|
controller.exit();
|
|
}
|
|
}
|
|
|
|
selftest() {
|
|
|
|
}
|
|
}
|
|
const controller = new Controller();
|
|
controller.progressview.removeProgress("rebooting");
|
|
|
|
|
|
|
|
|
|
const tasks = [
|
|
{ task: controller.populateTimezones, displayString: "Populating Timezones" },
|
|
{ task: controller.updateRTCData, displayString: "Updating RTC Data" },
|
|
{ task: controller.updateBatteryData, displayString: "Updating Battery Data" },
|
|
{ task: controller.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;
|
|
}
|
|
}); |