feat: implement OTA update functionality with web interface; update partition settings and build configuration
This commit is contained in:
parent
175d614d1f
commit
3c783c9844
@ -1,5 +1,6 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xe000, 0x2000,
|
||||
app0, app, ota_0, 0x10000, 0x280000,
|
||||
spiffs, data, spiffs, 0x290000, 0x170000,
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xe000, 0x2000,
|
||||
app0, app, factory, 0x10000, 0x1E0000,
|
||||
app1, app, ota_0, 0x1F0000, 0x1E0000,
|
||||
spiffs, data, spiffs, 0x3D0000, 0x30000,
|
|
@ -34,9 +34,8 @@ board_build.filesystem = spiffs
|
||||
; Update partition settings
|
||||
board_build.partitions = partitions.csv
|
||||
board_upload.flash_size = 4MB
|
||||
; Remove these as they're now defined in partitions.csv
|
||||
; board_build.spiffs.partition = 2M
|
||||
; board_build.spiffs.upload_size = 2M
|
||||
board_build.flash_mode = dio
|
||||
board_upload.flash_freq = "40m"
|
||||
|
||||
build_flags =
|
||||
-Os
|
||||
@ -45,7 +44,14 @@ build_flags =
|
||||
-DNDEBUG
|
||||
-mtext-section-literals
|
||||
'-D VERSION="${common.version}"'
|
||||
-DESPASYNCHTTPUPDATESERVER_PRETTY
|
||||
-DASYNCWEBSERVER_REGEX
|
||||
-DCORE_DEBUG_LEVEL=1
|
||||
-DCONFIG_ARDUHAL_LOG_COLORS=1
|
||||
-DOTA_DEBUG=1
|
||||
-DARDUINO_RUNNING_CORE=1
|
||||
-DARDUINO_EVENT_RUNNING_CORE=1
|
||||
-DCONFIG_OPTIMIZATION_LEVEL_DEBUG=1
|
||||
-DCONFIG_ESP32_PANIC_PRINT_REBOOT
|
||||
|
||||
extra_scripts =
|
||||
scripts/extra_script.py
|
||||
|
211
src/ota.cpp
211
src/ota.cpp
@ -1,3 +1,214 @@
|
||||
#include <Arduino.h>
|
||||
#include "ota.h"
|
||||
#include <Update.h>
|
||||
#include <SPIFFS.h>
|
||||
#include "commonFS.h"
|
||||
#include <esp_task_wdt.h>
|
||||
#include <esp_int_wdt.h>
|
||||
#include <esp_pthread.h>
|
||||
#include <esp_ota_ops.h>
|
||||
|
||||
// Reduzierter Puffer für Dateioperationen
|
||||
const size_t BUFFER_SIZE = 128;
|
||||
const size_t MIN_FREE_HEAP = 32768; // 32KB minimum free heap
|
||||
|
||||
// Files to backup before update
|
||||
const char* CONFIG_FILES[] = {
|
||||
"/bambu_credentials.json",
|
||||
"/spoolman_url.json"
|
||||
};
|
||||
const int CONFIG_FILES_COUNT = sizeof(CONFIG_FILES) / sizeof(CONFIG_FILES[0]);
|
||||
|
||||
bool backupConfigs() {
|
||||
if (!SPIFFS.begin(true)) {
|
||||
Serial.println("Failed to mount SPIFFS");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < CONFIG_FILES_COUNT; i++) {
|
||||
if (SPIFFS.exists(CONFIG_FILES[i])) {
|
||||
String backupFile = String(CONFIG_FILES[i]) + ".bak";
|
||||
|
||||
if (SPIFFS.exists(backupFile)) {
|
||||
SPIFFS.remove(backupFile);
|
||||
}
|
||||
|
||||
File sourceFile = SPIFFS.open(CONFIG_FILES[i], "r");
|
||||
File destFile = SPIFFS.open(backupFile, "w");
|
||||
|
||||
if (!sourceFile || !destFile) {
|
||||
Serial.printf("Failed to open files for backup: %s\n", CONFIG_FILES[i]);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verwenden Sie einen kleineren Puffer
|
||||
uint8_t* buf = (uint8_t*)malloc(BUFFER_SIZE);
|
||||
if (!buf) {
|
||||
Serial.println("Failed to allocate buffer");
|
||||
sourceFile.close();
|
||||
destFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t len = 0;
|
||||
bool success = true;
|
||||
while ((len = sourceFile.read(buf, BUFFER_SIZE)) > 0) {
|
||||
if (destFile.write(buf, len) != len) {
|
||||
Serial.println("Write failed");
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
free(buf);
|
||||
sourceFile.close();
|
||||
destFile.close();
|
||||
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool restoreConfigs() {
|
||||
if (!SPIFFS.begin(true)) {
|
||||
Serial.println("Failed to mount SPIFFS");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = true;
|
||||
for (int i = 0; i < CONFIG_FILES_COUNT; i++) {
|
||||
String backupFile = String(CONFIG_FILES[i]) + ".bak";
|
||||
if (SPIFFS.exists(backupFile)) {
|
||||
if (SPIFFS.exists(CONFIG_FILES[i])) {
|
||||
SPIFFS.remove(CONFIG_FILES[i]);
|
||||
}
|
||||
|
||||
File sourceFile = SPIFFS.open(backupFile, "r");
|
||||
File destFile = SPIFFS.open(CONFIG_FILES[i], "w");
|
||||
|
||||
if (!sourceFile || !destFile) {
|
||||
Serial.printf("Failed to open files for restore: %s\n", CONFIG_FILES[i]);
|
||||
success = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verwenden Sie einen kleineren Puffer
|
||||
uint8_t* buf = (uint8_t*)malloc(BUFFER_SIZE);
|
||||
if (!buf) {
|
||||
Serial.println("Failed to allocate buffer");
|
||||
sourceFile.close();
|
||||
destFile.close();
|
||||
success = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t len = 0;
|
||||
while ((len = sourceFile.read(buf, BUFFER_SIZE)) > 0) {
|
||||
if (destFile.write(buf, len) != len) {
|
||||
Serial.println("Write failed");
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
free(buf);
|
||||
sourceFile.close();
|
||||
destFile.close();
|
||||
|
||||
SPIFFS.remove(backupFile);
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
||||
static bool updateStarted = false;
|
||||
static size_t totalBytes = 0;
|
||||
|
||||
if (!index) {
|
||||
updateStarted = false;
|
||||
totalBytes = 0;
|
||||
|
||||
// Check minimum heap size
|
||||
if (ESP.getFreeHeap() < MIN_FREE_HEAP) {
|
||||
request->send(500, "text/plain", "Not enough memory available");
|
||||
return;
|
||||
}
|
||||
|
||||
size_t updateSize = request->contentLength();
|
||||
if (updateSize == 0) {
|
||||
request->send(400, "text/plain", "Invalid file size");
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("Update size: %u bytes\n", updateSize);
|
||||
Serial.printf("Free space: %u bytes\n", ESP.getFreeSketchSpace());
|
||||
Serial.printf("Free heap: %u bytes\n", ESP.getFreeHeap());
|
||||
|
||||
// Backup configs first
|
||||
if (!backupConfigs()) {
|
||||
request->send(500, "text/plain", "Failed to backup configuration");
|
||||
return;
|
||||
}
|
||||
|
||||
// Close all files and unmount SPIFFS
|
||||
SPIFFS.end();
|
||||
|
||||
// Begin update with minimal configuration
|
||||
if (!Update.begin(updateSize)) {
|
||||
Serial.printf("Update.begin failed: %s\n", Update.errorString());
|
||||
request->send(500, "text/plain", "Failed to start update");
|
||||
return;
|
||||
}
|
||||
|
||||
updateStarted = true;
|
||||
Serial.println("Update process started");
|
||||
}
|
||||
|
||||
if (!updateStarted) {
|
||||
request->send(500, "text/plain", "Update not properly started");
|
||||
return;
|
||||
}
|
||||
|
||||
// Write update data
|
||||
if (Update.write(data, len) != len) {
|
||||
Serial.printf("Update.write failed: %s\n", Update.errorString());
|
||||
Update.abort();
|
||||
request->send(500, "text/plain", "Error during update");
|
||||
return;
|
||||
}
|
||||
|
||||
totalBytes += len;
|
||||
if (totalBytes % (32 * 1024) == 0) {
|
||||
Serial.printf("Progress: %u bytes\n", totalBytes);
|
||||
}
|
||||
|
||||
if (final) {
|
||||
if (!Update.end(true)) {
|
||||
Serial.printf("Update.end failed: %s\n", Update.errorString());
|
||||
request->send(500, "text/plain", "Update failed");
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to restore configs
|
||||
if (!SPIFFS.begin(true)) {
|
||||
Serial.println("Failed to mount SPIFFS for restore");
|
||||
request->send(200, "text/plain", "Update successful but config restore failed. Device will restart...");
|
||||
delay(2000);
|
||||
ESP.restart();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!restoreConfigs()) {
|
||||
Serial.println("Failed to restore configs");
|
||||
}
|
||||
|
||||
request->send(200, "text/plain", "Update successful. Device will restart...");
|
||||
delay(2000);
|
||||
ESP.restart();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
#ifndef OTA_H
|
||||
#define OTA_H
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final);
|
||||
|
||||
#endif
|
104
src/website.cpp
104
src/website.cpp
@ -338,6 +338,110 @@ void setupWebserver(AsyncWebServer &server) {
|
||||
Serial.println("RFID.js gesendet");
|
||||
});
|
||||
|
||||
// Route für OTA Updates
|
||||
server.on("/ota", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("Anfrage für /ota erhalten");
|
||||
|
||||
String html = R"(
|
||||
<!DOCTYPE html>
|
||||
<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,
|
||||
[](AsyncWebServerRequest *request) {
|
||||
// The response will be sent from handleOTAUpload when the upload is complete
|
||||
},
|
||||
handleOTAUpload
|
||||
);
|
||||
|
||||
// Fehlerbehandlung für nicht gefundene Seiten
|
||||
server.onNotFound([](AsyncWebServerRequest *request){
|
||||
Serial.print("404 - Nicht gefunden: ");
|
||||
|
Loading…
x
Reference in New Issue
Block a user