added file manager, upload animation, unsaved config indicator

This commit is contained in:
Empire Phoenix 2025-01-21 01:18:36 +01:00
parent e7556b7ec9
commit 1ce4d74a65
21 changed files with 550 additions and 186 deletions

View File

@ -956,6 +956,8 @@ fn wait_infinity(wait_type: WaitType, reboot_now: Arc<AtomicBool>) -> ! {
}
}
if reboot_now.load(std::sync::atomic::Ordering::Relaxed) {
//ensure clean http answer
Delay::new_default().delay_ms(500);
BOARD_ACCESS.lock().unwrap().deep_sleep( 1);
}
}
@ -1067,9 +1069,17 @@ fn get_version() -> VersionInfo {
let branch = env!("VERGEN_GIT_BRANCH").to_owned();
let hash = &env!("VERGEN_GIT_SHA")[0..8];
let running_partition = unsafe { esp_ota_get_running_partition() };
let address = unsafe { (*running_partition).address };
let partition = if address > 20000 {
"ota_1"
} else {
"ota_0"
};
return VersionInfo {
git_hash: (branch + "@" + hash),
build_time: env!("VERGEN_BUILD_TIMESTAMP").to_owned(),
partition: partition.to_owned()
};
}
@ -1077,4 +1087,5 @@ fn get_version() -> VersionInfo {
struct VersionInfo {
git_hash: String,
build_time: String,
partition: String,
}

View File

@ -24,7 +24,7 @@ use esp_idf_svc::wifi::EspWifi;
use measurements::Temperature;
use once_cell::sync::Lazy;
use plant_ctrl2::sipo::ShiftRegister40;
use esp_idf_sys::{esp_deep_sleep, esp_sleep_enable_ext1_wakeup, esp_sleep_ext1_wakeup_mode_t_ESP_EXT1_WAKEUP_ANY_LOW};
use esp_idf_sys::{esp_deep_sleep, esp_sleep_enable_ext1_wakeup, esp_sleep_ext1_wakeup_mode_t_ESP_EXT1_WAKEUP_ANY_LOW, esp_spiffs_info};
use esp_idf_sys::esp_restart;
use anyhow::{anyhow, Context};
@ -182,6 +182,8 @@ pub struct FileInfo {
#[derive(Serialize, Debug)]
pub struct FileList {
total: usize,
used: usize,
files: Vec<FileInfo>,
file_system_corrupt: Option<String>,
iter_error: Option<String>,
@ -232,19 +234,8 @@ impl PlantCtrlBoard<'_> {
pub fn list_files(&self, filename: &str) -> FileList {
let storage = CString::new(SPIFFS_PARTITION_NAME).unwrap();
let error = unsafe {
esp! {
esp_spiffs_check(storage.as_ptr())
}
};
let mut file_system_corrupt = match error {
OkStd(_) => None,
Err(err) => {
println!("Corrupt spiffs {err:?}");
Some(format!("{err:?}"))
}
};
let mut file_system_corrupt = Option::None;
let mut iter_error = None;
let mut result = Vec::new();
@ -280,8 +271,15 @@ impl PlantCtrlBoard<'_> {
file_system_corrupt = Some(format!("{err:?}"));
}
}
let mut total:usize = 0;
let mut used:usize = 0;
unsafe {
esp_spiffs_info(storage.as_ptr(), &mut total, &mut used);
}
return FileList {
total,
used,
file_system_corrupt,
files: result,
iter_error,

View File

@ -5,7 +5,7 @@ use std::{
sync::{atomic::AtomicBool, Arc},
};
use crate::{
espota::OtaUpdate, get_version, map_range_moisture, plant_hal::FileInfo, BOARD_ACCESS,
espota::OtaUpdate, get_version, map_range_moisture, plant_hal::{FileInfo, PLANT_COUNT}, BOARD_ACCESS,
};
use anyhow::bail;
use chrono::DateTime;
@ -159,6 +159,7 @@ fn get_battery_state(
fn get_version_web(
_request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
anyhow::Ok(Some(serde_json::to_string(&get_version())?))
}
@ -197,19 +198,30 @@ fn list_files(
fn ota(
request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
let mut board = BOARD_ACCESS.lock().unwrap();
let mut ota = OtaUpdate::begin()?;
println!("start ota");
//having a larger buffer is not really faster, requires more stack and prevents the progress bar from working ;)
const BUFFER_SIZE: usize = 512;
let mut buffer: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
let mut total_read: usize = 0;
let mut lastiter = 0;
loop {
let read = request.read(&mut buffer)?;
total_read += read;
println!("received {read} bytes ota {total_read}");
let to_write = &buffer[0..read];
let iter = (total_read/1024)%8;
if iter != lastiter {
for i in 0..PLANT_COUNT {
board.fault(i, iter==i);
}
lastiter = iter;
}
ota.write(to_write)?;
println!("wrote {read} bytes ota {total_read}");
if read == 0 {
@ -222,6 +234,8 @@ fn ota(
let mut finalizer = ota.finalize()?;
println!("changing boot partition");
board.set_restart_to_conf(true);
drop(board);
finalizer.set_as_boot_partition()?;
finalizer.restart();
}
@ -413,10 +427,11 @@ pub fn httpd(reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
server
.fn_handler("/file", Method::Post, move |mut request| {
let filename = query_param(request.uri(), "filename").unwrap();
let file_handle = BOARD_ACCESS
.lock()
.unwrap()
.get_file_handle(&filename, true);
let lock = BOARD_ACCESS
.lock()
.unwrap();
let file_handle =
lock.get_file_handle(&filename, true);
match file_handle {
//TODO get free filesystem size, check against during write if not to large
@ -424,13 +439,21 @@ pub fn httpd(reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
const BUFFER_SIZE: usize = 512;
let mut buffer: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
let mut total_read: usize = 0;
let mut lastiter = 0;
loop {
let iter = (total_read/1024)%8;
if iter != lastiter {
for i in 0..PLANT_COUNT {
lock.fault(i, iter==i);
}
lastiter = iter;
}
let read = request.read(&mut buffer)?;
total_read += read;
println!("sending {read} bytes of {total_read} for upload {filename}");
let to_write = &buffer[0..read];
std::io::Write::write(&mut file_handle, to_write)?;
println!("wrote {read} bytes of {total_read} for upload {filename}");
if read == 0 {
break;
}
@ -444,6 +467,7 @@ pub fn httpd(reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
cors_response(request, 500, &error_text)?;
}
}
drop(lock);
anyhow::Ok(())
})
.unwrap();
@ -466,6 +490,11 @@ pub fn httpd(reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
anyhow::Ok(())
})
.unwrap();
server
.fn_handler("/file", Method::Options, |request| {
cors_response(request, 200, "")
})
.unwrap();
server
.fn_handler("/flashbattery", Method::Post, move |request| {
@ -529,6 +558,7 @@ fn cors_response(
let headers = [
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
];
let mut response = request.into_response(status, None, &headers)?;
response.write(body.as_bytes())?;

View File

@ -6,6 +6,7 @@
"": {
"dependencies": {
"copy-webpack-plugin": "^12.0.2",
"fast-equals": "^5.2.2",
"source-map-loader": "^4.0.1"
},
"devDependencies": {
@ -1765,6 +1766,15 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-equals": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz",
"integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/fast-glob": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",

View File

@ -11,6 +11,7 @@
},
"dependencies": {
"copy-webpack-plugin": "^12.0.2",
"fast-equals": "^5.2.2",
"source-map-loader": "^4.0.1"
}
}

View File

@ -6,6 +6,19 @@ interface NetworkConfig {
base_topic: string
}
interface FileList {
total: number,
used: number,
files: FileInfo[],
file_system_corrupt: string,
iter_error: string,
}
interface FileInfo{
filename: string,
size: number,
}
interface NightLampConfig {
night_lamp_hour_start: number,
night_lamp_hour_end: number,
@ -64,7 +77,8 @@ interface Moistures {
interface VersionInfo {
git_hash: string,
build_time: string
build_time: string,
partition: string
}
interface BatteryState {

View File

@ -1,3 +1,13 @@
<style>
.powerflexkey {
min-width: 150px;
}
.powerflexvalue {
text-wrap: nowrap;
flex-grow: 1;
}
</style>
<div class="flexcontainer">
<div class="subtitle">
Battery:

View 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>

View File

@ -0,0 +1,97 @@
import { Controller } from "./main";
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 = () => {
var 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 = () => {
var 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;
const fileRaw = template.replaceAll("${fileid}", String(fileid));
this.view.innerHTML = fileRaw
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()
}
}

View 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>

View File

@ -1,6 +1,7 @@
<style>
.progressPane {
display: block;
display: flex;
flex-direction: column;
position: fixed;
width: 100%;
height: 100%;
@ -8,27 +9,21 @@
padding: 0;
top: 0;
left: 0;
background-color: lightgrey;
background-color: grey;
opacity: 0.8;
}
.progressPaneCenter {
display: inline-block;
margin-top: 48%;
position: absolute;
height: 4%;
width: 50%;
margin-left: 25%;
margin-right: 25%;
}
.progress {
height: 1.5em;
width: 100%;
background-color: #c9c9c9;
background-color: #555;
position: relative;
}
.progressSpacer{
flex-grow: 1;
}
.progress:after {
content: attr(data-label);
font-size: 0.8em;
@ -40,13 +35,13 @@
}
.progress .value {
background-color: #7cc4ff;
background-color: darkcyan;
display: inline-block;
height: 100%;
}
.progress .valueIndeterminate {
background-color: #7cc4ff;
background-color: darkcyan;
display: inline-block;
height: 100%;
animation: indeterminateAnimation 1s infinite linear;
@ -73,8 +68,13 @@
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;
@ -100,7 +100,7 @@
padding: 8px;
}
}
@media (min-width: 900px) {
@media (min-width: 1100px) {
.plantcontainer {
flex-grow: 1;
min-width: 20%;
@ -109,7 +109,7 @@
padding: 8px;
}
}
@media (min-width: 1800px) {
@media (min-width: 2150px) {
.plantcontainer {
flex-grow: 1;
min-width: 200px;
@ -132,75 +132,6 @@
font-weight: bold;
}
.powerflexkey {
min-width: 150px;
}
.powerflexvalue {
text-wrap: nowrap;
flex-grow: 1;
}
.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;
}
.otakey{
min-width: 100px;
}
.otavalue{
flex-grow: 1;
}
.otaform {
min-width: 100px;
flex-grow: 1;
}
.otachooser {
min-width: 100px;
width: 100%;
}
.lightcheckbox{
margin: 0px;
min-width: 20px
}
.lightkey{
min-width: 200px;
}
.lightvalue{
flex-grow: 1;
}
.lightnumberbox{
min-width: 75px;
}
.tankcheckbox {
min-width: 20px;
margin: 0px;
}
.tankkey{
min-width: 250px;
}
.tankvalue{
flex-grow: 1;
margin: 0px;
}
</style>
@ -208,11 +139,11 @@
<div class="container-xl">
<div style="display:flex; flex-wrap: wrap;">
<div id="firmwareview" style="border-width: 1px; border-style: solid; flex-grow: 1; min-width: 350px">
<div id="firmwareview" class="subcontainer">
</div>
<div id="timeview" style="border-width: 1px; border-style: solid; flex-grow: 1;">
<div id="timeview" class="subcontainer">
</div>
<div id="batteryview" style="border-width: 1px; border-style: solid; flex-grow: 1;">
<div id="batteryview" class="subcontainer">
</div>
</div>
@ -224,31 +155,32 @@
</div>
</div>
<h2>config</h2>
<h3>Plants:</h3>
<button id="measure_moisture">Measure Moisture</button>
<div id="plants" class="plantlist"></div>
<button id="submit">Submit</button>
<div id="submit_status"></div>
<br>
<textarea id="json" cols=50 rows=10></textarea>
<script src="bundle.js"></script>
<div class="flexcontainer-rev">
<div>
<textarea id="json" cols=50 rows=10></textarea>
<button id="submit">Submit</button>
<div id="submit_status"></div>
</div>
<div id="fileview" class="subcontainer">
</div>
</div>
<button id="exit">Exit</button>
<button id="reboot">Reboot</button>
<script src="bundle.js"></script>
</div>
<div id="progressPane" class="progressPane">
<div class="progressPaneCenter">
<div id="progressPaneBar" class="progress" data-label="50% Complete">
<span id="progressPaneSpan" class="value" style="width:100%;"></span>
</div>
<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>

View File

@ -1,4 +1,6 @@
import { deepEqual } from 'fast-equals';
declare var PUBLIC_URL: string;
console.log("Url is " + PUBLIC_URL);
@ -14,8 +16,69 @@ import { SubmitView } from "./submitView";
import { ProgressView } from "./progress";
import { OTAView } from "./ota";
import { BatteryView } from "./batteryview";
import { FileView } from './fileview';
export class Controller {
updateFileList() {
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() {
fetch(PUBLIC_URL + "/time")
.then(response => response.json())
@ -81,12 +144,16 @@ export class Controller {
.then(response => response.json())
.then(loaded => {
var currentConfig = loaded as PlantControllerConfig;
this.setConfig(currentConfig);
controller.setInitialConfig(currentConfig);
controller.setConfig(currentConfig);
//sync json view initially
this.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", {
@ -96,6 +163,8 @@ export class Controller {
.then(response => response.text())
.then(text => statusCallback(text))
controller.progressview.removeProgress("set_config")
//load from remote to be clean
controller.downloadConfig()
}
syncRTCFromBrowser() {
controller.progressview.addIndeterminate("write_rtc", "Writing RTC")
@ -113,12 +182,19 @@ export class Controller {
configChanged() {
const current = controller.getConfig();
var pretty = JSON.stringify(current, undefined, 1);
console.log(pretty)
var pretty = JSON.stringify(current, undefined, 0);
var initial = JSON.stringify(this.initialConfig, undefined, 0);
controller.submitView.setJson(pretty);
if (deepEqual(current, controller.initialConfig)) {
document.title = "PlantCtrl"
} else {
document.title = "*PlantCtrl"
}
}
testPlant(plantId: number) {
let counter = 0
let limit = 30
@ -261,8 +337,7 @@ export class Controller {
setTimeout(this.waitForReboot, 1000)
}
initialConfig: PlantControllerConfig | null = null
readonly rebootBtn: HTMLButtonElement
readonly exitBtn: HTMLButtonElement
readonly timeView: TimeView;
@ -274,6 +349,7 @@ export class Controller {
readonly firmWareView: OTAView;
readonly progressview: ProgressView;
readonly batteryView: BatteryView;
readonly fileview: FileView;
constructor() {
this.timeView = new TimeView(this)
this.plantViews = new PlantViews(this)
@ -284,6 +360,7 @@ export class Controller {
this.submitView = new SubmitView(this)
this.firmWareView = new OTAView(this)
this.progressview = new ProgressView(this)
this.fileview = new FileView(this)
this.rebootBtn = document.getElementById("reboot") as HTMLButtonElement
this.rebootBtn.onclick = () => {
controller.reboot();
@ -300,5 +377,6 @@ controller.updateBatteryData();
controller.downloadConfig();
//controller.measure_moisture();
controller.version();
controller.updateFileList();
controller.progressview.removeProgress("rebooting");

View File

@ -1,3 +1,25 @@
<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">
@ -14,7 +36,7 @@
<div class="flexcontainer">
<label class="basicnetworkkey" for="ssid">Station Mode:</label>
<input class="basicnetworkkeyssid1" type="text" id="ssid" list="ssidlist">
<input class="basicnetworkkeyssid1" type="search" id="ssid" list="ssidlist">
<datalist id="ssidlist">
<option value="Not scanned yet">
</datalist>

View File

@ -1,5 +1,22 @@
<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="flexcontainer" style="text-decoration-line: line-through;">
<div class="lightkey">Enable Nightlight</div>
<input class="lightcheckbox" type="checkbox" id="night_lamp_enabled" checked="false">
</div>

View File

@ -1,3 +1,19 @@
<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
@ -11,6 +27,10 @@
<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">
<form class="otaform" id="upload_form" method="post">
<input class="otachooser" type="file" name="file1" id="firmware_file"><br>

View File

@ -1,16 +1,19 @@
import { Controller } from "./main";
export class OTAView {
file1Upload: HTMLInputElement;
firmware_buildtime: HTMLDivElement;
firmware_githash: HTMLDivElement;
readonly file1Upload: HTMLInputElement;
readonly firmware_buildtime: HTMLDivElement;
readonly firmware_githash: HTMLDivElement;
readonly firmware_partition: HTMLDivElement;
constructor(controller: Controller) {
(document.getElementById("firmwareview") as HTMLElement).innerHTML = require("./ota.html")
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;
const file = document.getElementById("firmware_file") as HTMLInputElement;
this.file1Upload = file
this.file1Upload.onchange = () => {
@ -26,5 +29,6 @@ export class OTAView {
setVersion(versionInfo: VersionInfo) {
this.firmware_buildtime.innerText = versionInfo.build_time;
this.firmware_githash.innerText = versionInfo.git_hash;
this.firmware_partition.innerText = versionInfo.partition;
}
}

View File

@ -1,65 +1,88 @@
<style>
.plantsensorkey{
min-width: 100px;
}
.plantsensorvalue{
flex-grow: 1;
}
.plantkey{
min-width: 175px;
}
.plantvalue{
flex-grow: 1;
}
.plantcheckbox{
min-width: 20px;
margin: 0px;
}
</style>
<div>
<div style="font-weight: bold; text-align: center; flex-grow: 1;"
id="plant_${plantId}_header">Plant ${plantId}</div>
<div class="subtitle"
id="plant_${plantId}_header">
Plant ${plantId}
</div>
<div>
<div class="row">
<div class="col-1"></div>
<button class="col-10" id="plant_${plantId}_test">Test</button>
<div class="col-1"></div>
</div>
<div class="row">
<div class="col-12">Live:</div>
</div>
<div class="row">
<div class="col-7">Sensor A:</div>
<span class="col-4" id="plant_${plantId}_moisture_a">loading</span>
<div class="col-7">Sensor B:</div>
<span class="col-4" id="plant_${plantId}_moisture_b">loading</span>
</div>
<div class="row">
<div class="col-7">
<div class="flexcontainer">
<div class="plantkey">
Mode:
</div>
<select class="col-4" id="plant_${plantId}_mode">
<select class="plantvalue" id="plant_${plantId}_mode">
<option value="OFF">Off</option>
<option value="TargetMoisture">Target</option>
<option value="TimerOnly">Timer</option>
</select>
</div>
<div class="row">
<div class="col-7">Target Moisture:</div>
<input class="col-4" id="plant_${plantId}_target_moisture" type="number" min="0" max="100" placeholder="0">
<div class="flexcontainer">
<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="row">
<div class="col-7">Pump Time (s):</div>
<input class="col-4" id="plant_${plantId}_pump_time_s" type="number" min="0" max="600" placeholder="30">
<div class="flexcontainer">
<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="row">
<div class="col-7">Pump Cooldown (m):</div>
<input class="col-4" id="plant_${plantId}_pump_cooldown_min" type="number" min="0" max="600" placeholder="30">
<div class="flexcontainer">
<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="row">
<div class="col-7">"Pump Hour Start":</div>
<select class="col-4" id="plant_${plantId}_pump_hour_start">10</select>
<div class="flexcontainer">
<div class="plantkey">"Pump Hour Start":</div>
<select class="plantvalue" id="plant_${plantId}_pump_hour_start">10</select>
</div>
<div class="row">
<div class="col-7">"Pump Hour End":</div>
<select class="col-4" id="plant_${plantId}_pump_hour_end">19</select>
<div class="flexcontainer">
<div class="plantkey">"Pump Hour End":</div>
<select class="plantvalue" id="plant_${plantId}_pump_hour_end">19</select>
</div>
<div class="row">
<div class="col-7">Sensor B installed:</div>
<input class="col-4" id="plant_${plantId}_sensor_b" type="checkbox">
</div>
<div class="row">
<div class="col-7">Warn Pump Count:</div>
<input class="col-4" id="plant_${plantId}_max_consecutive_pump_count" type="number" min="1" , max="50" ,
<div class="flexcontainer">
<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>
<div class="flexcontainer">
<div class="plantkey">Sensor B installed:</div>
<input class="plantcheckbox" id="plant_${plantId}_sensor_b" type="checkbox">
</div>
<div class="flexcontainer">
<button class="subtitle" id="plant_${plantId}_test">Test Pump</button>
</div>
<div class="flexcontainer">
<div class="subtitle">Live:</div>
</div>
<div class="flexcontainer">
<span class="plantsensorkey">Sensor A:</span>
<span class="plantsensorvalue" id="plant_${plantId}_moisture_a">loading</span>
</div>
<div class="flexcontainer">
<div class="plantsensorkey">Sensor B:</div>
<span class="plantsensorvalue" id="plant_${plantId}_moisture_b">loading</span>
</div>
</div>

View File

@ -144,7 +144,6 @@ export class PlantView {
}
setConfig(plantConfig: PlantConfig) {
console.log("apply config to ui plant " + this.plantId + " config: " + JSON.stringify(plantConfig))
this.mode.value = plantConfig.mode;
this.targetMoisture.value = plantConfig.target_moisture.toString();
this.pumpTimeS.value = plantConfig.pump_time_s.toString();

View File

@ -44,14 +44,14 @@ export class ProgressView{
addIndeterminate(id:string, displayText:string){
this.progresses.set(id, new ProgressInfo(displayText,0,true))
this.progressPane.style.display = "block"
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 = "block"
this.progressPane.style.display = "flex"
this.updateView();
}
removeProgress(id:string){

View File

@ -1,7 +1,21 @@
<style>
.tankcheckbox {
min-width: 20px;
margin: 0px;
}
.tankkey{
min-width: 250px;
}
.tankvalue{
flex-grow: 1;
margin: 0px;
}
</style>
<div class="flexcontainer">
<div class="subtitle">Tank:</div>
</div>
<div class="flexcontainer">
<div class="flexcontainer" style="text-decoration-line: line-through;">
<span class="tankkey">Enable Tank Sensor</span>
<input class="tankcheckbox" type="checkbox" id="tank_sensor_enabled">
</div>

View File

@ -8,7 +8,8 @@ const isDevServer = process.env.WEBPACK_SERVE;
console.log("Dev server is " + isDevServer);
var host;
if (isDevServer){
host = 'http://10.23.43.24';
//ensure no trailing /
host = 'http://192.168.1.172';
} else {
host = '';
}