feat: implement OTA update functionality with web interface; update partition settings and build configuration

This commit is contained in:
Manuel Weiser 2025-02-18 11:42:52 +01:00
parent 175d614d1f
commit 3c783c9844
5 changed files with 335 additions and 11 deletions

View File

@ -1,5 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags # Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000, nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000, otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x280000, app0, app, factory, 0x10000, 0x1E0000,
spiffs, data, spiffs, 0x290000, 0x170000, app1, app, ota_0, 0x1F0000, 0x1E0000,
spiffs, data, spiffs, 0x3D0000, 0x30000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 factory 0x10000 0x280000 0x1E0000
5 spiffs app1 data app spiffs ota_0 0x290000 0x1F0000 0x170000 0x1E0000
6 spiffs data spiffs 0x3D0000 0x30000

View File

@ -34,9 +34,8 @@ board_build.filesystem = spiffs
; Update partition settings ; Update partition settings
board_build.partitions = partitions.csv board_build.partitions = partitions.csv
board_upload.flash_size = 4MB board_upload.flash_size = 4MB
; Remove these as they're now defined in partitions.csv board_build.flash_mode = dio
; board_build.spiffs.partition = 2M board_upload.flash_freq = "40m"
; board_build.spiffs.upload_size = 2M
build_flags = build_flags =
-Os -Os
@ -45,7 +44,14 @@ build_flags =
-DNDEBUG -DNDEBUG
-mtext-section-literals -mtext-section-literals
'-D VERSION="${common.version}"' '-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 = extra_scripts =
scripts/extra_script.py scripts/extra_script.py

View File

@ -1,3 +1,214 @@
#include <Arduino.h> #include <Arduino.h>
#include "ota.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();
}
}

View File

@ -1,6 +1,8 @@
#ifndef OTA_H #ifndef OTA_H
#define 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 #endif

View File

@ -338,6 +338,110 @@ void setupWebserver(AsyncWebServer &server) {
Serial.println("RFID.js gesendet"); 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 // Fehlerbehandlung für nicht gefundene Seiten
server.onNotFound([](AsyncWebServerRequest *request){ server.onNotFound([](AsyncWebServerRequest *request){
Serial.print("404 - Nicht gefunden: "); Serial.print("404 - Nicht gefunden: ");