diff --git a/src/CHANGES.md b/src/CHANGES.md index d113abb1..20951e4c 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -2,6 +2,12 @@ (starting from release version `0.5.66`) +## 0.5.93 +* improved web API for `live` +* added dark mode option +* converted all forms to reponsive design +* repaired menu with password protection #720, #716, #709 + ## 0.5.92 * fix mobile menu * fix inverters in select `serial.html` #709 diff --git a/src/config/settings.h b/src/config/settings.h index 0528a902..f674dac4 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -51,6 +51,7 @@ typedef struct { char deviceName[DEVNAME_LEN]; char adminPwd[PWD_LEN]; uint16_t protectionMask; + bool darkMode; // wifi char stationSsid[SSID_LEN]; @@ -292,6 +293,7 @@ class settings { memset(&mCfg, 0, sizeof(settings_t)); mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT; + mCfg.sys.darkMode = false; // restore temp settings if(keepWifi) memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t)); @@ -354,6 +356,7 @@ class settings { obj[F("dev")] = mCfg.sys.deviceName; obj[F("adm")] = mCfg.sys.adminPwd; obj[F("prot_mask")] = mCfg.sys.protectionMask; + obj[F("dark")] = mCfg.sys.darkMode; ah::ip2Char(mCfg.sys.ip.ip, buf); obj[F("ip")] = String(buf); ah::ip2Char(mCfg.sys.ip.mask, buf); obj[F("mask")] = String(buf); ah::ip2Char(mCfg.sys.ip.dns1, buf); obj[F("dns1")] = String(buf); @@ -365,6 +368,7 @@ class settings { snprintf(mCfg.sys.deviceName, DEVNAME_LEN, "%s", obj[F("dev")].as()); snprintf(mCfg.sys.adminPwd, PWD_LEN, "%s", obj[F("adm")].as()); mCfg.sys.protectionMask = obj[F("prot_mask")]; + mCfg.sys.darkMode = obj[F("dark")]; ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as()); ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as()); ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as()); diff --git a/src/defines.h b/src/defines.h index 490f2546..56e02583 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 5 -#define VERSION_PATCH 92 +#define VERSION_PATCH 93 //------------------------------------- typedef struct { diff --git a/src/hm/hmDefines.h b/src/hm/hmDefines.h index 19c35a27..bd12f72b 100644 --- a/src/hm/hmDefines.h +++ b/src/hm/hmDefines.h @@ -29,6 +29,10 @@ const char* const fields[] = {"U_DC", "I_DC", "P_DC", "YieldDay", "YieldWeek", " "active_PowerLimit", /*"reactivePowerLimit","Powerfactor",*/ "LastAlarmCode"}; const char* const notAvail = "n/a"; +const uint8_t fieldUnits[] = {UNIT_V, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_KWH, + UNIT_V, UNIT_A, UNIT_W, UNIT_HZ, UNIT_C, UNIT_NONE, UNIT_PCT, UNIT_PCT, UNIT_VAR, + UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_PCT, UNIT_NONE}; + // mqtt discovery device classes enum {DEVICE_CLS_NONE = 0, DEVICE_CLS_CURRENT, DEVICE_CLS_ENERGY, DEVICE_CLS_PWR, DEVICE_CLS_VOLTAGE, DEVICE_CLS_FREQ, DEVICE_CLS_TEMP}; const char* const deviceClasses[] = {0, "current", "energy", "power", "voltage", "frequency", "temperature"}; diff --git a/src/platformio.ini b/src/platformio.ini index 60245f69..b791cbf1 100644 --- a/src/platformio.ini +++ b/src/platformio.ini @@ -15,6 +15,7 @@ include_dir = . [env] framework = arduino board_build.filesystem = littlefs +upload_speed = 921600 ;build_flags = ; ;;;;; Possible Debug options ;;;;;; diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 77180351..0f18728d 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -24,6 +24,9 @@ #define F(sl) (sl) #endif +const uint8_t acList[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q}; +const uint8_t dcList[] = {FLD_UDC, FLD_IDC, FLD_PDC, FLD_YD, FLD_YT, FLD_IRR}; + template class RestApi { public: @@ -91,8 +94,12 @@ class RestApi { else if(path == "record/alarm") getRecord(root, AlarmData); else if(path == "record/config") getRecord(root, SystemConfigPara); else if(path == "record/live") getRecord(root, RealTimeRunData_Debug); - else - getNotFound(root, F("http://") + request->host() + F("/api/")); + else { + if(path.substring(0, 12) == "inverter/id/") + getInverter(root, request->url().substring(17).toInt()); + else + getNotFound(root, F("http://") + request->host() + F("/api/")); + } //DPRINTLN(DBG_INFO, "API mem usage: " + String(root.memoryUsage())); response->addHeader("Access-Control-Allow-Origin", "*"); @@ -158,7 +165,7 @@ class RestApi { File fp = LittleFS.open("/settings.json", "r"); if(!fp) { DPRINTLN(DBG_ERROR, F("failed to load settings")); - response = request->beginResponse(200, F("application/json"), "{}"); + response = request->beginResponse(200, F("application/json; charset=utf-8"), "{}"); } else { String tmp = fp.readString(); @@ -171,7 +178,7 @@ class RestApi { tmp.remove(i, tmp.indexOf("\"", i)-i); } } - response = request->beginResponse(200, F("application/json"), tmp); + response = request->beginResponse(200, F("application/json; charset=utf-8"), tmp); } response->addHeader("Content-Type", "application/octet-stream"); @@ -182,12 +189,11 @@ class RestApi { } void getGeneric(JsonObject obj) { - obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI(); - obj[F("ts_uptime")] = mApp->getUptime(); - obj[F("menu_prot")] = mApp->getProtection(); - obj[F("menu_maskH")] = ((mConfig->sys.protectionMask >> 8) & 0xff); - obj[F("menu_maskL")] = ((mConfig->sys.protectionMask ) & 0xff); - obj[F("menu_protEn")] = (bool) (strlen(mConfig->sys.adminPwd) > 0); + obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI(); + obj[F("ts_uptime")] = mApp->getUptime(); + obj[F("menu_prot")] = mApp->getProtection(); + obj[F("menu_mask")] = (uint16_t)(mConfig->sys.protectionMask ); + obj[F("menu_protEn")] = (bool) (strlen(mConfig->sys.adminPwd) > 0); #if defined(ESP32) obj[F("esp_type")] = F("ESP32"); @@ -199,6 +205,7 @@ class RestApi { void getSysInfo(JsonObject obj) { obj[F("ssid")] = mConfig->sys.stationSsid; obj[F("device_name")] = mConfig->sys.deviceName; + obj[F("dark_mode")] = (bool)mConfig->sys.darkMode; obj[F("mac")] = WiFi.macAddress(); obj[F("hostname")] = mConfig->sys.deviceName; @@ -312,6 +319,41 @@ class RestApi { obj[F("rstComStop")] = (bool)mConfig->inst.rstValsCommStop; } + void getInverter(JsonObject obj, uint8_t id) { + Inverter<> *iv = mSys->getInverterByPos(id); + if(NULL != iv) { + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + obj[F("id")] = id; + obj[F("enabled")] = (bool)iv->config->enabled; + obj[F("name")] = String(iv->config->name); + obj[F("serial")] = String(iv->config->serial.u64, HEX); + obj[F("version")] = String(iv->getFwVersion()); + obj[F("power_limit_read")] = ah::round3(iv->actPowerLimit); + obj[F("ts_last_success")] = rec->ts; + + JsonArray ch = obj.createNestedArray("ch"); + + // AC + uint8_t pos; + obj[F("ch_name")][0] = "AC"; + JsonArray ch0 = ch.createNestedArray(); + for (uint8_t fld = 0; fld < sizeof(acList); fld++) { + pos = (iv->getPosByChFld(CH0, acList[fld], rec)); + ch0[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; + } + + // DC + for(uint8_t j = 0; j < iv->channels; j ++) { + obj[F("ch_name")][j+1] = iv->config->chName[j]; + JsonArray cur = ch.createNestedArray(); + for (uint8_t fld = 0; fld < sizeof(dcList); fld++) { + pos = (iv->getPosByChFld((j+1), dcList[fld], rec)); + cur[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; + } + } + } + } + void getMqtt(JsonObject obj) { obj[F("broker")] = String(mConfig->mqtt.broker); obj[F("port")] = String(mConfig->mqtt.port); @@ -444,12 +486,28 @@ class RestApi { void getLive(JsonObject obj) { getGeneric(obj.createNestedObject(F("generic"))); - JsonArray invArr = obj.createNestedArray(F("inverter")); - obj["refresh_interval"] = mConfig->nrf.sendInterval; + //JsonArray invArr = obj.createNestedArray(F("inverter")); + obj[F("refresh")] = mConfig->nrf.sendInterval; - uint8_t list[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q}; + for (uint8_t fld = 0; fld < sizeof(acList); fld++) { + obj[F("ch0_fld_units")][fld] = String(units[fieldUnits[acList[fld]]]); + obj[F("ch0_fld_names")][fld] = String(fields[acList[fld]]); + } + for (uint8_t fld = 0; fld < sizeof(dcList); fld++) { + obj[F("fld_units")][fld] = String(units[fieldUnits[dcList[fld]]]); + obj[F("fld_names")][fld] = String(fields[dcList[fld]]); + } Inverter<> *iv; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { + iv = mSys->getInverterByPos(i); + bool parse = false; + if(NULL != iv) + parse = iv->config->enabled; + obj[F("iv")][i] = parse; + } + + /*Inverter<> *iv; uint8_t pos; for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { iv = mSys->getInverterByPos(i); @@ -493,7 +551,7 @@ class RestApi { } } } - } + }*/ } void getRecord(JsonObject obj, uint8_t recType) { diff --git a/src/web/html/api.js b/src/web/html/api.js index 0c96e6c2..e92f326c 100644 --- a/src/web/html/api.js +++ b/src/web/html/api.js @@ -33,23 +33,63 @@ iconSuccess = [ /** * GENERIC FUNCTIONS */ +function ml(tagName, ...args) { + var el = document.createElement(tagName); + if(args[0]) { + for(var name in args[0]) { + if(name.indexOf("on") === 0) { + el.addEventListener(name.substr(2).toLowerCase(), args[0][name], false) + } else { + el.setAttribute(name, args[0][name]); + } + } + } + if (!args[1]) { + return el; + } + return nester(el, args[1]) +} + +function nester(el, n) { + if (typeof n === "string") { + var t = document.createTextNode(n); + el.appendChild(t); + } else if (n instanceof Array) { + for(var i = 0; i < n.length; i++) { + if (typeof n[i] === "string") { + var t = document.createTextNode(n[i]); + el.appendChild(t); + } else if (n[i] instanceof Node){ + el.appendChild(n[i]); + } + } + } else if (n instanceof Node){ + el.appendChild(n) + } + return el; +} function topnav() { toggle("topnav", "mobile"); } function parseNav(obj) { - for(i = 0; i < 7; i++) { + for(i = 0; i < 10; i++) { + if(i == 2) + continue; var l = document.getElementById("nav"+i); if(window.location.pathname == "/" + l.href.split('/').pop()) l.classList.add("active"); if(obj["menu_protEn"]) { if(obj["menu_prot"]) { - if((((obj["menu_mask"] >> i) & 0x01) == 0x01) || (1 == i)) + if(0 == i) l.classList.remove("hide"); - - } else if(0 == i) + else if(i > 2) { + if(((obj["menu_mask"] >> (i-2)) & 0x01) == 0x00) + l.classList.remove("hide"); + } + } else if(0 != i) l.classList.remove("hide"); } else if(i > 1) l.classList.remove("hide"); @@ -72,7 +112,7 @@ function parseRssi(obj) { icon = iconWifi1; else if(obj["wifi_rssi"] <= -70) icon = iconWifi2; - document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "#fff", null, obj["wifi_rssi"])); + document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "wifi", obj["wifi_rssi"])); } function setHide(id, hide) { @@ -201,11 +241,10 @@ function link(dst, text, target=null) { return a; } -function svg(data=null, w=24, h=24, color="#000", cl=null, tooltip=null) { +function svg(data=null, w=24, h=24, cl=null, tooltip=null) { var s = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); s.setAttribute('width', w); s.setAttribute('height', h); - s.setAttribute('fill', color); s.setAttribute('viewBox', '0 0 16 16'); if(null != cl) s.setAttribute('class', cl); if(null != data) { diff --git a/src/web/html/colorBright.css b/src/web/html/colorBright.css new file mode 100644 index 00000000..929828ca --- /dev/null +++ b/src/web/html/colorBright.css @@ -0,0 +1,27 @@ +:root { + --bg: #fff; + --fg: #000; + --fg2: #fff; + + --info: #0000dd; + --warn: #ff7700; + --success: #009900; + + --input-bg: #eee; + + --nav-bg: #333; + --primary: #006ec0; + --primary-hover: #044e86; + --secondary: #0072c8; + --nav-active: #555; + --footer-bg: #282828; + + --total-head-title: #8e5903; + --total-bg: #b06e04; + --iv-head-title: #1c6800; + --iv-head-bg: #32b004; + --ch-head-title: #003c80; + --ch-head-bg: #006ec0; + --ts-head: #333; + --ts-bg: #555; +} diff --git a/src/web/html/colorDark.css b/src/web/html/colorDark.css new file mode 100644 index 00000000..aa98c862 --- /dev/null +++ b/src/web/html/colorDark.css @@ -0,0 +1,27 @@ +:root { + --bg: #222; + --fg: #ccc; + --fg2: #fff; + + --info: #0072c8; + --warn: #ffaa00; + --success: #00bb00; + + --input-bg: #333; + + --nav-bg: #333; + --primary: #004d87; + --primary-hover: #023155; + --secondary: #0072c8; + --nav-active: #555; + --footer-bg: #282828; + + --total-head-title: #555511; + --total-bg: #666622; + --iv-head-title: #115511; + --iv-head-bg: #226622; + --ch-head-title: #112255; + --ch-head-bg: #223366; + --ts-head: #333; + --ts-bg: #555; +} diff --git a/src/web/html/includes/header.html b/src/web/html/includes/header.html index 0531efb6..f38a30f7 100644 --- a/src/web/html/includes/header.html +++ b/src/web/html/includes/header.html @@ -1,3 +1,5 @@ + + diff --git a/src/web/html/includes/nav.html b/src/web/html/includes/nav.html index 95ba005d..f1e1a2bd 100644 --- a/src/web/html/includes/nav.html +++ b/src/web/html/includes/nav.html @@ -6,15 +6,15 @@
- Live - Serial / Control - Settings + Live + Serial / Control + Settings - Update - System + Update + System - REST API - Documentation + REST API + Documentation Login Logout diff --git a/src/web/html/index.html b/src/web/html/index.html index 1c997340..27a46a9c 100644 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -67,12 +67,8 @@ } function parseGeneric(obj) { - // Disclaimer - //if(obj["disclaimer"] == false) sessionStorage.setItem("gDisclaimer", promptFunction()); - /*if(exeOnce){ - parseVersion(obj); + if(exeOnce) parseESP(obj); - }*/ parseRssi(obj); } @@ -124,14 +120,14 @@ var p = div(["none"]); for(var i of obj) { var icon = iconWarn; - var color = "#F70"; + var cl = "icon-warn"; avail = ""; if(false == i["enabled"]) { avail = "disabled"; } else if(false == i["is_avail"]) { icon = iconInfo; - color = "#00d"; + cl = "icon-info"; avail = "not yet available"; } else if(0 == i["ts_last_success"]) { @@ -144,12 +140,12 @@ if(false == i["is_producing"]) avail += "not "; else - color = "#090"; + cl = "icon-success"; avail += "producing"; } p.append( - svg(icon, 20, 20, color, "icon"), + svg(icon, 30, 30, "icon " + cl), span("Inverter #" + i["id"] + ": " + i["name"] + " (v" + i["version"] + ") is " + avail), br() ); @@ -167,22 +163,22 @@ function parseWarnInfo(warn, success) { var p = div(["none"]); for(var w of warn) { - p.append(svg(iconWarn, 20, 20, "#F70", "icon"), span(w), br()); + p.append(svg(iconWarn, 30, 30, "icon icon-warn"), span(w), br()); } for(var i of success) { - p.append(svg(iconSuccess, 20, 20, "#090", "icon"), span(i), br()); + p.append(svg(iconSuccess, 30, 30, "icon icon-success"), span(i), br()); } if(commInfo.length > 0) - p.append(svg(iconInfo, 20, 20, "#00d", "icon"), span(commInfo), br()); + p.append(svg(iconInfo, 30, 30, "icon icon-info"), span(commInfo), br()); if(null != release) { if(getVerInt("{#VERSION}") < getVerInt(release)) - p.append(svg(iconInfo, 20, 20, "#00d", "icon"), span("Update available, current released version: " + release), br()); + p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("Update available, current released version: " + release), br()); else if(getVerInt("{#VERSION}") > getVerInt(release)) - p.append(svg(iconInfo, 20, 20, "#00d", "icon"), span("You are using development version {#VERSION}. In case of issues you may want to try the current stable release: " + release), br()); + p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("You are using development version {#VERSION}. In case of issues you may want to try the current stable release: " + release), br()); else - p.append(svg(iconInfo, 20, 20, "#00d", "icon"), span("You are using the current stable release: " + release), br()); + p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("You are using the current stable release: " + release), br()); } document.getElementById("warn_info").replaceChildren(p); diff --git a/src/web/html/login.html b/src/web/html/login.html index 30531e3b..e790f6a4 100644 --- a/src/web/html/login.html +++ b/src/web/html/login.html @@ -7,11 +7,13 @@
-
+
-

AhoyDTU

- - +

AhoyDTU

+
+
+
+
diff --git a/src/web/html/serial.html b/src/web/html/serial.html index fbb09ed9..da9d2816 100644 --- a/src/web/html/serial.html +++ b/src/web/html/serial.html @@ -8,37 +8,56 @@ {#HTML_NAV}
-
-
- connected: - Uptime: - - -
+
+ +
+
+
connected:
+
Uptime:
+
+ + +
+
+
+

Commands

- - - - - - -
- -
-
+
+
+
Select Inverter
+
+
+
+
Power Limit Command
+
+ +
+
+
+
Power Limit Value
+
+
+
+
+
+
+
+
Control Inverter
+
-
-

Ctrl result: n/a

+
+
+
Ctrl result
+
n/a
diff --git a/src/web/html/setup.html b/src/web/html/setup.html index 9eb9a8bd..a9fa695a 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -20,141 +20,20 @@
-
+ +
+
Device Host Name - - +
+
Device Name
+
+
+
+
Dark Mode
+
+
- - -
-
- WiFi -

Enter the credentials to your prefered WiFi station. After rebooting the device tries to connect with this information.

- -
- - - - - - -
-
- Static IP (optional) -

- Leave fields blank for DHCP
- The following fields are parsed in this format: 192.168.1.1 -

- - - - - - - - - - -
-
- - -
-
- Protection - - - -

Select pages which should be protected by password

-
-
-
- - -
-
- Inverter -

- -

General

- - - - - - - -
- -
- -
-
-
- - -
-
- NTP Server - - - - - - - - -
-
- - -
-
- Sunrise & Sunset -

- Use a decimal separator: '.' (dot) for Latitude and Longitude -

- - - - - - -
- -
-
-
- - -
-
- MQTT - - - - - - - - - - -

Send Inverter data in a fixed interval, even if there is no change. A value of '0' disables the fixed interval. The data is published once it was successfully received from inverter. (default: 0)

- - - - - -
-
- - -
-
+
System Config

Pinout

@@ -163,52 +42,265 @@

Serial Console

- -
- -
- - +
+
print inverter data
+
+
+
+
Serial Debug
+
+
+
+
Interval [s]
+
+
+
+
+ + +
+
+ WiFi +

Enter the credentials to your prefered WiFi station. After rebooting the device tries to connect with this information.

+ +
+
Search Networks
+
+
+ +
+
Avail Networks
+
+ +
+
+
+
SSID
+
+
+
+
Password
+
+
+
+
+ Static IP (optional) +

+ Leave fields blank for DHCP
+ The following fields are parsed in this format: 192.168.4.1 +

+
+
IP Address
+
+
+
+
Submask
+
+
+
+
DNS 1
+
+
+
+
DNS 2
+
+
+
+
Gateway
+
+
+
+
+ + +
+
+ Protection +
+
Admin Password
+
+
+

Select pages which should be protected by password

+
+
+
+ + +
+
+ Inverter +
+
+
+
+
+
+

General

+
+
+
+
Interval [s]
+
+
+
+
Max retries per Payload
+
+
+
+
Reset values and YieldDay at midnight
+
+
+
+
Reset values when inverter polling stops at sunset
+
+
+
+
Reset values when inverter status is 'not available'
+
+
+
+
+ + +
+
+ NTP Server +
+
NTP Server / IP
+
+
+
+
NTP Port
+
+
+
+
set system time
+
+ + + +
+
+
+
+ + +
+
+ Sunrise & Sunset +

Use a decimal separator: '.' (dot) for Latitude and Longitude

+ +
+
Latitude (decimal)
+
+
+
+
Longitude (decimal)
+
+
+
+
Offset (pre sunrise, post sunset)
+
+
+
+
Stop polling inverters during night
+
+
+
+
+ + +
+
+ MQTT +
+
Broker / Server IP
+
+
+
+
Port
+
+
+
+
Username (optional)
+
+
+
+
Password (optional)
+
+
+
+
Topic
+
+
+

Send Inverter data in a fixed interval, even if there is no change. A value of '0' disables the fixed interval. The data is published once it was successfully received from inverter. (default: 0)

+
+
Interval [s]
+
+
+
+
Discovery Config (homeassistant)
+
+ + +
+
-
+
Display Config
- -
- -
- -
- -
+
+
Show Logo
+
+
+
+
Turn off while inverters are offline
+
+
+
+
Enable pixel shifting
+
+
+
+
Rotate 180 degree
+
+
+
+
Contrast
+
+
+ - -

Pinout

-
- - - +
+
Reboot device after successful save
+
+ + +
+
-
+
ERASE SETTINGS (not WiFi) -
+
Upload / Store JSON Settings
- Download settings (JSON file) (only saved values, passwords will be removed!) + Download settings (JSON file) (only saved values, passwords will be removed!)
@@ -343,23 +435,38 @@ document.getElementsByName(id + "Name")[0].value = ""; } + function mlCb(id, des, chk=false) { + var cb = ml("input", {type: "checkbox", id: id, name: id}, ""); + if(chk) + cb.checked = true; + return ml("div", {class: "row mb-3"}, [ + ml("div", {class: "col-8 col-sm-3"}, des), + ml("div", {class: "col-4 col-sm-9"}, cb) + ]); + } + + function mlE(des, e) { + return ml("div", {class: "row mb-3"}, [ + ml("div", {class: "col-12 col-sm-3 my-2"}, des), + ml("div", {class: "col-12 col-sm-9"}, e) + ]); + } + function ivHtml(obj, id) { highestId = id + 1; if(highestId == maxInv) setHide("btnAdd", true); - iv = document.getElementById("inverter"); + + var iv = document.getElementById("inverter"); iv.appendChild(des("Inverter " + id)); id = "inv" + id; - iv.appendChild(lbl(id + "Enable", "Communication Enable")); - var en = inp(id + "Enable", null, null, ["cb"], id + "Enable", "checkbox"); - en.checked = obj["enabled"]; - iv.appendChild(en); - iv.appendChild(br()); - - iv.appendChild(lbl(id + "Addr", "Serial Number (12 digits)*")); var addr = inp(id + "Addr", obj["serial"], 12, ["text"], null, "text", "[0-9]+", "Invalid input"); - iv.appendChild(addr); + iv.append( + mlCb(id + "Enable", "Communication Enable", obj["enabled"]), + mlE("Serial Number (12 digits)*", addr) + ); + ['keyup', 'change'].forEach(function(evt) { addr.addEventListener(evt, (e) => { var serial = addr.value.substring(0,4); @@ -369,9 +476,9 @@ setHide(id+"ModName"+i, true); setHide(id+"YieldCor"+i, true); } - setHide("lbl"+id+"ModPwr", true); - setHide("lbl"+id+"ModName", true); - setHide("lbl"+id+"YieldCor", true); + setHide("row"+id+"ModPwr", true); + setHide("row"+id+"ModName", true); + setHide("row"+id+"YieldCor", true); if(serial.charAt(0) == 1) { if((serial.charAt(1) == 0) || (serial.charAt(1) == 1)) { @@ -391,39 +498,44 @@ setHide(id+"ModName"+i, false); setHide(id+"YieldCor"+i, false); } - setHide("lbl"+id+"ModPwr", false); - setHide("lbl"+id+"ModName", false); - setHide("lbl"+id+"YieldCor", false); + setHide("row"+id+"ModPwr", false); + setHide("row"+id+"ModName", false); + setHide("row"+id+"YieldCor", false); } }) }); - iv.append( - lbl(id + "Name", "Name*"), - inp(id + "Name", obj["name"], 16, ["text"], null, "text", "[A-Za-z0-9./#$%&=+_-]+", "Invalid input") - ); + iv.append(mlE("Name*", inp(id + "Name", obj["name"], 16, ["text"], null, "text", "[A-Za-z0-9./#$%&=+_-]+", "Invalid input"))); for(var j of [ ["ModPwr", "ch_max_power", "Max Module Power (Wp)", 4, "[0-9]+"], ["ModName", "ch_name", "Module Name", 16, null], ["YieldCor", "ch_yield_cor", "Yield Total Correction [kWh]", 16, "[0-9-]+"]]) { + var cl = (re.test(obj["serial"])) ? null : ["hide"]; - iv.appendChild(lbl(null, j[2], cl, "lbl" + id + j[0])); - d = div([j[0]]); + i = 0; - cl = (re.test(obj["serial"])) ? ["text", "sh"] : ["text", "sh", "hide"]; + arrIn = []; for(it of obj[j[1]]) { - d.appendChild(inp(id + j[0] + i, it, j[3], cl, id + j[0] + i, "text", j[4], "Invalid input")); + arrIn.push(ml("div", {class: "col-3 "}, + inp(id + j[0] + i, it, j[3], [], id + j[0] + i, "text", j[4], "Invalid input") + )); i++; } - iv.appendChild(d); + + iv.append( + ml("div", {class: "row mb-2 mb-sm-3", id: "row" + id + j[0]}, [ + ml("div", {class: "col-12 col-sm-3 my-2"}, j[2]), + ml("div", {class: "col-12 col-sm-9"}, + ml("div", {class: "row"}, arrIn) + ) + ]) + ); } + var del = inp(id+"del", "X", 0, ["btn", "btnDel"], id+"del", "button"); del.addEventListener("click", delIv); - iv.append( - lbl(id + "lbldel", "Delete"), - del - ); + iv.append(mlE("Delete", del)); } function ivGlob(obj) { @@ -436,20 +548,18 @@ function parseSys(obj) { for(var i of [["device", "device_name"], ["ssid", "ssid"]]) document.getElementsByName(i[0])[0].value = obj[i[1]]; - var e = document.getElementsByName("adminpwd")[0]; + document.getElementsByName("darkMode")[0].checked = obj["dark_mode"]; + e = document.getElementsByName("adminpwd")[0]; if(!obj["pwd_set"]) e.value = ""; var d = document.getElementById("prot_mask"); - var a = ["Index", "Live", "Serial / Console", "Settings", "Update", "System"] + var a = ["Index", "Live", "Serial / Console", "Settings", "Update", "System"]; + var el = []; for(var i = 0; i < 6; i++) { - var chkd = ((obj["prot_mask"] & (1 << i)) == (1 << i)); - var sp = lbl("protMask" + i, a[i]); - var cb = inp("protMask" + i, null, null, ["cb"], "protMask" + i, "checkbox", null, null, chkd); - if(0 == i) - d.replaceChildren(sp, cb, br()); - else - d.append(sp, cb, br()); + var chk = ((obj["prot_mask"] & (1 << i)) == (1 << i)); + el.push(mlCb("protMask" + i, a[i], chk)) } + d.append(...el); } function parseGeneric(obj) { @@ -495,20 +605,31 @@ var e = document.getElementById("pinout"); pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq'], ['led0', 'pinLed0'], ['led1', 'pinLed1']]; for(p of pins) { - e.appendChild(lbl(p[1], p[0].toUpperCase())); - e.appendChild(sel(p[1], ("ESP8266" == type) ? esp8266pins : esp32pins, obj[p[0]])); + e.append( + ml("div", {class: "row mb-3"}, [ + ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()), + ml("div", {class: "col-12 col-sm-9"}, + sel(p[1], ("ESP8266" == type) ? esp8266pins : esp32pins, obj[p[0]]) + ) + ]) + ); } } function parseRadio(obj) { - var e = document.getElementById("rf24"); - e.appendChild(lbl("rf24Power", "Amplifier Power Level")); - e.appendChild(sel("rf24Power", [ - [0, "MIN"], - [1, "LOW"], - [2, "HIGH"], - [3, "MAX"] - ], obj["power_level"])); + var e = document.getElementById("rf24").append( + ml("div", {class: "row mb-3"}, [ + ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()), + ml("div", {class: "col-12 col-sm-9"}, + sel("rf24Power", [ + [0, "MIN"], + [1, "LOW"], + [2, "HIGH"], + [3, "MAX"] + ], obj["power_level"]) + ) + ]) + ); } function parseSerial(obj) { @@ -524,14 +645,22 @@ var e = document.getElementById("dispPins"); pins = [['SCL / CS', 'pinDisp0'], ['SDA / DC', 'pinDisp1']]; for(p of pins) { - e.appendChild(lbl(p[1], p[0].toUpperCase())); - e.appendChild(sel(p[1], ("ESP8266" == type) ? esp8266pins : esp32pins, obj[p[1]])); + e.append( + ml("div", {class: "row mb-3"}, [ + ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()), + ml("div", {class: "col-12 col-sm-9"}, + sel(p[1], ("ESP8266" == type) ? esp8266pins : esp32pins, obj[p[1]]) + ) + ]) + ); } var opts = [[0, "None"], [1, "Nokia5110"], [2, "SSD1306 0.96\""], [3, "SH1106 1.3\""]]; document.getElementById("dispType").append( - lbl("dispType", "Type"), - sel("dispType", opts, obj["disp_type"]) + ml("div", {class: "row mb-3"}, [ + ml("div", {class: "col-12 col-sm-3 my-2"}, "Type"), + ml("div", {class: "col-12 col-sm-9"}, sel("dispType", opts, obj["disp_type"])) + ]) ); e = document.getElementById("contrast"); @@ -576,11 +705,7 @@ e.value = s.value; } - hiddenInput = document.getElementById("disclaimer") - hiddenInput.value = sessionStorage.getItem("gDisclaimer"); - getAjax("/api/setup", parse); - diff --git a/src/web/html/style.css b/src/web/html/style.css index 384ff1f7..7a4266ee 100644 --- a/src/web/html/style.css +++ b/src/web/html/style.css @@ -4,26 +4,39 @@ html, body { padding: 0; height: 100%; min-height: 100%; + background-color: var(--bg); + color: var(--fg); } h2 { padding-left: 10px; } +span, li, h3, label, fieldset { + color: var(--fg); +} + +fieldset, input[type=submit], .btn { + border-radius: 4px; +} + +#live span { + color: var(--fg2); +} + .topnav { - background-color: #333; + background-color: var(--nav-bg); position: fixed; top: 0; width: 100%; } .topnav a { - color: #fff; + color: var(--fg2); padding: 14px 14px; text-decoration: none; font-size: 17px; display: block; - height: 20px; } #topnav a { @@ -33,18 +46,17 @@ h2 { .topnav a.icon { top: 0; left: 0; - background: #333; + background: var(--nav-bg); display: block; position: absolute; } .topnav a:hover { - background-color: #044e86 !important; - color: #000; + background-color: var(--primary-hover) !important; } .topnav .info { - color: #fff; + color: var(--fg2); position: absolute; right: 24px; top: 5px; @@ -61,8 +73,24 @@ svg.icon { padding: 5px 7px 5px 0px; } +.icon-info { + fill: var(--info); +} + +.icon-warn { + fill: var(--warn); +} + +.icon-success { + fill: var(--success); +} + +.wifi { + fill: var(--fg2); +} + .title { - background-color: #006ec0; + background-color: var(--primary); color: #fff !important; padding-left: 80px !important } @@ -78,7 +106,7 @@ svg.icon { } .topnav .active { - background-color: #555; + background-color: var(--nav-active); } span.seperator { @@ -89,6 +117,197 @@ span.seperator { display: block; } +#content { + max-width: 1140px; +} + +.total-h { + background-color: var(--total-head-title); + color: var(--fg2); +} + +.total-bg { + background-color: var(--total-bg); + color: var(--fg2); +} + +.iv-h { + background-color: var(--iv-head-title); + color: var(--fg2); +} + +.iv-bg { + background-color: var(--iv-head-bg); + color: var(--fg2); +} + +.ch-h { + background-color: var(--ch-head-title); + color: var(--fg2); +} + +.ch-bg { + background-color: var(--ch-head-bg); + color: var(--fg2); +} + +.ts-h { + background-color: var(--ts-head); + color: var(--fg2); +} + +.ts-bg { + background-color: var(--ts-bg); + color: var(--fg2); +} + +.hr { + border-top: 1px solid var(--iv-head-title); + margin: 1rem 0 1rem; +} + +p { + text-align: justify; + font-size: 13pt; + color: var(--fg); +} + +#footer { + background-color: var(--footer-bg); +} + +.row { display: flex; max-width: 100%; flex-wrap: wrap; } +.col { flex: 1 0 0%; } + +.col-1, .col-2, .col-3, .col-4, +.col-5, .col-6, .col-7, .col-8, +.col-9, .col-10, .col-11, .col-12 { flex: 0 0 auto; } + +.col-1 { width: 8.333333333%; } +.col-2 { width: 16.66666667%; } +.col-3 { width: 25%; } +.col-4 { width: 33.33333333%; } +.col-5 { width: 41.66666667%; } +.col-6 { width: 50%; } +.col-7 { width: 58.33333333%; } +.col-8 { width: 66.66666667%; } +.col-9 { width: 75%; } +.col-10 { width: 83.33333333%; } +.col-11 { width: 91.66666667%; } +.col-12 { width: 100%; } + +.p-1 { padding: 0.25rem; } +.p-2 { padding: 0.5rem; } +.p-3 { padding: 1rem; } +.p-4 { padding: 1.5rem; } +.p-5 { padding: 3rem; } + +.px-1 { padding: 0 0.25rem 0 0.25rem; } +.px-2 { padding: 0 0.5rem 0 0.5rem; } +.px-3 { padding: 0 1rem 0 1rem; } +.px-4 { padding: 0 1.5rem 0 1.5rem; } +.px-5 { padding: 0 3rem 0 3rem; } + +.py-1 { padding: 0.25rem 0 0.25rem; } +.py-2 { padding: 0.5rem 0 0.5rem; } +.py-3 { padding: 1rem 0 1rem; } +.py-4 { padding: 1.5rem 0 1.5rem; } +.py-5 { padding: 3rem 0 3rem; } + +.mx-1 { margin: 0 0.25rem 0 0.25rem; } +.mx-2 { margin: 0 0.5rem 0 0.5rem; } +.mx-3 { margin: 0 1rem 0 1rem; } +.mx-4 { margin: 0 1.5rem 0 1.5rem; } +.mx-5 { margin: 0 3rem 0 3rem; } + +.my-1 { margin: 0.25rem 0 0.25rem; } +.my-2 { margin: 0.5rem 0 0.5rem; } +.my-3 { margin: 1rem 0 1rem; } +.my-4 { margin: 1.5rem 0 1.5rem; } +.my-5 { margin: 3rem 0 3rem; } + +.mt-1 { margin-top: 0.25rem } +.mt-2 { margin-top: 0.5rem } +.mt-3 { margin-top: 1rem } +.mt-4 { margin-top: 1.5rem } +.mt-5 { margin-top: 3rem } + +.mb-1 { margin-bottom: 0.25rem } +.mb-2 { margin-bottom: 0.5rem } +.mb-3 { margin-bottom: 1rem } +.mb-4 { margin-bottom: 1.5rem } +.mb-5 { margin-bottom: 3rem } + +.fs-1 { font-size: 3.5rem; } +.fs-2 { font-size: 3rem; } +.fs-3 { font-size: 2.5rem; } +.fs-4 { font-size: 2rem; } +.fs-5 { font-size: 1.75rem; } +.fs-6 { font-size: 1.5rem; } +.fs-7 { font-size: 1.25rem; } +.fs-8 { font-size: 1rem; } +.fs-9 { font-size: 0.75rem; } +.fs-10 { font-size: 0.5rem; } + +.a-r { text-align: right; } +.a-c { text-align: center; } + +.row > * { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +*, ::after, ::before { + box-sizing: border-box; +} + +/* sm */ +@media(min-width: 768px) { + .col-sm-1 { width: 8.333333333%; } + .col-sm-2 { width: 16.66666667%; } + .col-sm-3 { width: 25%; } + .col-sm-4 { width: 33.33333333%; } + .col-sm-5 { width: 41.66666667%; } + .col-sm-6 { width: 50%; } + .col-sm-7 { width: 58.33333333%; } + .col-sm-8 { width: 66.66666667%; } + .col-sm-9 { width: 75%; } + .col-sm-10 { width: 83.33333333%; } + .col-sm-11 { width: 91.66666667%; } + .col-sm-12 { width: 100%; } + + .mb-sm-1 { margin-bottom: 0.25rem } + .mb-sm-2 { margin-bottom: 0.5rem } + .mb-sm-3 { margin-bottom: 1rem } + .mb-sm-4 { margin-bottom: 1.5rem } + .mb-sm-5 { margin-bottom: 3rem } + + .fs-sm-1 { font-size: 3.5rem; } + .fs-sm-2 { font-size: 3rem; } + .fs-sm-3 { font-size: 2.5rem; } + .fs-sm-4 { font-size: 2rem; } + .fs-sm-5 { font-size: 1.75rem; } + .fs-sm-6 { font-size: 1.5rem; } + .fs-sm-7 { font-size: 1.25rem; } + .fs-sm-8 { font-size: 1rem; } +} + +/* md */ +@media(min-width: 992px) { + .col-md-1 { width: 8.333333333%; } + .col-md-2 { width: 16.66666667%; } + .col-md-3 { width: 25%; } + .col-md-4 { width: 33.33333333%; } + .col-md-5 { width: 41.66666667%; } + .col-md-6 { width: 50%; } + .col-md-7 { width: 58.33333333%; } + .col-md-8 { width: 66.66666667%; } + .col-md-9 { width: 75%; } + .col-md-10 { width: 83.33333333%; } + .col-md-11 { width: 91.66666667%; } + .col-md-12 { width: 100%; } +} + #wrapper { min-height: 100%; } @@ -101,7 +320,6 @@ span.seperator { #footer { height: 121px; margin-top: -121px; - background-color: #555; width: 100%; font-size: 13px; } @@ -176,13 +394,6 @@ span.seperator { } } -/** old CSS below **/ - -p { - text-align: justify; - font-size: 13pt; -} - p.lic, p.lic a { font-size: 8pt; color: #999; @@ -191,11 +402,11 @@ p.lic, p.lic a { .des { margin-top: 20px; font-size: 13pt; - color: #006ec0; + color: var(--secondary); } .s_active, .s_collapsible:hover { - background-color: #044e86; + background-color: var(--primary-hover); color: #fff; } @@ -205,34 +416,34 @@ p.lic, p.lic a { } .s_collapsible { - background-color: #006ec0; + background-color: var(--primary); color: white; cursor: pointer; - padding: 18px; + padding: 12px; width: 100%; border: none; text-align: left; outline: none; font-size: 15px; - margin-bottom: 4px; + margin-bottom: 5px; } .subdes { font-size: 12pt; - color: #006ec0; + color: var(--secondary); margin-left: 7px; } .subsubdes { font-size:12pt; - color:#006ec0; + color:var(--secondary); margin: 0 0 7px 12px; } a:link, a:visited { text-decoration: none; font-size: 13pt; - color: #006ec0; + color: var(--secondary); } a:hover, a:focus { @@ -240,14 +451,14 @@ a:hover, a:focus { } a.btn { - background-color: #006ec0; + background-color: var(--primary); color: #fff; padding: 7px 15px 7px 15px; display: inline-block; } a.btn:hover { - background-color: #044e86 !important; + background-color: var(--primary-hover) !important; } input, select { @@ -255,11 +466,13 @@ input, select { font-size: 13pt; } -input.text, select { - width: 70%; +input[type=text], input[type=password], select, input[type=number] { + width: 100%; box-sizing: border-box; - margin-bottom: 10px; border: 1px solid #ccc; + border-radius: 4px; + background-color: var(--input-bg); + color: var(--fg); } input.sh { @@ -272,7 +485,7 @@ input.btnDel { } input.btn { - background-color: #006ec0; + background-color: var(--primary); color: #fff; border: 0px; padding: 7px 20px 7px 20px; @@ -303,10 +516,6 @@ pre { white-space: pre-wrap; } -fieldset { - margin-bottom: 15px; -} - .left { float: left; } @@ -315,88 +524,11 @@ fieldset { float: right; } -div.ch-iv { - width: 100%; - background-color: #32b004; - display: inline-block; - margin-bottom: 15px; - padding-bottom: 20px; - overflow: auto; -} - -div.ch { - width: 220px; - min-height: 350px; - background-color: #006ec0; - display: inline-block; - margin: 0 20px 10px 0px; - overflow: auto; - padding-bottom: 20px; -} - -div.ch-all { - width: 100%; - background-color: #b06e04; - display: inline-block; - margin-bottom: 15px; - padding-bottom: 20px; - overflow: auto; -} - -div.ch .value, div.ch .info, div.ch .head, div.ch-iv .value, div.ch-iv .info, div.ch-iv .head, div.ch-all .value, div.ch-all .info, div.ch-all .head { - color: #fff; - display: block; - width: 100%; - text-align: center; -} - .subgrp { float: left; width: 220px; } -div.ch .unit, div.ch-iv .unit, div.ch-all .unit { - font-size: 19px; - margin-left: 10px; -} - -div.ch .value, div.ch-iv .value, div.ch-all .value { - margin-top: 20px; - font-size: 24px; -} - -div.ch .info, div.ch-iv .info, div.ch-all .info { - margin-top: 3px; - font-size: 10px; -} - -div.ch .head { - background-color: #003c80; - padding: 10px 0 10px 0; -} - -div.ch-all .head { - background-color: #8e5903; - padding: 10px 0 10px 0; -} - -div.ch-iv .head { - background-color: #1c6800; - padding: 10px 0 10px 0; -} - -div.iv { - max-width: 960px; - margin-bottom: 40px; -} - -div.ts { - font-size: 13px; - background-color: #ddd; - border-top: 7px solid #999; - padding: 7px; -} - div.ModPwr, div.ModName, div.YieldCor { width:70%; display: inline-block; @@ -447,104 +579,19 @@ div.hr { } #login { - width: 300px; + width: 450px; height: 200px; border: 1px solid #ccc; - background-color: #eee; + background-color: var(--ts-head); position: absolute; top: 50%; left: 50%; margin-top: -160px; - margin-left: -150px; -} - -#login .pad { - padding: 20px; -} - -#login .pad input { - width: 100%; - padding: 7px 0 7px 0; - border: 0px; - margin-bottom: 10px; + margin-left: -225px; } .head { - background-color: #006ec0; + background-color: var(--primary); color: #fff; } - -.row { display: flex; max-width: 100%; flex-wrap: wrap; } -.col { flex: 1 0 0%; } - -.col-1, .col-2, .col-3, .col-4, -.col-5, .col-6, .col-7, .col-8, -.col-9, .col-10, .col-11, .col-12 { flex: 0 0 auto; } - - -.col-1 { width: 8.333333333%; } -.col-2 { width: 16.66666667%; } -.col-3 { width: 25%; } -.col-4 { width: 33.33333333%; } -.col-5 { width: 41.66666667%; } -.col-6 { width: 50%; } -.col-7 { width: 58.33333333%; } -.col-8 { width: 66.66666667%; } -.col-9 { width: 75%; } -.col-10 { width: 83.33333333%; } -.col-11 { width: 91.66666667%; } -.col-12 { width: 100%; } - -.p-1 { padding: 0.25rem; } -.p-2 { padding: 0.5rem; } -.p-3 { padding: 1rem; } -.p-4 { padding: 1.5rem; } -.p-5 { padding: 3rem; } - -.mt-1 { margin-top: 0.25rem } -.mt-2 { margin-top: 0.5rem } -.mt-3 { margin-top: 1rem } -.mt-4 { margin-top: 1.5rem } -.mt-5 { margin-top: 3rem } - -.mb-1 { margin-bottom: 0.25rem } -.mb-2 { margin-bottom: 0.5rem } -.mb-3 { margin-bottom: 1rem } -.mb-4 { margin-bottom: 1.5rem } -.mb-5 { margin-bottom: 3rem } - -.a-r { text-align: right; } -.a-c { text-align: center; } - -/* sm */ -@media(min-width: 768px) { - .col-sm-1 { width: 8.333333333%; } - .col-sm-2 { width: 16.66666667%; } - .col-sm-3 { width: 25%; } - .col-sm-4 { width: 33.33333333%; } - .col-sm-5 { width: 41.66666667%; } - .col-sm-6 { width: 50%; } - .col-sm-7 { width: 58.33333333%; } - .col-sm-8 { width: 66.66666667%; } - .col-sm-9 { width: 75%; } - .col-sm-10 { width: 83.33333333%; } - .col-sm-11 { width: 91.66666667%; } - .col-sm-12 { width: 100%; } -} - -/* md */ -@media(min-width: 992px) { - .col-md-1 { width: 8.333333333%; } - .col-md-2 { width: 16.66666667%; } - .col-md-3 { width: 25%; } - .col-md-4 { width: 33.33333333%; } - .col-md-5 { width: 41.66666667%; } - .col-md-6 { width: 50%; } - .col-md-7 { width: 58.33333333%; } - .col-md-8 { width: 66.66666667%; } - .col-md-9 { width: 75%; } - .col-md-10 { width: 83.33333333%; } - .col-md-11 { width: 91.66666667%; } - .col-md-12 { width: 100%; } -} diff --git a/src/web/html/update.html b/src/web/html/update.html index dd5f4d71..214bc19f 100644 --- a/src/web/html/update.html +++ b/src/web/html/update.html @@ -8,10 +8,13 @@ {#HTML_NAV}
-
- - -
+
+ Select firmware file (*.bin) +
+ + +
+
{#HTML_FOOTER} diff --git a/src/web/html/visualization.html b/src/web/html/visualization.html index a1d11bcc..232eb60a 100644 --- a/src/web/html/visualization.html +++ b/src/web/html/visualization.html @@ -16,6 +16,11 @@ {#HTML_FOOTER}