diff --git a/data/icons/close.svg b/data/icons/close.svg new file mode 100644 index 0000000..c0e3e69 --- /dev/null +++ b/data/icons/close.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/data/icons/hotspot.svg b/data/icons/hotspot.svg new file mode 100644 index 0000000..258ad6a --- /dev/null +++ b/data/icons/hotspot.svg @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/data/icons/lan.svg b/data/icons/lan.svg new file mode 100644 index 0000000..afb4cd5 --- /dev/null +++ b/data/icons/lan.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/data/icons/open.svg b/data/icons/open.svg new file mode 100644 index 0000000..b7e47a3 --- /dev/null +++ b/data/icons/open.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/data/icons/refresh.svg b/data/icons/refresh.svg index e773e95..bdeefb4 100644 --- a/data/icons/refresh.svg +++ b/data/icons/refresh.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/data/icons/signal0.svg b/data/icons/signal0.svg new file mode 100644 index 0000000..ecbd6bc --- /dev/null +++ b/data/icons/signal0.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/data/icons/signal1.svg b/data/icons/signal1.svg new file mode 100644 index 0000000..527cc6a --- /dev/null +++ b/data/icons/signal1.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/data/icons/signal2.svg b/data/icons/signal2.svg new file mode 100644 index 0000000..30e3176 --- /dev/null +++ b/data/icons/signal2.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/data/icons/signal3.svg b/data/icons/signal3.svg new file mode 100644 index 0000000..554b638 --- /dev/null +++ b/data/icons/signal3.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/data/icons/signal4.svg b/data/icons/signal4.svg new file mode 100644 index 0000000..7cd931e --- /dev/null +++ b/data/icons/signal4.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/data/index.html b/data/index.html index c6f81ab..aede861 100644 --- a/data/index.html +++ b/data/index.html @@ -12,6 +12,7 @@ +
@@ -30,183 +31,281 @@

- +
+ Input/Output 2 + - -
-
- Input/Output 1 - - -
-
- Input/Output 2 - - -
-
- Sonstiges - -
-
- - -
- + + +
+ Sonstiges + +
+
+ + +
+ +
diff --git a/data/input-visibility.js b/data/input-visibility.js index d22615f..32445a0 100644 --- a/data/input-visibility.js +++ b/data/input-visibility.js @@ -1,4 +1,4 @@ -const form = document.querySelector("form"); +const form = document.querySelector("form.config"); const dynamicInputs = form.querySelectorAll("[data-field][data-values]"); document.addEventListener("change", updateVisibility); diff --git a/data/load-data.js b/data/load-data.js index b169abf..96c9fe8 100644 --- a/data/load-data.js +++ b/data/load-data.js @@ -4,7 +4,7 @@ import { hideLoadingScreen, } from "./loading-screen.js"; -const form = document.querySelector("form"); +const form = document.querySelector("form.config"); export let data = {}; diff --git a/data/loading-screen.js b/data/loading-screen.js index 4b04769..322c479 100644 --- a/data/loading-screen.js +++ b/data/loading-screen.js @@ -1,11 +1,11 @@ -const form = document.querySelector("form"); +const content = document.querySelector("section.content"); const loadingScreen = document.querySelector(".loading-screen"); const loadingMsg = loadingScreen.querySelector("h2"); const spinner = loadingScreen.querySelector(".spinner"); const reloadBtn = loadingScreen.querySelector(".reload"); export function showLoadingScreen(msg) { - hide(form, reloadBtn); + hide(content, reloadBtn); show(loadingScreen, spinner); loadingMsg.classList.remove("error"); loadingMsg.textContent = msg; @@ -22,7 +22,7 @@ export function showError(msg) { export function hideLoadingScreen() { hide(loadingScreen, reloadBtn); - show(form); + show(content); loadingMsg.classList.remove("error"); loadingMsg.textContent = ""; } diff --git a/data/range-input.js b/data/range-input.js index 4b31c04..7e42f1b 100644 --- a/data/range-input.js +++ b/data/range-input.js @@ -1,4 +1,4 @@ -document.querySelector("form").addEventListener("input", (event) => { +document.querySelector("form.config").addEventListener("input", (event) => { if (event.target.classList.contains("range")) { updateValue(event.target); } diff --git a/data/reset.js b/data/reset.js index c34c41f..ecf29ec 100644 --- a/data/reset.js +++ b/data/reset.js @@ -1,6 +1,6 @@ import { updateConfig } from "/submit.js"; -const form = document.querySelector("form"); +const form = document.querySelector("form.config"); form.addEventListener("reset", async (event) => { event.preventDefault(); diff --git a/data/status.js b/data/status.js new file mode 100644 index 0000000..de00373 --- /dev/null +++ b/data/status.js @@ -0,0 +1,130 @@ +import { data } from "./load-data.js"; + +const statusDialog = document.querySelector(".dialog-status"); +const expandButton = document.querySelector(".expand-status"); + +expandButton.addEventListener("click", () => { + statusDialog.showModal(); +}); + +async function loadStatus() { + try { + const res = await fetch("/status"); + if (!res.ok) { + throw new Error( + `Response status: ${res.status}\n${await res.text()}` + ); + } + + const data = await res.json(); + console.log(data); + + return data; + } catch (e) { + console.error(e); + return null; + } +} + +function setStatus(status) { + setValue("model", status.chip.model); + setValue("mac", formatMac(status.chip.mac)); + setValue("sdk-version", status.sdkVersion); + + setValue("rssi", status.connection.signalStrength); + const icon = selectConnectionIcon(status.connection.signalStrength); + document.querySelectorAll(".connection-icon").forEach((img) => { + img.src = `/icons/${icon}`; + }); + + setValue("cpu-freq", status.chip.cpuFreqMHz); + setValue("cpu-cycle-count", status.chip.cycleCount); + setValue("cpu-temp", status.chip.tempC); + + const usedHeap = status.heap.total - status.heap.free; + setValue("heap-used", formatBytes(usedHeap)); + setValue("heap-total", formatBytes(status.heap.total)); + setValue( + "heap-percentage", + Math.round((usedHeap / status.heap.total) * 100) + ); + + const usedPsram = status.psram.total - status.psram.free; + setValue("psram-used", formatBytes(usedPsram)); + setValue("psram-total", formatBytes(status.psram.total)); + setValue( + "psram-percentage", + Math.round((usedPsram / status.psram.total) * 100) + ); + + setValue("uptime", parseDuration(status.uptime)); + + setValue("hash", parseHash(status.sketch.md5)); +} + +function setValue(className, value) { + document.querySelectorAll("." + className).forEach((element) => { + element.innerText = value; + }); +} + +function parseDuration(ms) { + const date = new Date(ms); + console.log(date); + const time = + date.getUTCHours().toString().padStart(2, "0") + + ":" + + date.getUTCMinutes().toString().padStart(2, "0") + + " h"; + const days = Math.floor(date.getTime() / (1000 * 60 * 60 * 24)); + + return days !== 0 ? `${days} Tage, ${time}` : time; +} + +function parseHash(hash) { + return hash.toUpperCase().substring(0, 16); +} + +function formatBytes(bytes) { + const units = ["Bytes", "KB", "MB", "GB"]; + + let value = bytes; + let index = 0; + while (value >= 1000) { + value /= 1000; + index++; + } + + return `${Math.round(value * 10) / 10} ${units[index]}`; +} + +function formatMac(decimalMac) { + const octets = decimalMac.toString(16).toUpperCase().match(/../g) || []; + return octets.reverse().join(":"); +} + +function selectConnectionIcon(signalStrength) { + // access point + if (data.connection == 1) { + return "hotspot.svg"; + } + + // ethernet + if (data.connection == 2) { + return "lan.svg"; + } + + // station + if (signalStrength >= -50) { + return "signal4.svg"; + } + if (signalStrength >= -60) { + return "signal3.svg"; + } + if (signalStrength >= -70) { + return "signal2.svg"; + } + return "signal1.svg"; +} + +setStatus(await loadStatus()); diff --git a/data/style.css b/data/style.css index e0553be..365f982 100644 --- a/data/style.css +++ b/data/style.css @@ -2,6 +2,7 @@ --color-primary: #087e8b; --color-on-primary: white; --color-background: #222; + --color-surface: #333; --color-danger: #fa2b58; --appended-item-size: 2.5rem; } @@ -151,6 +152,48 @@ label.switch input:checked + .slider::before { translate: -100% -50%; } +dialog { + width: 80%; + max-width: 500px; + max-height: 80%; + overflow: scroll; + background-color: var(--color-background); + color: white; + border: none; + border-radius: 8px; + padding: 16px; +} + +dialog::backdrop { + background-color: #000a; +} + +.dialog-header { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + & button { + margin: 0; + } +} + +.card { + background-color: var(--color-surface); + padding: 8px; + border-radius: 8px; +} + +.card > * { + display: block; +} + +.card > :first-child { + color: var(--color-primary); + margin-bottom: 8px; +} + .buttons { display: flex; flex-direction: row; @@ -246,3 +289,39 @@ div:has(.range-value) { text-align: center; line-height: var(--appended-item-size); } + +.status { + background-color: var(--color-surface); + padding: 8px; + border-radius: 8px; + + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + gap: 16px; +} + +.dialog-status-content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} + +.connection-icon { + width: 24px; + height: 24px; + padding: 8px; +} + +.connection-icon.small { + padding: 0; + height: 1em; +} + +.centered-vertical { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; +} diff --git a/data/submit.js b/data/submit.js index 5607381..ee599e5 100644 --- a/data/submit.js +++ b/data/submit.js @@ -5,7 +5,7 @@ import { showError, } from "./loading-screen.js"; -const form = document.querySelector("form"); +const form = document.querySelector("form.config"); function parseValue(input) { if (input.type === "checkbox") { diff --git a/src/main.cpp b/src/main.cpp index 40b1153..3f587a0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,6 +17,7 @@ #include #include "routes/config.h" #include "routes/networks.h" +#include "routes/status.h" DMXESPSerial dmx1; DMXESPSerial dmx2; @@ -309,6 +310,9 @@ void setup() server.on("/networks", HTTP_GET, [](AsyncWebServerRequest *request) { onGetNetworks(request); }); + server.on("/status", HTTP_GET, [](AsyncWebServerRequest *request) + { onGetStatus(request); }); + server.onRequestBody([](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { if (request->url() == "/config" && request->method() == HTTP_PUT) { diff --git a/src/routes/status.cpp b/src/routes/status.cpp new file mode 100644 index 0000000..e25d21a --- /dev/null +++ b/src/routes/status.cpp @@ -0,0 +1,45 @@ +#include "status.h" + +int getTemperature() +{ + float tempC = -1.0f; + temp_sensor_read_celsius(&tempC); + return static_cast(round(tempC)); +} + +int8_t getWiFiStrength() +{ + try + { + return WiFi.RSSI(); + } + catch (...) + { + return 0; + } +} + +void onGetStatus(AsyncWebServerRequest *request) +{ + JsonDocument doc; + + doc["uptime"] = millis(); + doc["chip"]["model"] = ESP.getChipModel(); + doc["chip"]["mac"] = ESP.getEfuseMac(); + doc["chip"]["revision"] = ESP.getChipRevision(); + doc["chip"]["cpuFreqMHz"] = ESP.getCpuFreqMHz(); + doc["chip"]["cycleCount"] = ESP.getCycleCount(); + doc["chip"]["tempC"] = getTemperature(); + doc["sdkVersion"] = ESP.getSdkVersion(); + doc["sketch"]["size"] = ESP.getSketchSize(); + doc["sketch"]["md5"] = ESP.getSketchMD5(); + doc["heap"]["free"] = ESP.getFreeHeap(); + doc["heap"]["total"] = ESP.getHeapSize(); + doc["psram"]["free"] = ESP.getFreePsram(); + doc["psram"]["total"] = ESP.getPsramSize(); + doc["connection"]["signalStrength"] = getWiFiStrength(); + + String jsonString; + serializeJson(doc, jsonString); + request->send(200, "application/json", jsonString); +} \ No newline at end of file diff --git a/src/routes/status.h b/src/routes/status.h new file mode 100644 index 0000000..03e821b --- /dev/null +++ b/src/routes/status.h @@ -0,0 +1,5 @@ +#include +#include +#include + +void onGetStatus(AsyncWebServerRequest *request); \ No newline at end of file