diff --git a/Implementation-Plan.md b/Implementation-Plan.md index b585048..c13e695 100644 --- a/Implementation-Plan.md +++ b/Implementation-Plan.md @@ -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 ` + - 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 diff --git a/README.md b/README.md index f001b94..1118c09 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/include/web_server.h b/include/web_server.h index 847d2b4..e319da8 100644 --- a/include/web_server.h +++ b/include/web_server.h @@ -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 diff --git a/platformio.ini b/platformio.ini index 95e5647..2f35fbe 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 = (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 diff --git a/src/main.cpp b/src/main.cpp index 96a4765..11ccae0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -112,6 +112,11 @@ void loop() { } } + // WebServer-Loop: ArduinoOTA verarbeiten + if (webStarted) { + webServer.loop(); + } + // ========================================================================== // Display: rate-limited (Modul-Updates nur bei Bedarf) // ========================================================================== diff --git a/src/web_server.cpp b/src/web_server.cpp index 47b7e80..df750f6 100644 --- a/src/web_server.cpp +++ b/src/web_server.cpp @@ -5,6 +5,7 @@ #include "web_server.h" #include // voller Include nur in .cpp (PIMPL) +#include // 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) // ----------------------------------------------------------------------------- diff --git a/upload_ota.py b/upload_ota.py new file mode 100644 index 0000000..33a3df5 --- /dev/null +++ b/upload_ota.py @@ -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)