added file manager, upload animation, unsaved config indicator
This commit is contained in:
parent
e7556b7ec9
commit
1ce4d74a65
@ -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,
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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())?;
|
||||
|
10
rust/src_webpack/package-lock.json
generated
10
rust/src_webpack/package-lock.json
generated
@ -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",
|
||||
|
@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"fast-equals": "^5.2.2",
|
||||
"source-map-loader": "^4.0.1"
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -1,3 +1,13 @@
|
||||
<style>
|
||||
.powerflexkey {
|
||||
min-width: 150px;
|
||||
}
|
||||
.powerflexvalue {
|
||||
text-wrap: nowrap;
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="flexcontainer">
|
||||
<div class="subtitle">
|
||||
Battery:
|
||||
|
72
rust/src_webpack/src/fileview.html
Normal file
72
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>
|
97
rust/src_webpack/src/fileview.ts
Normal file
97
rust/src_webpack/src/fileview.ts
Normal 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()
|
||||
}
|
||||
}
|
11
rust/src_webpack/src/fileviewentry.html
Normal file
11
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>
|
||||
|
@ -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>
|
@ -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");
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
@ -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();
|
||||
|
@ -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){
|
||||
|
@ -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>
|
||||
|
@ -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 = '';
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user