feat(ota): ArduinoOTA integration + platformio.ini refactor

- ArduinoOTA in web_server.cpp integriert (Hostname: lasercutter-display)
- WebServerManager::loop() hinzugefuegt -> ArduinoOTA.handle()
- webServer.loop() in main.cpp aufgerufen
- platformio.ini: gemeinsamer [env]-Basisblock (lib_deps einmalig)
- Neues env az-delivery-devkit-v4-ota (espota, upload_flags --auth Anleitung)
- Neues env az-delivery-devkit-v4-ota-http (ElegantOTA HTTP Fallback)
- upload_ota.py: HTTP-Multipart-Upload Script fuer ElegantOTA
- README.md: Abschnitt Via WiFi (OTA) mit Passwort-Anleitung
- Implementation-Plan.md: Task 9.9 dokumentiert
This commit is contained in:
MaPaLo76 2026-02-26 21:35:55 +01:00
parent 974616aee2
commit 6bef93210e
7 changed files with 174 additions and 11 deletions

View File

@ -305,6 +305,18 @@
- Display: rate-limited (W 500 ms-Blinker, Minuten bei Änderung/60 s, Sekunden 1 s, M bei Statuswechsel)
- Verifiziert: LaserTracker läuft sofort nach Neustart, unabhängig vom WiFi-Status ✅
- [x] **9.9** ArduinoOTA Integration + platformio.ini Refaktorierung
- `ArduinoOTA` in `web_server.cpp` integriert (ESP32 built-in, kein lib_deps-Eintrag nötig)
- Hostname: `lasercutter-display` → erscheint in Arduino IDE als Netzwerk-Port `lasercutter-display at <IP>`
- Passwort: `ArduinoOTA.setPassword(cfg.webPassword)` gleiche Auth wie Webinterface, kein separates OTA-Passwort
- `WebServerManager::loop()` hinzugefügt → `ArduinoOTA.handle()` wird in `main.cpp` via `webServer.loop()` aufgerufen
- `platformio.ini` auf gemeinsamen `[env]`-Basisblock umgestellt (lib_deps + build_flags einmalig, alle Environments erben)
- Neues Environment `az-delivery-devkit-v4-ota`: `upload_protocol = espota`, IP konfigurierbar
- Neues Environment `az-delivery-devkit-v4-ota-http`: ElegantOTA HTTP-Fallback via `upload_ota.py` extra_script
- `upload_flags = --auth=...`: Kommentar + Anleitung für Passwort-Übergabe an espota (inkl. Umgebungsvariable-Option)
- OTA-Passwort-Authentifizierung verifiziert: Upload via `pio run -e az-delivery-devkit-v4-ota --target upload` erfolgreich ✅
- README.md: Abschnitt "Via WiFi (OTA)" mit vollständiger Anleitung ergänzt
---
## Phase 10 Dokumentation & Abschluss

View File

@ -334,6 +334,8 @@ Firmware-Updates können kabellos über die Weboberfläche unter `/update` einge
## Build & Flash
### Via USB (Standard)
```bash
# Haupt-Firmware bauen und flashen
pio run -e az-delivery-devkit-v4 --target upload
@ -346,6 +348,46 @@ Ziel-Board: `az-delivery-devkit-v4` (ESP32), Upload-Port: `COM3`
---
### Via WiFi (OTA)
Sobald das Gerät einmal per USB geflasht wurde und im WLAN erreichbar ist, kann jeder weitere Flash-Vorgang kabellos erfolgen.
**Voraussetzung:** ESP32 läuft und ist im WLAN verbunden.
**1. IP-Adresse in `platformio.ini` eintragen** (einmalig):
```ini
[env:az-delivery-devkit-v4-ota]
upload_port = 192.168.2.62 ; <-- hier die IP des ESP32 eintragen
```
Die aktuelle IP ist im Router (DHCP-Tabelle) oder über mDNS (`lasercutter-display.local`) im Browser erreichbar.
**2. Flashen via PlatformIO:**
```bash
pio run -e az-delivery-devkit-v4-ota --target upload
```
**3. Flashen via Arduino IDE:**
Im Menü **Tools → Port** erscheint nach ein paar Sekunden automatisch ein Netzwerk-Port:
```
lasercutter-display at 192.168.x.x
```
Diesen auswählen und normal über **Sketch → Hochladen** flashen.
> **Hinweis:** Falls ein Web-Passwort gesetzt ist, verwendet ArduinoOTA dasselbe Passwort. Es muss in `platformio.ini` per `upload_flags` übergeben werden:
> ```ini
> [env:az-delivery-devkit-v4-ota]
> upload_flags = --auth=DEIN_WEBPASSWORT
> ```
> Arduino IDE fragt das Passwort beim Upload automatisch ab.
> **Wichtig (Erstinbetriebnahme):** Der erste OTA-Flash setzt voraus, dass die aktuelle Firmware bereits ArduinoOTA enthält. Nach einem Neuflash via USB (`az-delivery-devkit-v4`) ist OTA dauerhaft verfügbar.
---
## Projektstruktur
```

View File

@ -30,9 +30,12 @@ class WebServerManager {
public:
WebServerManager();
// Routen registrieren und Server starten (einmalig in setup())
// Routen registrieren und Server starten (einmalig nach WiFi-Connect)
void begin();
// ArduinoOTA verarbeiten in jedem loop()-Durchlauf aufrufen
void loop();
private:
AsyncWebServer* _server; // PIMPL: Pointer, full type only in web_server.cpp

View File

@ -8,27 +8,21 @@
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:az-delivery-devkit-v4]
; =============================================================================
; GEMEINSAME BASIS - wird von allen Environments geerbt
; =============================================================================
[env]
platform = espressif32
board = az-delivery-devkit-v4
framework = arduino
; Upload & Monitor
upload_port = COM3
monitor_speed = 115200
monitor_echo = yes
monitor_filters = esp32_exception_decoder, default
; Partitionsschema: min_spiffs gibt mehr Platz für OTA (2x ~1.8 MB App)
board_build.partitions = min_spiffs.csv
; Build-Flags
build_flags =
-DCORE_DEBUG_LEVEL=1 ; 0=keine, 1=Fehler, 3=Info, 5=Verbose
-DARDUINO_LOOP_STACK_SIZE=8192
-DELEGANTOTA_USE_ASYNC_WEBSERVER=1
; Bibliotheken
lib_deps =
majicDesigns/MD_Parola @ ^3.7.3
majicDesigns/MD_MAX72XX @ ^3.5.1
@ -39,6 +33,40 @@ lib_deps =
bblanchon/ArduinoJson @ ^7.3.0
https://github.com/ayushsharma82/ElegantOTA.git
; =============================================================================
; HAUPT-ENVIRONMENT - USB Flash & Monitor
; pio run -e az-delivery-devkit-v4 --target upload
; =============================================================================
[env:az-delivery-devkit-v4]
upload_port = COM3
; =============================================================================
; OTA ENVIRONMENT Firmware via WiFi flashen (ArduinoOTA / espota)
; Erscheint in Arduino IDE als Netzwerk-Port: lasercutter-display.local
; PlatformIO: pio run -e az-delivery-devkit-v4-ota --target upload
; IP anpassen: upload_port = <IP des ESP32> (oder lasercutter-display.local)
;
; Falls ein Web-Passwort gesetzt ist, muss es hier eingetragen werden:
; upload_flags = --auth=DEIN_WEBPASSWORT
; Oder als Umgebungsvariable (empfohlen, damit das Passwort nicht im Repo landet):
; Vor dem Upload: set LASERCUTTER_OTA_PW=DEIN_PASSWORT (Windows)
; upload_flags = --auth=${sysenv.LASERCUTTER_OTA_PW}
; =============================================================================
[env:az-delivery-devkit-v4-ota]
upload_port = 192.168.2.62
upload_protocol = espota
; upload_flags = --auth=DEIN_WEBPASSWORT ; <-- auskommentieren und anpassen wenn Passwort gesetzt
; =============================================================================
; OTA ENVIRONMENT (HTTP) Firmware via ElegantOTA Webinterface
; Nur als Fallback falls ArduinoOTA nicht erreichbar
; pio run -e az-delivery-devkit-v4-ota-http --target upload
; =============================================================================
[env:az-delivery-devkit-v4-ota-http]
upload_port = 192.168.2.62
upload_protocol = custom
extra_scripts = upload_ota.py
; =============================================================================
; TEST ENVIRONMENT 1.4 Dot-Matrix-Display Verdrahtungstest
; Flash: pio run -e test-display --target upload

View File

@ -112,6 +112,11 @@ void loop() {
}
}
// WebServer-Loop: ArduinoOTA verarbeiten
if (webStarted) {
webServer.loop();
}
// ==========================================================================
// Display: rate-limited (Modul-Updates nur bei Bedarf)
// ==========================================================================

View File

@ -5,6 +5,7 @@
#include "web_server.h"
#include <ESPAsyncWebServer.h> // voller Include nur in .cpp (PIMPL)
#include <ArduinoOTA.h> // ESP32 built-in, kein lib_deps-Eintrag noetig
#include "settings.h"
#include "laser_tracker.h"
#include "mqtt_client.h"
@ -42,12 +43,32 @@ void WebServerManager::begin() {
}
ElegantOTA.begin(_server);
_server->begin();
// ArduinoOTA erscheint als Netzwerk-Port in Arduino IDE und PlatformIO
const Settings& cfg = settings.get();
ArduinoOTA.setHostname("lasercutter-display");
if (cfg.webPassword[0] != '\0') {
ArduinoOTA.setPassword(cfg.webPassword); // gleiche Auth wie Webinterface
}
ArduinoOTA.onStart([]() { LOG_I("OTA", "Start OTA-Update..."); });
ArduinoOTA.onEnd([]() { LOG_I("OTA", "OTA fertig Neustart"); });
ArduinoOTA.onError([](ota_error_t e) { LOG_E("OTA", "Fehler [%u]", e); });
ArduinoOTA.begin();
LOG_I("WEB", "ArduinoOTA aktiv: lasercutter-display.local");
Serial.println(F("[WebServer] gestartet auf Port 80"));
Serial.print(F("[WebServer] URL: http://"));
Serial.println(WiFi.localIP());
LOG_I("WEB", "Auth: %s", s.webPassword[0] ? "aktiv" : "deaktiviert (kein Passwort)");
}
// -----------------------------------------------------------------------------
// loop() ArduinoOTA verarbeiten (in main loop() aufrufen)
// -----------------------------------------------------------------------------
void WebServerManager::loop() {
ArduinoOTA.handle();
}
// -----------------------------------------------------------------------------
// requireAuth() HTTP Basic Auth pruefen (nur wenn Passwort konfiguriert)
// -----------------------------------------------------------------------------

52
upload_ota.py Normal file
View File

@ -0,0 +1,52 @@
# upload_ota.py OTA-Upload via ElegantOTA HTTP-Endpoint
# PlatformIO extra_script: ersetzt den Upload-Befehl durch HTTP-POST an /update
#
# Verwendung: pio run -e az-delivery-devkit-v4-ota --target upload
# IP-Adresse in platformio.ini: upload_port = 192.168.x.x
Import("env")
import urllib.request
import os
def do_ota_upload(source, target, env):
firmware_path = str(source[0])
ip = env.get("UPLOAD_PORT", "")
if not ip:
print("FEHLER: upload_port in platformio.ini nicht gesetzt!")
env.Exit(1)
url = f"http://{ip}/update"
print(f"\n=== OTA Upload ===")
print(f" Firmware : {firmware_path}")
print(f" Ziel : {url}")
print(f" Groesse : {os.path.getsize(firmware_path)} Bytes")
boundary = "ElegantOtaBoundary"
with open(firmware_path, "rb") as f:
firmware_data = f.read()
body = (
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="firmware"; filename="firmware.bin"\r\n'
f"Content-Type: application/octet-stream\r\n\r\n"
).encode() + firmware_data + f"\r\n--{boundary}--\r\n".encode()
req = urllib.request.Request(
url,
data=body,
method="POST",
headers={"Content-Type": f"multipart/form-data; boundary={boundary}"}
)
try:
with urllib.request.urlopen(req, timeout=60) as resp:
result = resp.read().decode("utf-8", errors="ignore")
print(f" Status : {resp.status} {resp.reason}")
print(f" Antwort : {result}")
print("=== Upload erfolgreich - ESP32 startet neu ===\n")
except Exception as e:
print(f"\nFEHLER beim OTA-Upload: {e}")
env.Exit(1)
env.Replace(UPLOADCMD=do_ota_upload)