further components and bootstrap initial

This commit is contained in:
Empire Phoenix 2024-12-20 03:29:29 +01:00
parent 5fedbec433
commit 58b63fc8ee
22 changed files with 3347 additions and 436 deletions

View File

@ -293,7 +293,7 @@ fn safe_main() -> anyhow::Result<()> {
board.general_fault(true);
}
}
if (config.network.mqtt_url.is_some()){
if (config.network.mqtt_url.is_some()) {
match board.mqtt(&config) {
Ok(_) => {
println!("Mqtt connection ready");
@ -319,8 +319,11 @@ fn safe_main() -> anyhow::Result<()> {
match board.wifi_ap(Some(config.network.ap_ssid.clone())) {
Ok(_) => {
println!("Started ap, continuing")
},
Err(err) => println!("Could not start config override ap mode due to {}", err.to_string()),
}
Err(err) => println!(
"Could not start config override ap mode due to {}",
err.to_string()
),
}
}

View File

@ -515,7 +515,8 @@ impl PlantCtrlBoard<'_> {
}
pub fn wifi_ap(&mut self, ap_ssid: Option<heapless::String<32>>) -> Result<()> {
let ssid = ap_ssid.unwrap_or(heapless::String::from_str("PlantCtrl Emergency Mode").unwrap());
let ssid =
ap_ssid.unwrap_or(heapless::String::from_str("PlantCtrl Emergency Mode").unwrap());
let apconfig = AccessPointConfiguration {
ssid,
auth_method: AuthMethod::None,
@ -644,7 +645,6 @@ impl PlantCtrlBoard<'_> {
}
pub fn get_rtc_time(&mut self) -> Result<DateTime<Utc>> {
match self.rtc.datetime() {
OkStd(rtc_time) => {
return Ok(rtc_time.and_utc());
@ -757,7 +757,6 @@ impl PlantCtrlBoard<'_> {
bail!("Mqtt url was empty")
}
let last_will_topic = format!("{}/state", base_topic);
let mqtt_client_config = MqttClientConfiguration {
lwt: Some(LwtConfiguration {
@ -835,20 +834,19 @@ impl PlantCtrlBoard<'_> {
}
esp_idf_svc::mqtt::client::EventPayload::BeforeConnect => {
println!("Mqtt before connect")
},
}
esp_idf_svc::mqtt::client::EventPayload::Subscribed(_) => {
println!("Mqtt subscribed")
},
}
esp_idf_svc::mqtt::client::EventPayload::Unsubscribed(_) => {
println!("Mqtt unsubscribed")
},
}
esp_idf_svc::mqtt::client::EventPayload::Published(_) => {
println!("Mqtt published")
},
}
esp_idf_svc::mqtt::client::EventPayload::Deleted(_) => {
println!("Mqtt deleted")
},
}
}
})?;

View File

@ -39,6 +39,10 @@ struct VersionInfo<'a> {
struct LoadData<'a> {
rtc: &'a str,
native: &'a str,
}
#[derive(Serialize, Debug)]
struct Moistures {
moisture_a: Vec<u8>,
moisture_b: Vec<u8>,
}
@ -67,18 +71,10 @@ fn write_time(
anyhow::Ok(None)
}
fn get_data(
fn get_live_moisture(
_request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
let mut board = BOARD_ACCESS.lock().unwrap();
let native = board
.time()
.and_then(|t| Ok(t.to_rfc3339()))
.unwrap_or("error".to_string());
let rtc = board
.get_rtc_time()
.and_then(|t| Ok(t.to_rfc3339()))
.unwrap_or("error".to_string());
let mut a: Vec<u8> = Vec::new();
let mut b: Vec<u8> = Vec::new();
@ -107,11 +103,31 @@ fn get_data(
}
}
let data = Moistures {
moisture_a: a,
moisture_b: b,
};
let json = serde_json::to_string(&data)?;
anyhow::Ok(Some(json))
}
fn get_data(
_request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
let mut board = BOARD_ACCESS.lock().unwrap();
let native = board
.time()
.and_then(|t| Ok(t.to_rfc3339()))
.unwrap_or("error".to_string());
let rtc = board
.get_rtc_time()
.and_then(|t| Ok(t.to_rfc3339()))
.unwrap_or("error".to_string());
let data = LoadData {
rtc: rtc.as_str(),
native: native.as_str(),
moisture_a: a,
moisture_b: b,
};
let json = serde_json::to_string(&data)?;
@ -145,8 +161,11 @@ fn set_config(
fn get_version(
_request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
let version_info = VersionInfo {
git_hash: env!("VERGEN_GIT_DESCRIBE"),
let branch = env!("VERGEN_GIT_BRANCH").to_owned();
let hash = &env!("VERGEN_GIT_SHA")[0..8];
let version_info: VersionInfo<'_> = VersionInfo {
git_hash: &(branch + "@" + hash),
build_time: env!("VERGEN_BUILD_TIMESTAMP"),
};
anyhow::Ok(Some(serde_json::to_string(&version_info)?))
@ -282,10 +301,15 @@ pub fn httpd(_reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
.unwrap();
server
.fn_handler("/data", Method::Get, |request| {
.fn_handler("/time", Method::Get, |request| {
handle_error_to500(request, get_data)
})
.unwrap();
server
.fn_handler("/moisture", Method::Get, |request| {
handle_error_to500(request, get_live_moisture)
})
.unwrap();
server
.fn_handler("/time", Method::Post, |request| {
handle_error_to500(request, write_time)
@ -311,6 +335,11 @@ pub fn httpd(_reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
handle_error_to500(request, ota)
})
.unwrap();
server
.fn_handler("/ota", Method::Options, |request| {
cors_response(request, 200, "")
})
.unwrap();
server
.fn_handler("/get_config", Method::Get, move |request| {
handle_error_to500(request, get_config)
@ -476,6 +505,14 @@ pub fn httpd(_reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
})
.unwrap();
server
.fn_handler("/bootstrap-grid.css", Method::Get, |request| {
request
.into_ok_response()?
.write(include_bytes!("bootstrap-grid.css"))?;
anyhow::Ok(())
})
.unwrap();
server
}
fn cors_response(
@ -483,7 +520,10 @@ fn cors_response(
status: u16,
body: &str,
) -> Result<(), anyhow::Error> {
let headers = [("Access-Control-Allow-Origin", "*")];
let headers = [
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*")
];
let mut response = request.into_response(status, None, &headers)?;
response.write(body.as_bytes())?;
response.flush()?;

View File

@ -9,6 +9,9 @@
},
"devDependencies": {
"copy-webpack-plugin": "^12.0.2",
"css-loader": "^7.1.2",
"html-webpack-harddisk-plugin": "^2.0.0",
"html-webpack-plugin": "^5.6.3",
"raw-loader": "^4.0.2",
"ts-loader": "^9.5.1",
"typescript": "^5.3.3",
@ -304,6 +307,13 @@
"@types/send": "*"
}
},
"node_modules/@types/html-minifier-terser": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
"integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/http-errors": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
@ -747,6 +757,16 @@
"ansi-html": "bin/ansi-html"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -863,6 +883,13 @@
"multicast-dns": "^7.2.5"
}
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"dev": true,
"license": "ISC"
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
@ -959,6 +986,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/camel-case": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz",
"integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"pascal-case": "^3.1.2",
"tslib": "^2.0.3"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001687",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001687.tgz",
@ -1029,6 +1067,29 @@
"node": ">=6.0"
}
},
"node_modules/clean-css": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
"integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==",
"dev": true,
"license": "MIT",
"dependencies": {
"source-map": "~0.6.0"
},
"engines": {
"node": ">= 10.0"
}
},
"node_modules/clean-css/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/clone-deep": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
@ -1273,6 +1334,85 @@
"node": ">= 8"
}
},
"node_modules/css-loader": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz",
"integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==",
"dev": true,
"license": "MIT",
"dependencies": {
"icss-utils": "^5.1.0",
"postcss": "^8.4.33",
"postcss-modules-extract-imports": "^3.1.0",
"postcss-modules-local-by-default": "^4.0.5",
"postcss-modules-scope": "^3.2.0",
"postcss-modules-values": "^4.0.0",
"postcss-value-parser": "^4.2.0",
"semver": "^7.5.4"
},
"engines": {
"node": ">= 18.12.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependencies": {
"@rspack/core": "0.x || 1.x",
"webpack": "^5.27.0"
},
"peerDependenciesMeta": {
"@rspack/core": {
"optional": true
},
"webpack": {
"optional": true
}
}
},
"node_modules/css-select": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
"integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.0.1",
"domhandler": "^4.3.1",
"domutils": "^2.8.0",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-what": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"license": "MIT",
"bin": {
"cssesc": "bin/cssesc"
},
"engines": {
"node": ">=4"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -1385,6 +1525,86 @@
"node": ">=6"
}
},
"node_modules/dom-converter": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
"integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==",
"dev": true,
"license": "MIT",
"dependencies": {
"utila": "~0.4"
}
},
"node_modules/dom-serializer": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
"integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
"dev": true,
"license": "MIT",
"dependencies": {
"domelementtype": "^2.0.1",
"domhandler": "^4.2.0",
"entities": "^2.0.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/domhandler": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
"integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.2.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
"integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^1.0.1",
"domelementtype": "^2.2.0",
"domhandler": "^4.2.0"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dot-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
"integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"no-case": "^3.0.4",
"tslib": "^2.0.3"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -1431,6 +1651,16 @@
"node": ">=10.13.0"
}
},
"node_modules/entities": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
"dev": true,
"license": "BSD-2-Clause",
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/envinfo": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz",
@ -1910,6 +2140,16 @@
"node": ">= 0.4"
}
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true,
"license": "MIT",
"bin": {
"he": "bin/he"
}
},
"node_modules/hpack.js": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
@ -1973,6 +2213,105 @@
],
"license": "MIT"
},
"node_modules/html-minifier-terser": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
"integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"camel-case": "^4.1.2",
"clean-css": "^5.2.2",
"commander": "^8.3.0",
"he": "^1.2.0",
"param-case": "^3.0.4",
"relateurl": "^0.2.7",
"terser": "^5.10.0"
},
"bin": {
"html-minifier-terser": "cli.js"
},
"engines": {
"node": ">=12"
}
},
"node_modules/html-minifier-terser/node_modules/commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/html-webpack-harddisk-plugin": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/html-webpack-harddisk-plugin/-/html-webpack-harddisk-plugin-2.0.0.tgz",
"integrity": "sha512-fWKH72FyaQ5K/j+kYy6LnQsQucSDnsEkghmB6g29TtpJ4sxHYFduEeUV1hfDqyDpCRW+bP7yacjQ+1ikeIDqeg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.13"
},
"peerDependencies": {
"html-webpack-plugin": "^5.0.0",
"webpack": "^5.0.0"
}
},
"node_modules/html-webpack-plugin": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz",
"integrity": "sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/html-minifier-terser": "^6.0.0",
"html-minifier-terser": "^6.0.2",
"lodash": "^4.17.21",
"pretty-error": "^4.0.0",
"tapable": "^2.0.0"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/html-webpack-plugin"
},
"peerDependencies": {
"@rspack/core": "0.x || 1.x",
"webpack": "^5.20.0"
},
"peerDependenciesMeta": {
"@rspack/core": {
"optional": true
},
"webpack": {
"optional": true
}
}
},
"node_modules/htmlparser2": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
"integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
"dev": true,
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.0.1",
"domhandler": "^4.0.0",
"domutils": "^2.5.2",
"entities": "^2.0.0"
}
},
"node_modules/http-deceiver": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
@ -2065,6 +2404,19 @@
"node": ">=0.10.0"
}
},
"node_modules/icss-utils": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
"integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^10 || ^12 || >= 14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -2395,6 +2747,23 @@
"node": ">=8"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true,
"license": "MIT"
},
"node_modules/lower-case": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
"integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
"dev": true,
"license": "MIT",
"dependencies": {
"tslib": "^2.0.3"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -2534,6 +2903,25 @@
"multicast-dns": "cli.js"
}
},
"node_modules/nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/negotiator": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
@ -2549,6 +2937,17 @@
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
},
"node_modules/no-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
"integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"lower-case": "^2.0.2",
"tslib": "^2.0.3"
}
},
"node_modules/node-forge": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
@ -2575,6 +2974,19 @@
"node": ">=0.10.0"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/object-inspect": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
@ -2691,6 +3103,17 @@
"node": ">=6"
}
},
"node_modules/param-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
"integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==",
"dev": true,
"license": "MIT",
"dependencies": {
"dot-case": "^3.0.4",
"tslib": "^2.0.3"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@ -2701,6 +3124,17 @@
"node": ">= 0.8"
}
},
"node_modules/pascal-case": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
"integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"no-case": "^3.0.4",
"tslib": "^2.0.3"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -2775,6 +3209,130 @@
"node": ">=8"
}
},
"node_modules/postcss": {
"version": "8.4.49",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-modules-extract-imports": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz",
"integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^10 || ^12 || >= 14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/postcss-modules-local-by-default": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz",
"integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==",
"dev": true,
"license": "MIT",
"dependencies": {
"icss-utils": "^5.0.0",
"postcss-selector-parser": "^7.0.0",
"postcss-value-parser": "^4.1.0"
},
"engines": {
"node": "^10 || ^12 || >= 14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/postcss-modules-scope": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz",
"integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==",
"dev": true,
"license": "ISC",
"dependencies": {
"postcss-selector-parser": "^7.0.0"
},
"engines": {
"node": "^10 || ^12 || >= 14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/postcss-modules-values": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
"integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"icss-utils": "^5.0.0"
},
"engines": {
"node": "^10 || ^12 || >= 14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/postcss-selector-parser": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz",
"integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/pretty-error": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",
"integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lodash": "^4.17.20",
"renderkid": "^3.0.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@ -2959,6 +3517,30 @@
"node": ">= 10.13.0"
}
},
"node_modules/relateurl": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
"integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/renderkid": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
"integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==",
"dev": true,
"license": "MIT",
"dependencies": {
"css-select": "^4.1.3",
"dom-converter": "^0.2.0",
"htmlparser2": "^6.1.0",
"lodash": "^4.17.21",
"strip-ansi": "^6.0.1"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@ -3409,9 +3991,10 @@
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
@ -3557,6 +4140,19 @@
"safe-buffer": "~5.2.0"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -3829,6 +4425,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/utila": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz",
"integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==",
"dev": true,
"license": "MIT"
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",

View File

@ -1,6 +1,7 @@
{
"devDependencies": {
"copy-webpack-plugin": "^12.0.2",
"html-webpack-harddisk-plugin": "^2.0.0",
"html-webpack-plugin": "^5.6.3",
"raw-loader": "^4.0.2",
"ts-loader": "^9.5.1",
"typescript": "^5.3.3",

2050
rust/src_webpack/public/bootstrap-grid.css vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -52,9 +52,12 @@ interface SetTime {
time: string
}
interface GetData {
interface GetTime {
rtc: string,
native: string,
native: string
}
interface Moistures {
moisture_a: [number],
moisture_b: [number],
}

View File

@ -1,127 +0,0 @@
<html>
<head>
</head>
<body>
<input type="button" id="test" value="Test">
<h2>Current Firmware</h2>
<div>
<div id="firmware_buildtime">Buildtime loading</div>
<div id="firmware_githash">Build githash loading</div>
</div>
<h2>Time</h2>
<div id="timeview">
<div id="timeview_esp_time">Esp time</div>
<div id="timeview_rtc_time">Rtc time</div>
<div id="timeview_browser_time">Rtc time</div>
<div>Store Browser time into esp and rtc</div>
<input type="button" id="timeview_time_upload" value="write">
</div>
<h2>firmeware OTA v3</h2>
<form id="upload_form" method="post">
<input type="file" name="file1" id="file1"><br>
<progress id="progressBar" value="0" max="100" style="width:300px;"></progress>
<h3 id="status"></h3>
<h3 id="answer"></h3>
<p id="loaded_n_total"></p>
</form>
<div>
<div id="remote_ip">remote ip</div>
<h2>WIFI</h2>
<input type="button" id="scan" value="Scan">
<br>
<label for="ap_ssid">AP SSID:</label>
<input type="text" id="ap_ssid" list="ssidlist">
<label for="ssid">SSID:</label>
<input type="text" id="ssid" list="ssidlist">
<datalist id="ssidlist">
<option value="Not scanned yet">
</datalist>
<label for="ssid">Password:</label>
<input type="text" id="password">
</div>
<h2>config</h2>
<div id="configform">
<h3>Mqtt:</h3>
<div>
MQTT Url
<input type="text" id="mqtt_url" placeholder="mqtt://192.168.1.1:1883">
</div>
<div>
Base Topic
<input type="text" id="base_topic" placeholder="plants/one">
</div>
<h3>Tank:</h3>
<div>
<input type="checkbox" id="tank_sensor_enabled">
Enable Tank Sensor
</div>
<div>
<input type="checkbox" id="tank_allow_pumping_if_sensor_error">
Allow Pumping if Sensor Error
</div>
<div>
<input type="number" min="2" max="500000" id="tank_useable_ml">
Tank Size mL
</div>
<div>
<input type="number" min="1" max="500000" id="tank_warn_percent">
Tank Warn Percent (mapped in relation to empty and full)
</div>
<div>
<input type="number" min="0" max="100" id="tank_empty_percent">
Tank Empty Percent (% max move)
</div>
<div>
<input type="number" min="0" max="100" id="tank_full_percent">
Tank Full Percent (% max move)
</div>
<h3>Light:</h3>
<div>
Start
<select type="time" id="night_lamp_time_start">
</select>
Stop
<select type="time" id="night_lamp_time_end">
</select>
</div>
<div>
<input type="checkbox" id="night_lamp_only_when_dark">
Light only when dark
</div>
<h2>Battery Firmeware (bq34z100 may be R2)</h2>
<form id="upload_form" method="post">
<input type="file" name="battery_flash_file" id="battery_flash_file"><br>
<progress id="battery_flash_progressBar" value="0" max="100" style="width:300px;"></progress>
<input type="button" name="battery_flash_button" id="battery_flash_button"><br>
<h3 id="battery_flash_status"></h3>
<p id="battery_flash_loaded_n_total"></p>
<div style="height: 100px; display: block; overflow-y: auto;" id="battery_flash_message"></div>
</form>
<h3>Plants:</h3>
<div id="plants"></div>
</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>
</body>
</html>

View File

@ -1,249 +0,0 @@
declare var PUBLIC_URL: string;
console.log("Url is " + PUBLIC_URL);
import { TimeView } from "./timeview";
import { PlantView, PlantViews } from "./plant";
import { NetworkConfigView } from "./network";
import { NightLampView } from "./nightmode";
import { TankConfigView } from "./tanks";
export class SubmitView{
json: HTMLInputElement;
submitFormBtn: HTMLButtonElement;
submit_status: HTMLElement;
constructor(controller: Controller){
this.json = document.getElementById('json') as HTMLInputElement
this.submitFormBtn = document.getElementById("submit") as HTMLButtonElement
this.submit_status = document.getElementById("submit_status") as HTMLElement
this.submitFormBtn.onclick = () => {
controller.uploadConfig(this.json.value, (status:string) => {
this.submit_status.innerHTML = status;
});
}
}
setJson(pretty: string) {
this.json.value = pretty
}
}
export class Controller {
downloadConfig() {
fetch(PUBLIC_URL + "/get_config")
.then(response => response.json())
.then(loaded => {
var currentConfig = loaded as PlantControllerConfig;
this.setConfig(currentConfig);
//sync json view initially
this.configChanged();
})
}
uploadConfig(json: string, statusCallback: (status: string) => void) {
fetch(PUBLIC_URL + "/set_config", {
method: "POST",
body: json,
})
.then(response => response.text())
.then(text => statusCallback(text))
}
readonly timeView: TimeView;
readonly plantViews: PlantViews;
readonly networkView: NetworkConfigView;
readonly tankView: TankConfigView;
readonly nightLampView: NightLampView;
readonly submitView: SubmitView;
constructor() {
this.timeView = new TimeView(this)
this.plantViews = new PlantViews(this)
this.networkView = new NetworkConfigView(this, PUBLIC_URL)
this.tankView = new TankConfigView(this)
this.nightLampView = new NightLampView(this)
this.submitView = new SubmitView(this)
}
syncRTCFromBrowser(){
var value: SetTime = {
time: new Date().toISOString()
}
var pretty = JSON.stringify(value, undefined, 1);
fetch(PUBLIC_URL + "/time", {
method: "POST",
body: pretty
})
}
configChanged() {
const current = controller.getConfig();
var pretty = JSON.stringify(current, undefined, 1);
console.log(pretty)
controller.submitView.setJson(pretty);
}
testPlant(plantId: number) {
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(text => console.log(text))
}
updateRealTimeData() {
fetch(PUBLIC_URL + "/data")
.then(response => response.json())
.then(json => json as GetData)
.then(time => {
controller.timeView.update(time.native, time.rtc)
controller.plantViews.update(time.moisture_a, time.moisture_b)
setTimeout(controller.updateRealTimeData, 1000);
})
.catch(error => {
console.log(error);
setTimeout(controller.updateRealTimeData, 10000);
});
}
getConfig(): PlantControllerConfig{
return {
network: controller.networkView.getConfig(),
tank: controller.tankView.getConfig(),
night_lamp: controller.nightLampView.getConfig(),
plants: controller.plantViews.getConfig()
}
}
scanWifi() {
var ajax = new XMLHttpRequest();
ajax.responseType = 'json';
ajax.onreadystatechange = () => {
if (ajax.readyState === 4) {
this.networkView.setScanResult(ajax.response as SSIDList)
}
};
ajax.onerror = (evt) => {
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);
}
}
const controller = new Controller();
setTimeout(controller.updateRealTimeData, 1000);
controller.downloadConfig();
export function uploadFile() {
var file1 = document.getElementById("file1") as HTMLInputElement;
var loaded_n_total = document.getElementById("loaded_n_total") as HTMLElement;
var progressBar = document.getElementById("progressBar") as HTMLProgressElement;
var status = document.getElementById("status") as HTMLElement;
var answer = document.getElementById("answer") as HTMLElement;
if (file1.files == null) {
//TODO error dialog here
return
}
var file = file1.files[0];
var ajax = new XMLHttpRequest();
ajax.upload.addEventListener("progress", event => {
loaded_n_total.innerHTML = "Uploaded " + event.loaded + " bytes of " + event.total;
var percent = (event.loaded / event.total) * 100;
progressBar.value = Math.round(percent);
status.innerHTML = Math.round(percent) + "%";
answer.innerHTML = "in progress";
}, false);
ajax.addEventListener("load", () => {
status.innerHTML = ajax.responseText;
answer.innerHTML = "finished";
progressBar.value = 0;
}, false);
ajax.addEventListener("error", () => {
status.innerHTML = ajax.responseText;
answer.innerHTML = "failed";
}, false);
ajax.addEventListener("abort", () => {
status.innerHTML = ajax.responseText;
answer.innerHTML = "aborted";
}, false);
ajax.open("POST", PUBLIC_URL + "/ota");
ajax.send(file);
}
let file1Upload = document.getElementById("file1") as HTMLInputElement;
file1Upload.onchange = uploadFile;
let firmware_buildtime = document.getElementById("firmware_buildtime") as HTMLDivElement;
let firmware_githash = document.getElementById("firmware_githash") as HTMLDivElement;
document.addEventListener('DOMContentLoaded', function () {
fetch(PUBLIC_URL + "/version")
.then(response => response.json())
.then(json => json as VersionInfo)
.then(versionInfo => {
firmware_buildtime.innerText = versionInfo.build_time;
firmware_githash.innerText = versionInfo.git_hash;
})
}, false);
let battery_flash_button = document.getElementById("battery_flash_button") as HTMLButtonElement;
let battery_flash_file = document.getElementById("battery_flash_file") as HTMLInputElement;
let battery_flash_message = document.getElementById("battery_flash_message") as HTMLElement;
let battery_flash_progressBar = document.getElementById("battery_flash_progressBar") as HTMLProgressElement;
let battery_flash_loaded_n_total = document.getElementById("battery_flash_loaded_n_total") as HTMLElement;
let battery_flash_status = document.getElementById("battery_flash_status") as HTMLElement;
var ajax = new XMLHttpRequest();
ajax.upload.addEventListener("progress", event => {
battery_flash_loaded_n_total.innerHTML = "Uploaded " + event.loaded + " bytes of " + event.total;
var percent = (event.loaded / event.total) * 100;
battery_flash_progressBar.value = Math.round(percent);
battery_flash_status.innerHTML = Math.round(percent) + "%";
battery_flash_message.innerHTML = "in progress";
}, false);
ajax.addEventListener("load", () => {
battery_flash_status.innerHTML = ajax.responseText;
battery_flash_message.innerHTML = "finished";
battery_flash_progressBar.value = 0;
}, false);
ajax.addEventListener("error", () => {
battery_flash_status.innerHTML = ajax.responseText;
battery_flash_message.innerHTML = "failed";
}, false);
ajax.addEventListener("abort", () => {
battery_flash_status.innerHTML = ajax.responseText;
battery_flash_message.innerHTML = "aborted";
}, false);
battery_flash_button.onclick = async function () {
//ajax.open("POST", "/flashbattery");
//ajax.send(battery_flash_file.files[0]);
ajax.open("POST", PUBLIC_URL + "/flashbattery?flash=true");
ajax.send(battery_flash_file.files![0]);
};

View File

@ -0,0 +1,157 @@
<div class="container-xxl">
<link rel="stylesheet" href="bootstrap-grid.css">
<style>
.progressPane{
display: block;
position: fixed;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
top: 0;
left: 0;
background-color: lightgrey;
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;
position: relative;
}
.progress:after {
content: attr(data-label);
font-size: 0.8em;
position: absolute;
text-align: center;
top: 5px;
left: 0;
right: 0;
}
.progress .value {
background-color: #7cc4ff;
display: inline-block;
height: 100%;
}
.progress .valueIndeterminate {
background-color: #7cc4ff;
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);
}
}
</style>
<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>
</div>
<input type="button" id="test" value="Test">
<h2>Current Firmware</h2>
<div>
<div id="firmware_buildtime">Buildtime loading</div>
<div id="firmware_githash">Build githash loading</div>
</div>
<h2>firmeware OTA v3</h2>
<form id="upload_form" method="post">
<input type="file" name="file1" id="firmware_file"><br>
<progress id="firmware_progressBar" value="0" max="100" style="width:300px;"></progress>
<h3 id="firmware_status"></h3>
<h3 id="firmware_answer"></h3>
<p id="firmware_loaded_n_total"></p>
</form>
<div id="timeview">
</div>
<div id="network_view">
</div>
<h2>config</h2>
<div id="configform">
<h3>Tank:</h3>
<div>
<input type="checkbox" id="tank_sensor_enabled">
Enable Tank Sensor
</div>
<div>
<input type="checkbox" id="tank_allow_pumping_if_sensor_error">
Allow Pumping if Sensor Error
</div>
<div>
<input type="number" min="2" max="500000" id="tank_useable_ml">
Tank Size mL
</div>
<div>
<input type="number" min="1" max="500000" id="tank_warn_percent">
Tank Warn Percent (mapped in relation to empty and full)
</div>
<div>
<input type="number" min="0" max="100" id="tank_empty_percent">
Tank Empty Percent (% max move)
</div>
<div>
<input type="number" min="0" max="100" id="tank_full_percent">
Tank Full Percent (% max move)
</div>
<h3>Light:</h3>
<input type="checkbox" id="night_lamp_enabled" checked="false"> Enable Nightlight
<div>
Start
<select type="time" id="night_lamp_time_start">
</select>
Stop
<select type="time" id="night_lamp_time_end">
</select>
</div>
<div>
<input type="checkbox" id="night_lamp_only_when_dark">
Light only when dark
</div>
<h3>Plants:</h3>
<button id="measure_moisture">Measure Moisture</button>
<div id="plants" class="row"></div>
</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>

View File

@ -0,0 +1,244 @@
declare var PUBLIC_URL: string;
console.log("Url is " + PUBLIC_URL);
document.body.innerHTML = require('./main.html') as string;
import { TimeView } from "./timeview";
import { PlantView, PlantViews } from "./plant";
import { NetworkConfigView } from "./network";
import { NightLampView } from "./nightmode";
import { TankConfigView } from "./tanks";
import { SubmitView } from "./submitView";
import { ProgressView } from "./progress";
import { OTAView } from "./ota";
export class Controller {
updateRTCData() {
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);
});
}
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", () => {
//TODO wait for reboot here!
controller.progressview.removeProgress("ota_upload")
}, 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() {
controller.progressview.addIndeterminate("version", "Getting buildVersion")
fetch(PUBLIC_URL + "/version")
.then(response => response.json())
.then(json => json as VersionInfo)
.then(versionInfo => {
controller.progressview.removeProgress("version")
controller.firmWareView.setVersion(versionInfo);
})
}
downloadConfig() {
controller.progressview.addIndeterminate("get_config", "Downloading Config")
fetch(PUBLIC_URL + "/get_config")
.then(response => response.json())
.then(loaded => {
var currentConfig = loaded as PlantControllerConfig;
this.setConfig(currentConfig);
//sync json view initially
this.configChanged();
controller.progressview.removeProgress("get_config")
})
}
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")
}
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, 1);
console.log(pretty)
controller.submitView.setJson(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(
text => {
clearTimeout(timerId);
controller.progressview.removeProgress("test_pump");
}
)
}
getConfig(): PlantControllerConfig{
return {
network: controller.networkView.getConfig(),
tank: controller.tankView.getConfig(),
night_lamp: controller.nightLampView.getConfig(),
plants: controller.plantViews.getConfig()
}
}
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);
}
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);
});
}
readonly timeView: TimeView;
readonly plantViews: PlantViews;
readonly networkView: NetworkConfigView;
readonly tankView: TankConfigView;
readonly nightLampView: NightLampView;
readonly submitView: SubmitView;
readonly firmWareView : OTAView;
readonly progressview: ProgressView;
constructor() {
this.timeView = new TimeView(this)
this.plantViews = new PlantViews(this)
this.networkView = new NetworkConfigView(this, PUBLIC_URL)
this.tankView = new TankConfigView(this)
this.nightLampView = new NightLampView(this)
this.submitView = new SubmitView(this)
this.firmWareView = new OTAView(this)
this.progressview = new ProgressView(this)
}
}
const controller = new Controller();
controller.updateRTCData();
controller.downloadConfig();
controller.measure_moisture();
controller.version();

View File

@ -0,0 +1,32 @@
<h2>Basic network</h2>
Api Redirection to:
<span id="remote_ip">remote ip</span>
<br>
AccessPoint Mode (or fallback)
<br>
<label for="ap_ssid">AP SSID:</label>
<input type="text" id="ap_ssid" list="ssidlist">
<br>
<br>
Station Mode:
<br>
<label for="ssid">SSID:</label>
<input type="text" id="ssid" list="ssidlist"> <input type="button" id="scan" value="Scan">
<datalist id="ssidlist">
<option value="Not scanned yet">
</datalist>
<br>
<label for="ssid">Password:</label>
<input type="text" id="password">
<br>
<br>
Mqtt Reporting
<br>
<div>
MQTT Url
<input type="text" id="mqtt_url" placeholder="mqtt://192.168.1.1:1883">
</div>
<div>
Base Topic
<input type="text" id="base_topic" placeholder="plants/one">
</div>

View File

@ -1,4 +1,4 @@
import { Controller } from ".";
import { Controller } from "./main";
export class NetworkConfigView {
setScanResult(ssidList: SSIDList) {
@ -17,6 +17,8 @@ export class NetworkConfigView {
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);

View File

@ -1,4 +1,4 @@
import { Controller } from ".";
import { Controller } from "./main";
export class NightLampView {
private readonly night_lamp_only_when_dark: HTMLInputElement;

View File

@ -0,0 +1,28 @@
import { Controller } from "./main";
export class OTAView {
file1Upload: HTMLInputElement;
firmware_buildtime: HTMLDivElement;
firmware_githash: HTMLDivElement;
constructor(controller: Controller) {
this.firmware_buildtime = document.getElementById("firmware_buildtime") as HTMLDivElement;
this.firmware_githash = document.getElementById("firmware_githash") as HTMLDivElement;
const file = document.getElementById("firmware_file") as HTMLInputElement;
this.file1Upload = file
this.file1Upload.onchange = () => {
var selectedFile = file.files?.[0];
if (selectedFile == null) {
//TODO error dialog here
return
}
controller.uploadNewFirmware(selectedFile);
};
}
setVersion(versionInfo: VersionInfo) {
this.firmware_buildtime.innerText = versionInfo.build_time;
this.firmware_githash.innerText = versionInfo.git_hash;
}
}

View File

@ -2,9 +2,22 @@
const PLANT_COUNT = 8;
import { Controller } from ".";
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++) {
@ -27,15 +40,6 @@ export class PlantViews {
plantView.setConfig(plantConfig)
}
}
private readonly plants: PlantView[] = []
private readonly plantsDiv: HTMLDivElement
constructor(syncConfig:Controller) {
this.plantsDiv = document.getElementById("plants") as HTMLDivElement;
for (let plantId = 0; plantId < PLANT_COUNT; plantId++) {
this.plants[plantId] = new PlantView(plantId, this.plantsDiv, syncConfig);
}
}
}
export class PlantView {
@ -62,6 +66,7 @@ export class PlantView {
const template = require('./plant.html') as string;
const plantRaw = template.replaceAll("${plantId}", String(plantId));
this.plantDiv.innerHTML = plantRaw
this.plantDiv.classList.add("col-auto" )
parent.appendChild(this.plantDiv)
this.header = document.getElementById("plant_"+plantId+"_header")!

View 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 = "block"
this.updateView();
}
addProgress(id:string, value:number, displayText:string) {
this.progresses.set(id, new ProgressInfo(displayText,value, false))
this.progressPane.style.display = "block"
this.updateView();
}
removeProgress(id:string){
this.progresses.delete(id)
this.updateView();
}
}

View File

@ -0,0 +1,22 @@
import { Controller } from "./main";
export class SubmitView{
json: HTMLInputElement;
submitFormBtn: HTMLButtonElement;
submit_status: HTMLElement;
constructor(controller: Controller){
this.json = document.getElementById('json') as HTMLInputElement
this.submitFormBtn = document.getElementById("submit") as HTMLButtonElement
this.submit_status = document.getElementById("submit_status") as HTMLElement
this.submitFormBtn.onclick = () => {
controller.uploadConfig(this.json.value, (status:string) => {
this.submit_status.innerHTML = status;
});
}
}
setJson(pretty: string) {
this.json.value = pretty
}
}

View File

@ -1,4 +1,4 @@
import { Controller } from ".";
import { Controller } from "./main";
export class TankConfigView {
private readonly tank_useable_ml: HTMLInputElement;

View File

@ -0,0 +1,7 @@
<h2>Time</h2>
AutoRefresh:<input id="timeview_auto_refresh" type="checkbox">
<div id="timeview_esp_time">Esp time</div>
<div id="timeview_rtc_time">Rtc time</div>
<div id="timeview_browser_time">Rtc time</div>
<div></div>
<button id="timeview_time_upload">Store Browser time into esp and rtc</button>

View File

@ -1,17 +1,33 @@
import { Controller } from ".";
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;
constructor(controller:Controller) {
(document.getElementById("timeview") as HTMLElement).innerHTML = require("./timeview.html")
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) {
@ -19,5 +35,13 @@ export class TimeView {
this.rtc_time.innerText = rtc;
var 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)
}
}
}
}

View File

@ -1,6 +1,8 @@
const webpack = require('webpack');
const CopyPlugin = require("copy-webpack-plugin");
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin');
const CopyPlugin = require("copy-webpack-plugin");
const isDevServer = process.env.WEBPACK_SERVE;
console.log("Dev server is " + isDevServer);
@ -13,31 +15,35 @@ if (isDevServer){
module.exports = {
mode: "development",
entry: ['./src/index.ts'],
entry: ['./src/main.ts'],
devtool: 'inline-source-map',
plugins: [
new CopyPlugin({
patterns: [
{
from: "src/index.html",
to: "index.html"
}
]
}),
new webpack.DefinePlugin({
PUBLIC_URL: JSON.stringify(host),
}),
new webpack.EnvironmentPlugin({
redirect: 'true'
})
}),
new HtmlWebpackPlugin({
alwaysWriteToDisk: true,
title: "PlantCtrl",
}),
new HtmlWebpackHarddiskPlugin(),
new CopyPlugin({
patterns: [
{
from: "public/bootstrap-grid.css",
to: "bootstrap-grid.css",
}
],
}),
],
module: {
rules: [
{
test: /\.html$/,
type: 'asset/source',
},
},
{
test: /\.tsx?$/,
use: 'ts-loader',