ESP32_powerMC/firmware/webserver.ino
2024-02-07 21:34:37 +01:00

566 lines
24 KiB
C++

void handleRoot() {
String html = "<html><head><script src='https://code.jquery.com/jquery-3.6.4.min.js'></script>";
html += "<link rel=\"stylesheet\" type=\"text/css\" href=\"/css\">";
html += "<link rel='stylesheet' href='https://unpkg.com/purecss@0.6.2/build/pure-min.css'>";
html += "<title>powerMC (ESP32) "+String(FIRMWARE_VERSION)+"</title></head>";
html += "<body><h1>powerMC (ESP32) "+String(FIRMWARE_VERSION)+"</h1>";
html += "<table><tr><td>";
html += "<p><a href='"+cs_jsonPath+"'>JSON Daten</a></p><br>";
html += "<p><a href='"+cs_demoMode1Path+"'>Start demo mode charge and discharge</a></p>";
html += "<p><a href='"+cs_demoMode2Path+"'>Start demo mode charge to max</a></p>";
html += "<p><a href='"+cs_demoMode3Path+"'>Start demo mode discharge to zero</a></p><br>";
html += "<p><a href='"+cs_resetESPPath+"'>Reset ESP32</a></p>";
html += "<p><a href='"+cs_resetWifiPath+"'>Reset Wifi settings</a></p><br>";
html += "<p><a href='"+cs_checkUpdatePath+"'>Firmware update</a></p><br>";
html += "<p><a href='"+cs_configPath+"'>Config</a></p>";
html += "</td><td>&nbsp;</td><td>";
html += "<p id='busVoltage'>Busvoltage: <span id='busVoltageValue'>0</span> V</p>";
html += "<p id='shuntVoltage'>Shuntvoltage: <span id='shuntVoltageValue'>0</span> V</p>";
html += "<p id='current'>Strom: <span id='currentValue'>0</span> A</p>";
html += "<p id='power'>Leistung: <span id='powerValue'>0</span> W</p>";
html += "<p id='energy'>Energie: <span id='energyValue'>0</span> Wh</p>";
html += "<p id='temp'>Temp: <span id='tempValue'>0</span> &deg;C</p>";
html += "<p id='humidity'>Humidity: <span id='humidityValue'>0</span> %</p>";
html += "<p>Shunt max voltage drop: <span>"+String(globalConfigData.shunt_voltage_drop,0)+"</span> mV</p>";
html += "<p>Shunt max current: <span>"+String(globalConfigData.shunt_max_current,0)+"</span> A</p>";
html += "<p>Current factor: <span>"+String(globalConfigData.current_fact/100,2)+" (div. by 100)</span></p>";
html += "<p>Max capacity: <span>"+String(globalConfigData.max_capacity,0)+"</span> Wh</p>";
html += "<p>Ina226 refresh period: <span>"+String(globalConfigData.time_ina226_refresh,0)+"</span> s</p>";
html += "<p id='errorCode'>Error code: <span id='error'>0</span></p>";
html += "</td></tr></table>";
html += "<script>";
html += "function showNotification(message, isSuccess) {";
html += " var notification = document.createElement('div');";
html += " notification.className = isSuccess ? 'success-notification' : 'error-notification';";
html += " notification.innerHTML = message;";
html += " document.body.appendChild(notification);";
html += " setTimeout(function() {";
html += " document.body.removeChild(notification);";
html += " }, 3000);";
html += "}";
html += "function updateValues() {";
html += "$.ajax({";
html += "url: '/json',";
html += "type: 'GET',";
html += "dataType: 'json',";
html += "success: function(data) {";
html += "$('#currentValue').text(data.current);";
html += "$('#powerValue').text(data.power);";
html += "$('#energyValue').text(data.energy);";
html += "$('#busVoltageValue').text(data.busVoltage);";
html += "$('#shuntVoltageValue').text(data.shuntVoltage);";
html += "$('#tempValue').text(data.temp.toFixed(2));";
html += "$('#humidityValue').text(data.humidity.toFixed(2));";
html += "$('#error').text('0b' + data.errorCode.toString(2).padStart(16, '0'));"; // Hier wird der errorCode als Binärzahl dargestellt
html += "},";
html += "error: function() {";
html += "console.log('Fehler beim Abrufen der Daten.');";
html += "}";
html += "});";
html += "}";
html += "updateValues();";
html += "setInterval(updateValues, " + String(LOOP_DISPLAY_DELAY_MS) + ");";
html += "</script></body></html>";
server.send(200, cs_textHtml, html);
}
void handleJson() {
// JSON-Daten aus globalen Variablen erstellen
DynamicJsonDocument doc(200);
doc[cs_busVoltageID] = globalBusVoltage;
doc[cs_shuntVoltageID] = globalShuntVoltage;
doc[cs_currentID] = globalCurrent;
doc[cs_powerID] = globalPower;
doc[cs_energyID] = globalEnergy;
doc[cs_tempID] = globalTemp;
doc[cs_humidityID] = globalHumidity;
doc[cs_errorCodeID] = globalErrorCode;
doc["shuntValue"] = globalShuntValue;
doc["ina226Status"] = ina226Status;
doc["ina226ShuntValue"] = ina226.getShunt();
doc["ina226AlertFlag"] = ina226.getAlertFlag();
doc["ina226AlertLimit"] = ina226.getAlertLimit();
doc["ina226CurrentLSB"] = ina226.getCurrentLSB();
String jsonData;
serializeJson(doc, jsonData);
server.send(200, cs_applicationJson, jsonData);
}
void handleConfig() {
String html = "<html><head>";
html += "<title>powerMC (ESP32) "+String(FIRMWARE_VERSION)+" - configuration</title>";
html += "<link rel=\"stylesheet\" type=\"text/css\" href=\"/css\">";
html += "<link rel='stylesheet' href='https://unpkg.com/purecss@0.6.2/build/pure-min.css'>";
html += "</head>";
html += "<body><h1>powerMC (ESP32) "+String(FIRMWARE_VERSION)+" - configuration</h1>";
html += "<p><a href='"+cs_rootPath+"'>Main</a></p><br>";
html += "<form id=\"configForm\" action=\"/saveConfig\" method=\"post\">";
html += "<table>";
html += "<tr><td><label for=\"temp_min\">Max capacity:</label></td><td>&nbsp;</td>";
html += "<td class=\"slider-container\"><input type=\"range\" id=\"max_capacity\" name=\"max_capacity\" value=\"" + String(globalConfigData.max_capacity) + "\" min=\"2000\" max=\"10000\" step=\"1\">";
html += "<span class=\"slider-value\" id=\"max_capacity_value\">" + String(globalConfigData.max_capacity, 0) + "</span>Wh</td></tr>";
html += "<tr><td><label for=\"temp_min\">Shunt voltage drop:</label></td><td>&nbsp;</td>";
html += "<td class=\"slider-container\"><input type=\"range\" id=\"shunt_voltage_drop\" name=\"shunt_voltage_drop\" value=\"" + String(globalConfigData.shunt_voltage_drop) + "\" min=\"10\" max=\"85\" step=\"1\">";
html += "<span class=\"slider-value\" id=\"shunt_voltage_drop_value\">" + String(globalConfigData.shunt_voltage_drop, 0) + "</span>mV</td></tr>";
html += "<tr><td><label for=\"shunt_max_current\">Shunt current Max:</label></td><td>&nbsp;</td>";
html += "<td class=\"slider-container\"><input type=\"range\" id=\"shunt_max_current\" name=\"shunt_max_current\" value=\"" + String(globalConfigData.shunt_max_current) + "\" min=\"10\" max=\"200\" step=\"1\">";
html += "<span class=\"slider-value\" id=\"shunt_max_current_value\">" + String(globalConfigData.shunt_max_current, 0) + "</span>A</td></tr>";
html += "<tr><td><label for=\"temp_min\">Temperature Min:</label></td><td>&nbsp;</td>";
html += "<td class=\"slider-container\"><input type=\"range\" id=\"temp_min\" name=\"temp_min\" value=\"" + String(globalConfigData.temp_min) + "\" min=\"0\" max=\"100\" step=\"1\">";
html += "<span class=\"slider-value\" id=\"temp_min_value\">" + String(globalConfigData.temp_min, 0) + "</span>&deg;C</td></tr>";
html += "<tr><td><label for=\"temp_max\">Temperature Max:</label></td><td>&nbsp;</td>";
html += "<td class=\"slider-container\"><input type=\"range\" id=\"temp_max\" name=\"temp_max\" value=\"" + String(globalConfigData.temp_max) + "\" min=\"0\" max=\"100\" step=\"1\">";
html += "<span class=\"slider-value\" id=\"temp_max_value\">" + String(globalConfigData.temp_max, 0) + "</span>&deg;C</td></tr>";
html += "<tr><td><label for=\"humidity_min\">Himidity Min:</label></td><td>&nbsp;</td>";
html += "<td class=\"slider-container\"><input type=\"range\" id=\"humi_min\" name=\"humi_min\" value=\"" + String(globalConfigData.humi_min) + "\" min=\"0\" max=\"100\" step=\"1\">";
html += "<span class=\"slider-value\" id=\"humi_min_value\">" + String(globalConfigData.humi_min, 0) + "</span>%</td></tr>";
html += "<tr><td><label for=\"humidity_max\">Humidity Max:</label></td><td>&nbsp;</td>";
html += "<td class=\"slider-container\"><input type=\"range\" id=\"humi_max\" name=\"humi_max\" value=\"" + String(globalConfigData.humi_max) + "\" min=\"0\" max=\"100\" step=\"1\">";
html += "<span class=\"slider-value\" id=\"humi_max_value\">" + String(globalConfigData.humi_max, 0) + "</span>%</td></tr>";
html += "<tr><td><label for=\"current_min\">Current Min:</label></td><td>&nbsp;</td>";
html += "<td class=\"slider-container\"><input type=\"range\" id=\"current_min\" name=\"current_min\" value=\"" + String(globalConfigData.current_min) + "\" min=\"-100\" max=\"100\" step=\"1\">";
html += "<span class=\"slider-value\" id=\"current_min_value\">" + String(globalConfigData.current_min, 0) + "</span>A</td></tr>";
html += "<tr><td><label for=\"current_max\">Current Max:</label></td><td>&nbsp;</td>";
html += "<td class=\"slider-container\"><input type=\"range\" id=\"current_max\" name=\"current_max\" value=\"" + String(globalConfigData.current_max) + "\" min=\"-100\" max=\"100\" step=\"1\">";
html += "<span class=\"slider-value\" id=\"current_max_value\">" + String(globalConfigData.current_max, 0) + "</span>A</td></tr>";
html += "<tr><td><label for=\"current_fact\">Current correction factor:</label></td><td>&nbsp;</td>";
html += "<td class=\"slider-container\"><input type=\"range\" id=\"current_fact\" name=\"current_fact\" value=\"" + String(globalConfigData.current_fact,0) + "\" min=\"1\" max=\"3000\" step=\"1\">";
html += "<span class=\"slider-value\" id=\"current_fact_value\">" + String(globalConfigData.current_fact/100,2) + "</span>(div. by 100)</td></tr>";
html += "<tr><td><label for=\"time_ina226_refresh\">Ina226 update interval (s):</label></td><td>&nbsp;</td>";
html += "<td class=\"slider-container\"><input type=\"range\" id=\"time_ina226_refresh\" name=\"time_ina226_refresh\" value=\"" + String(globalConfigData.time_ina226_refresh,0) + "\" min=\"10\" max=\"120\" step=\"1\">";
html += "<span class=\"slider-value\" id=\"time_ina226_refresh_value\">" + String(globalConfigData.time_ina226_refresh,0) + "</span>sek.</td></tr>";
html += "</table><br>";
html += "<button type=\"button\" onclick=\"saveConfig()\">Save Config</button></form>";
html += "<button onclick=\"location.reload()\">Reset</button>";
html += "<div id=\"responseMessage\" style=\"display: none;\"></div>";
html += "<script>";
html += "function showNotification(message, isSuccess) {";
html += " var notification = document.createElement('div');";
html += " notification.className = isSuccess ? 'success-notification' : 'error-notification';";
html += " notification.innerHTML = message;";
html += " document.body.appendChild(notification);";
html += " setTimeout(function() {";
html += " document.body.removeChild(notification);";
html += " }, 3000);";
html += "}";
html += "function saveConfig() {var form = document.getElementById('configForm');";
html += "var formData = new FormData(form);";
html += "var jsonData = {};";
html += "formData.forEach(function(value, key){jsonData[key] = value;});";
html += "fetch('/saveConfig', {method: 'PUT',headers: {'Content-Type': 'application/json'},body: JSON.stringify(jsonData)})";
html += ".then(response => response.json())";
html += ".then(data => { showNotification('Successful saved config', data.message.toLowerCase() === 'ok'); })";
html += ".catch(error => { console.error('Error:', error); });}";
html += "document.getElementById('temp_min').addEventListener('input', function() {";
html += "document.getElementById('temp_min_value').innerText = this.value;});";
html += "document.getElementById('temp_max').addEventListener('input', function() {";
html += "document.getElementById('temp_max_value').innerText = this.value;});";
html += "document.getElementById('humi_min').addEventListener('input', function() {";
html += "document.getElementById('humi_min_value').innerText = this.value;});";
html += "document.getElementById('humi_max').addEventListener('input', function() {";
html += "document.getElementById('humi_max_value').innerText = this.value;});";
html += "document.getElementById('current_min').addEventListener('input', function() {";
html += "document.getElementById('current_min_value').innerText = this.value;});";
html += "document.getElementById('current_max').addEventListener('input', function() {";
html += "document.getElementById('current_max_value').innerText = this.value;});";
html += "document.getElementById('current_fact').addEventListener('input', function() {";
html += "document.getElementById('current_fact_value').innerText = this.value / 100.0;});";
html += "document.getElementById('shunt_voltage_drop').addEventListener('input', function() {";
html += "document.getElementById('shunt_voltage_drop_value').innerText = this.value;});";
html += "document.getElementById('shunt_max_current').addEventListener('input', function() {";
html += "document.getElementById('shunt_max_current_value').innerText = this.value;});";
html += "document.getElementById('max_capacity').addEventListener('input', function() {";
html += "document.getElementById('max_capacity_value').innerText = this.value;});";
html += "document.getElementById('time_ina226_refresh').addEventListener('input', function() {";
html += "document.getElementById('time_ina226_refresh_value').innerText = this.value;});";
html += "</script></body></html>";
server.send(200, cs_textHtml, html);
}
void handleSaveConfig() {
if (server.hasArg("plain")) {
Serial.println("Received new config data:");
Serial.println(server.arg("plain"));
// JSON-Payload analysieren
DynamicJsonDocument jsonDoc(1024);
deserializeJson(jsonDoc, server.arg("plain"));
// Parameter überprüfen und in die Konfiguration übertragen
if (jsonDoc.containsKey("temp_min")) {
float temp_min = jsonDoc["temp_min"];
// Hier prüfen und in die globale Konfiguration übertragen
globalConfigData.temp_min = temp_min;
}
if (jsonDoc.containsKey("temp_max")) {
float temp_max = jsonDoc["temp_max"];
// Hier prüfen und in die globale Konfiguration übertragen
globalConfigData.temp_max = temp_max;
}
if (jsonDoc.containsKey("humi_min")) {
float humi_min = jsonDoc["humi_min"];
// Hier prüfen und in die globale Konfiguration übertragen
globalConfigData.humi_min = humi_min;
}
if (jsonDoc.containsKey("humi_max")) {
float humi_max = jsonDoc["humi_max"];
// Hier prüfen und in die globale Konfiguration übertragen
globalConfigData.humi_max = humi_max;
}
if (jsonDoc.containsKey("current_min")) {
float current_min = jsonDoc["current_min"];
// Hier prüfen und in die globale Konfiguration übertragen
globalConfigData.current_min = current_min;
}
if (jsonDoc.containsKey("current_max")) {
float current_max = jsonDoc["current_max"];
// Hier prüfen und in die globale Konfiguration übertragen
globalConfigData.current_max = current_max;
}
if (jsonDoc.containsKey("shunt_voltage_drop")) {
float shunt_voltage_drop = jsonDoc["shunt_voltage_drop"];
// Hier prüfen und in die globale Konfiguration übertragen
globalConfigData.shunt_voltage_drop = shunt_voltage_drop;
}
if (jsonDoc.containsKey("shunt_max_current")) {
float shunt_max_current = jsonDoc["shunt_max_current"];
// Hier prüfen und in die globale Konfiguration übertragen
globalConfigData.shunt_max_current = shunt_max_current;
}
if (jsonDoc.containsKey("max_capacity")) {
float max_capacity = jsonDoc["max_capacity"];
// Hier prüfen und in die globale Konfiguration übertragen
globalConfigData.max_capacity = max_capacity;
}
if (jsonDoc.containsKey("time_ina226_refresh")) {
float time_ina226_refresh = jsonDoc["time_ina226_refresh"];
// Hier prüfen und in die globale Konfiguration übertragen
globalConfigData.time_ina226_refresh = time_ina226_refresh;
}
if (jsonDoc.containsKey("current_fact")) {
float current_fact = jsonDoc["current_fact"];
// Hier prüfen und in die globale Konfiguration übertragen
globalConfigData.current_fact = current_fact;
}
writeConfigToEEPROM();
if (ina226.reset())
{
Serial.println("Ina226 reset");
configureIna226();
checkError();
} else {
Serial.println("Ina226 error: can't reset and reconfigure.");
}
// Schicke eine Bestätigung zurück
server.send(200, "application/json", "{\"message\": \"ok\"}");
} else {
// Ungültige Anfrage
server.send(400, "text/plain", "Invalid request");
}
}
void handleDemoMode1() {
demoMode1 = true;
demoMode2 = false;
demoMode3 = false;
globalBusVoltage = 25.6;
globalEnergy = globalConfigData.max_capacity / 2;
handleRoot();
}
void handleDemoMode2() {
demoMode1 = false;
demoMode2 = true;
demoMode3 = false;
globalBusVoltage = 25.6;
globalEnergy = globalConfigData.max_capacity * 0.97;
handleRoot();
}
void handleDemoMode3() {
demoMode1 = false;
demoMode2 = false;
demoMode3 = true;
globalBusVoltage = 25.6;
globalEnergy = globalConfigData.max_capacity * 0.03;
handleRoot();
}
void handleResetESP() {
String message = "<html><head><title>powerMC (ESP32) "+String(FIRMWARE_VERSION)+" - reset</title>"
"<script>setTimeout(function() { window.location.href = '/'; }, 10000);</script>"
"<link rel=\"stylesheet\" type=\"text/css\" href=\"/css\">"
"<link rel='stylesheet' href='https://unpkg.com/purecss@0.6.2/build/pure-min.css'>"
"</head><body><h1>powerMC (ESP32) "+String(FIRMWARE_VERSION)+" - reset</h1>"
"Rebooting...<br>"
"</body></html>";
server.send(200, cs_textHtml, message);
delay(5000);
// manual reset after restart is required
ESP.restart();
}
void handleResetWifi() {
String message = "<html><head><title>powerMC (ESP32) "+String(FIRMWARE_VERSION)+" - reset WiFi configuration</title>"
"<script>setTimeout(function() { window.location.href = '/'; }, 10000);</script>"
"<link rel=\"stylesheet\" type=\"text/css\" href=\"/css\">"
"<link rel='stylesheet' href='https://unpkg.com/purecss@0.6.2/build/pure-min.css'>"
"</head><body><h1>owerMC (ESP32) "+String(FIRMWARE_VERSION)+" - reset WiFi configuration</h1>"
"Reset WifiManager config, rebooting...<br>"
"</body></html>";
server.send(200, cs_textHtml, message);
// Erase WiFi Credentials, enable, compile, flash, disable and reflash.
wifiManager.resetSettings();
delay(5000);
// manual reset after restart is required
ESP.restart();
}
void handleUpdateFirmware() {
String message = "<html><head><title>powerMC (ESP32) " + String(FIRMWARE_VERSION) + " - firmware update</title>"
"<link rel=\"stylesheet\" type=\"text/css\" href=\"/css\">"
"<link rel='stylesheet' href='https://unpkg.com/purecss@0.6.2/build/pure-min.css'>"
"</head><body><h1>powerMC (ESP32) " + String(FIRMWARE_VERSION) + " - firmware update</h1>"
"<p><a href='"+cs_rootPath+"'>Main</a></p><br>"
"<button onclick='window.location.href=\"/updateFirmwareAction\";'>Update Firmware</button>"
"</body></html>";
server.send(200, cs_textHtml, message);
}
void handleUpdateFirmwareAction() {
if (fwUpdate_isRunning)
{
Serial.println("Firmware-Update is running...");
if (updateStatus == "") {
updateStatus = "Firmware-Update is running...<br>";
}
server.send(200, cs_textHtml, updateStatus);
} else {
String message = "<html><head><title>powerMC (ESP32) " + String(FIRMWARE_VERSION) + " - firmware update</title>"
"<script>"
"function readUpdateFirmwareStatus() {"
" var xhr = new XMLHttpRequest();"
" xhr.onreadystatechange = function() {"
" if (xhr.readyState == 4) {"
" document.getElementById('updateStatus').innerHTML = xhr.responseText;"
" if (xhr.responseText.includes('Firmware update successful')) { "
" setTimeout(function() { "
" window.location.href = '" + cs_resetESPPath + "'; "
" }, 5000); }"
" }"
" };"
" xhr.timeout = 1000;"
" xhr.open('GET', '/readUpdateFirmwareStatus', true);"
" xhr.send();"
"}"
"setInterval(readUpdateFirmwareStatus, 2000);"
"</script>"
"<link rel=\"stylesheet\" type=\"text/css\" href=\"/css\">"
"<link rel='stylesheet' href='https://unpkg.com/purecss@0.6.2/build/pure-min.css'>"
"</head><body><h1>powerMC (ESP32) " + String(FIRMWARE_VERSION) + " - firmware update</h1><br>"
"<p>Updating...</p>"
"<div id='updateStatus'></div>"
"</body></html>";
server.send(200, cs_textHtml, message);
disableWatchdog();
updateStatus = "";
Serial.print(cs_startingFirmwareUpdate);
ESPhttpUpdate.rebootOnUpdate(false);
t_httpUpdate_return ret = ESPhttpUpdate.update(String(FIRMWARE_UPDATE_URL));
fwUpdate_isRunning = true;
Serial.println(cs_finish);
switch (ret) {
case HTTP_UPDATE_FAILED:
Serial.printf("HTTP_UPDATE_FAILED Error (%d): %s\n", ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str());
updateStatus = "<b>Firmware-Update failed.</b><br>";
updateStatus += "<b>Last error: "+String(ESPhttpUpdate.getLastError())+"</b><br>";
updateStatus += "<b>Last error message: "+ESPhttpUpdate.getLastErrorString()+"</b><br>";
fwUpdate_isRunning = false;
enableWatchdog();
break;
case HTTP_UPDATE_NO_UPDATES:
Serial.println(cs_noUpdatesAvailable);
updateStatus = "<b>No Updates available.</b><br>";
fwUpdate_isRunning = false;
enableWatchdog();
break;
case HTTP_UPDATE_OK:
Serial.println(cs_fwUpdSuccess);
updateStatus = "<b>Firmware update successful, resetting in a few seconds.</b><br>";
fwUpdate_isRunning = false;
break;
default: // other
Serial.println(cs_unknownStatus);
updateStatus = "<b>Unknown status.</b><br>";
fwUpdate_isRunning = false;
}
}
}
void handleReadUpdateFirmwareStatus() {
if (fwUpdate_isRunning)
{
Serial.println(cs_fwUpdRunning);
if (updateStatus == "") {
updateStatus = "Firmware-Update is running...<br>";
}
}
server.send(200, cs_textHtml, updateStatus);
}
void handleCSS() {
// "<link rel="stylesheet" type="text/css" href="/css">"
String customCSS = R"(
body {
font-family: Arial, sans-serif;
margin: 20px;
}
h1 {
color: #333;
}
p {
margin: 5px 0;
}
a {
color: #0066cc;
text-decoration: none;
}
#busVoltage,
#current,
#power,
#energy,
#temp,
#humidity,
#errorCode {
margin-bottom: 10px;
}
span {
font-weight: bold;
}
td {
vertical-align: top;
padding: 10px;
}
.success-notification {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: #4CAF50;
color: #fff;
text-align: center;
padding: 10px;
}
.error-notification {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: #f44336;
color: #fff;
text-align: center;
padding: 10px;
}
button {
font-family: Arial, sans-serif;
background-color: #d3d3d3;
color: #000;
padding: 10px 20px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
}
button:active {
background-color: #a9a9a9; /* Dunkleres Grau beim Drücken */
}
)";
server.send(200, cs_textCSS, customCSS);
}