feat: update version to 1.2.2; change OTA upgrade link in HTML files; enhance OTA upload handling with progress updates and JSON responses
This commit is contained in:
parent
3c783c9844
commit
b5c014db86
@ -21,7 +21,7 @@
|
|||||||
<a href="/waage">Scale</a>
|
<a href="/waage">Scale</a>
|
||||||
<a href="/spoolman">Spoolman/Bambu</a>
|
<a href="/spoolman">Spoolman/Bambu</a>
|
||||||
<a href="/about">About</a>
|
<a href="/about">About</a>
|
||||||
<a href="/ota">Upgrade</a>
|
<a href="/upgrade">Upgrade</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="status-container">
|
<div class="status-container">
|
||||||
<div class="status-item">
|
<div class="status-item">
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
<a href="/waage">Scale</a>
|
<a href="/waage">Scale</a>
|
||||||
<a href="/spoolman">Spoolman/Bambu</a>
|
<a href="/spoolman">Spoolman/Bambu</a>
|
||||||
<a href="/about">About</a>
|
<a href="/about">About</a>
|
||||||
<a href="/ota">Upgrade</a>
|
<a href="/upgrade">Upgrade</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="status-container">
|
<div class="status-container">
|
||||||
<div class="status-item">
|
<div class="status-item">
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
<a href="/waage">Scale</a>
|
<a href="/waage">Scale</a>
|
||||||
<a href="/spoolman">Spoolman/Bambu</a>
|
<a href="/spoolman">Spoolman/Bambu</a>
|
||||||
<a href="/about">About</a>
|
<a href="/about">About</a>
|
||||||
<a href="/ota">Upgrade</a>
|
<a href="/upgrade">Upgrade</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="status-container">
|
<div class="status-container">
|
||||||
<div class="status-item">
|
<div class="status-item">
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
<a href="/waage">Scale</a>
|
<a href="/waage">Scale</a>
|
||||||
<a href="/spoolman">Spoolman/Bambu</a>
|
<a href="/spoolman">Spoolman/Bambu</a>
|
||||||
<a href="/about">About</a>
|
<a href="/about">About</a>
|
||||||
<a href="/ota">Upgrade</a>
|
<a href="/upgrade">Upgrade</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="status-container">
|
<div class="status-container">
|
||||||
<div class="status-item">
|
<div class="status-item">
|
||||||
|
@ -1013,4 +1013,78 @@ input[type="submit"]:disabled,
|
|||||||
color: #000;
|
color: #000;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
width: 100%;
|
||||||
|
margin: 20px 0;
|
||||||
|
display: none;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.progress-bar {
|
||||||
|
width: 0%;
|
||||||
|
height: 24px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 24px;
|
||||||
|
color: white;
|
||||||
|
transition: width 0.3s ease-in-out;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
background-color: #ffebee;
|
||||||
|
color: #c62828;
|
||||||
|
border: 1px solid #ef9a9a;
|
||||||
|
}
|
||||||
|
.success {
|
||||||
|
background-color: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
border: 1px solid #a5d6a7;
|
||||||
|
}
|
||||||
|
.update-form {
|
||||||
|
background: var(--primary-color);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
.update-form input[type="file"] {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
width: 80%;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.update-form input[type="submit"] {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.update-form input[type="submit"]:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
.update-form input[type="submit"]:disabled {
|
||||||
|
background-color: #cccccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border: 1px solid #ffe0b2;
|
||||||
|
color: #e65100;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
142
html/upgrade.html
Normal file
142
html/upgrade.html
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
|
||||||
|
<!-- head --><!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>FilaMan - Filament Management Tool</title>
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.ico">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="navbar">
|
||||||
|
<div style="display: flex; align-items: center; gap: 2rem;">
|
||||||
|
<img src="/logo.png" alt="FilaMan Logo" class="logo">
|
||||||
|
<div class="logo-text">
|
||||||
|
<h1>FilaMan<span class="version">v1.2.1</span></h1>
|
||||||
|
<h4>Filament Management Tool</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav style="display: flex; gap: 1rem;">
|
||||||
|
<a href="/">Start</a>
|
||||||
|
<a href="/waage">Scale</a>
|
||||||
|
<a href="/spoolman">Spoolman/Bambu</a>
|
||||||
|
<a href="/about">About</a>
|
||||||
|
<a href="/upgrade">Upgrade</a>
|
||||||
|
</nav>
|
||||||
|
<div class="status-container">
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-dot" id="bambuDot"></span>B
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-dot" id="spoolmanDot"></span>S
|
||||||
|
</div>
|
||||||
|
<div class="ram-status" id="ramStatus"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- head -->
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<h1>Firmware Upgrade</h1>
|
||||||
|
|
||||||
|
<div class="warning">
|
||||||
|
<strong>Warning:</strong> Please do not turn off or restart the device during the update.
|
||||||
|
Configuration files will be automatically backed up and restored after the update.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="update-form" style="align-items: center;">
|
||||||
|
<form id="updateForm" enctype='multipart/form-data' style="text-align: center;"></form>
|
||||||
|
<input type='file' name='update' accept='.bin' required>
|
||||||
|
<input type='submit' value='Firmware Update starten'>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress-container">
|
||||||
|
<div class="progress-bar">0%</div>
|
||||||
|
</div>
|
||||||
|
<div class="status"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('updateForm').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = e.target;
|
||||||
|
const file = form.update.files[0];
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('update', file);
|
||||||
|
|
||||||
|
const progress = document.querySelector('.progress-bar');
|
||||||
|
const progressContainer = document.querySelector('.progress-container');
|
||||||
|
const status = document.querySelector('.status');
|
||||||
|
|
||||||
|
progressContainer.style.display = 'block';
|
||||||
|
status.style.display = 'none';
|
||||||
|
status.className = 'status';
|
||||||
|
form.querySelector('input[type=submit]').disabled = true;
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', '/update', true);
|
||||||
|
|
||||||
|
xhr.upload.onprogress = (e) => {
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
const percentComplete = (e.loaded / e.total) * 100;
|
||||||
|
progress.style.width = percentComplete + '%';
|
||||||
|
progress.textContent = Math.round(percentComplete) + '%';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onload = function() {
|
||||||
|
try {
|
||||||
|
let response = this.responseText;
|
||||||
|
try {
|
||||||
|
// Versuche als JSON zu parsen
|
||||||
|
const jsonResponse = JSON.parse(response);
|
||||||
|
response = jsonResponse.message;
|
||||||
|
|
||||||
|
if (jsonResponse.restart) {
|
||||||
|
// Wenn Gerät neustartet, warte und lade dann neu
|
||||||
|
status.textContent = response + " Weiterleitung in 5 Sekunden...";
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/';
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Wenn kein JSON, nutze response als Text
|
||||||
|
if (!isNaN(response)) {
|
||||||
|
// Wenn es eine Zahl ist, update den Fortschritt
|
||||||
|
const percent = parseInt(response);
|
||||||
|
progress.style.width = percent + '%';
|
||||||
|
progress.textContent = percent + '%';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
status.textContent = response;
|
||||||
|
status.classList.add(xhr.status === 200 ? 'success' : 'error');
|
||||||
|
status.style.display = 'block';
|
||||||
|
|
||||||
|
if (xhr.status !== 200) {
|
||||||
|
form.querySelector('input[type=submit]').disabled = false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
status.textContent = 'Fehler: ' + error.message;
|
||||||
|
status.classList.add('error');
|
||||||
|
status.style.display = 'block';
|
||||||
|
form.querySelector('input[type=submit]').disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onerror = function() {
|
||||||
|
status.textContent = 'Update fehlgeschlagen: Netzwerkfehler';
|
||||||
|
status.classList.add('error');
|
||||||
|
status.style.display = 'block';
|
||||||
|
form.querySelector('input[type=submit]').disabled = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.send(formData);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -21,7 +21,7 @@
|
|||||||
<a href="/waage">Scale</a>
|
<a href="/waage">Scale</a>
|
||||||
<a href="/spoolman">Spoolman/Bambu</a>
|
<a href="/spoolman">Spoolman/Bambu</a>
|
||||||
<a href="/about">About</a>
|
<a href="/about">About</a>
|
||||||
<a href="/ota">Upgrade</a>
|
<a href="/upgrade">Upgrade</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="status-container">
|
<div class="status-container">
|
||||||
<div class="status-item">
|
<div class="status-item">
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
<a href="/waage">Scale</a>
|
<a href="/waage">Scale</a>
|
||||||
<a href="/spoolman">Spoolman/Bambu</a>
|
<a href="/spoolman">Spoolman/Bambu</a>
|
||||||
<a href="/about">About</a>
|
<a href="/about">About</a>
|
||||||
<a href="/ota">Upgrade</a>
|
<a href="/upgrade">Upgrade</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="status-container">
|
<div class="status-container">
|
||||||
<div class="status-item">
|
<div class="status-item">
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
; https://docs.platformio.org/page/projectconf.html
|
; https://docs.platformio.org/page/projectconf.html
|
||||||
|
|
||||||
[common]
|
[common]
|
||||||
version = "1.2.1"
|
version = "1.2.2"
|
||||||
|
|
||||||
[env:esp32dev]
|
[env:esp32dev]
|
||||||
platform = espressif32
|
platform = espressif32
|
||||||
|
23
src/ota.cpp
23
src/ota.cpp
@ -127,10 +127,12 @@ bool restoreConfigs() {
|
|||||||
void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
||||||
static bool updateStarted = false;
|
static bool updateStarted = false;
|
||||||
static size_t totalBytes = 0;
|
static size_t totalBytes = 0;
|
||||||
|
static size_t totalSize = 0;
|
||||||
|
|
||||||
if (!index) {
|
if (!index) {
|
||||||
updateStarted = false;
|
updateStarted = false;
|
||||||
totalBytes = 0;
|
totalBytes = 0;
|
||||||
|
totalSize = request->contentLength();
|
||||||
|
|
||||||
// Check minimum heap size
|
// Check minimum heap size
|
||||||
if (ESP.getFreeHeap() < MIN_FREE_HEAP) {
|
if (ESP.getFreeHeap() < MIN_FREE_HEAP) {
|
||||||
@ -138,13 +140,12 @@ void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t ind
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t updateSize = request->contentLength();
|
if (totalSize == 0) {
|
||||||
if (updateSize == 0) {
|
|
||||||
request->send(400, "text/plain", "Invalid file size");
|
request->send(400, "text/plain", "Invalid file size");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("Update size: %u bytes\n", updateSize);
|
Serial.printf("Update size: %u bytes\n", totalSize);
|
||||||
Serial.printf("Free space: %u bytes\n", ESP.getFreeSketchSpace());
|
Serial.printf("Free space: %u bytes\n", ESP.getFreeSketchSpace());
|
||||||
Serial.printf("Free heap: %u bytes\n", ESP.getFreeHeap());
|
Serial.printf("Free heap: %u bytes\n", ESP.getFreeHeap());
|
||||||
|
|
||||||
@ -158,7 +159,7 @@ void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t ind
|
|||||||
SPIFFS.end();
|
SPIFFS.end();
|
||||||
|
|
||||||
// Begin update with minimal configuration
|
// Begin update with minimal configuration
|
||||||
if (!Update.begin(updateSize)) {
|
if (!Update.begin(totalSize)) {
|
||||||
Serial.printf("Update.begin failed: %s\n", Update.errorString());
|
Serial.printf("Update.begin failed: %s\n", Update.errorString());
|
||||||
request->send(500, "text/plain", "Failed to start update");
|
request->send(500, "text/plain", "Failed to start update");
|
||||||
return;
|
return;
|
||||||
@ -166,6 +167,9 @@ void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t ind
|
|||||||
|
|
||||||
updateStarted = true;
|
updateStarted = true;
|
||||||
Serial.println("Update process started");
|
Serial.println("Update process started");
|
||||||
|
|
||||||
|
// Send initial progress
|
||||||
|
request->send(200, "text/plain", "0");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!updateStarted) {
|
if (!updateStarted) {
|
||||||
@ -182,8 +186,11 @@ void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t ind
|
|||||||
}
|
}
|
||||||
|
|
||||||
totalBytes += len;
|
totalBytes += len;
|
||||||
if (totalBytes % (32 * 1024) == 0) {
|
|
||||||
Serial.printf("Progress: %u bytes\n", totalBytes);
|
// Send progress update
|
||||||
|
if (!final) {
|
||||||
|
int progress = (totalBytes * 100) / totalSize;
|
||||||
|
request->send(200, "text/plain", String(progress));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (final) {
|
if (final) {
|
||||||
@ -196,7 +203,7 @@ void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t ind
|
|||||||
// Try to restore configs
|
// Try to restore configs
|
||||||
if (!SPIFFS.begin(true)) {
|
if (!SPIFFS.begin(true)) {
|
||||||
Serial.println("Failed to mount SPIFFS for restore");
|
Serial.println("Failed to mount SPIFFS for restore");
|
||||||
request->send(200, "text/plain", "Update successful but config restore failed. Device will restart...");
|
request->send(200, "application/json", "{\"status\": \"success\", \"message\": \"Update successful but config restore failed. Device will restart...\", \"restart\": true}");
|
||||||
delay(2000);
|
delay(2000);
|
||||||
ESP.restart();
|
ESP.restart();
|
||||||
return;
|
return;
|
||||||
@ -206,7 +213,7 @@ void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t ind
|
|||||||
Serial.println("Failed to restore configs");
|
Serial.println("Failed to restore configs");
|
||||||
}
|
}
|
||||||
|
|
||||||
request->send(200, "text/plain", "Update successful. Device will restart...");
|
request->send(200, "application/json", "{\"status\": \"success\", \"message\": \"Update successful! Device will restart...\", \"restart\": true}");
|
||||||
delay(2000);
|
delay(2000);
|
||||||
ESP.restart();
|
ESP.restart();
|
||||||
}
|
}
|
||||||
|
101
src/website.cpp
101
src/website.cpp
@ -338,101 +338,12 @@ void setupWebserver(AsyncWebServer &server) {
|
|||||||
Serial.println("RFID.js gesendet");
|
Serial.println("RFID.js gesendet");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Route für OTA Updates
|
// Route für Firmware Update
|
||||||
server.on("/ota", HTTP_GET, [](AsyncWebServerRequest *request) {
|
server.on("/upgrade", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||||
Serial.println("Anfrage für /ota erhalten");
|
AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/upgrade.html.gz", "text/html");
|
||||||
|
response->addHeader("Content-Encoding", "gzip");
|
||||||
String html = R"(
|
response->addHeader("Cache-Control", CACHE_CONTROL);
|
||||||
<!DOCTYPE html>
|
request->send(response);
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Firmware Update</title>
|
|
||||||
<link rel="stylesheet" type="text/css" href="/style.css">
|
|
||||||
<style>
|
|
||||||
.progress-container {
|
|
||||||
width: 100%;
|
|
||||||
margin: 20px 0;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.progress-bar {
|
|
||||||
width: 0%;
|
|
||||||
height: 20px;
|
|
||||||
background-color: #4CAF50;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 20px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.status {
|
|
||||||
margin: 10px 0;
|
|
||||||
padding: 10px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.error { background-color: #ffebee; color: #c62828; }
|
|
||||||
.success { background-color: #e8f5e9; color: #2e7d32; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h2>Firmware Update</h2>
|
|
||||||
<form id="updateForm" enctype='multipart/form-data'>
|
|
||||||
<input type='file' name='update' accept='.bin' required>
|
|
||||||
<input type='submit' value='Update Firmware'>
|
|
||||||
</form>
|
|
||||||
<div class="progress-container">
|
|
||||||
<div class="progress-bar">0%</div>
|
|
||||||
</div>
|
|
||||||
<div class="status"></div>
|
|
||||||
<script>
|
|
||||||
document.getElementById('updateForm').addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const form = e.target;
|
|
||||||
const file = form.update.files[0];
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('update', file);
|
|
||||||
|
|
||||||
const progress = document.querySelector('.progress-bar');
|
|
||||||
const progressContainer = document.querySelector('.progress-container');
|
|
||||||
const status = document.querySelector('.status');
|
|
||||||
|
|
||||||
progressContainer.style.display = 'block';
|
|
||||||
status.style.display = 'none';
|
|
||||||
status.className = 'status';
|
|
||||||
form.querySelector('input[type=submit]').disabled = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/update', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
onUploadProgress: (e) => {
|
|
||||||
const percent = (e.loaded / e.total) * 100;
|
|
||||||
progress.style.width = percent + '%';
|
|
||||||
progress.textContent = Math.round(percent) + '%';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.text();
|
|
||||||
status.textContent = result;
|
|
||||||
status.classList.add(response.ok ? 'success' : 'error');
|
|
||||||
status.style.display = 'block';
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload();
|
|
||||||
}, 5000);
|
|
||||||
} else {
|
|
||||||
form.querySelector('input[type=submit]').disabled = false;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
status.textContent = 'Update failed: ' + error.message;
|
|
||||||
status.classList.add('error');
|
|
||||||
status.style.display = 'block';
|
|
||||||
form.querySelector('input[type=submit]').disabled = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
)";
|
|
||||||
request->send(200, "text/html", html);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on("/update", HTTP_POST,
|
server.on("/update", HTTP_POST,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user