Over-the-Air Updates für ESP-Microcontroller
https://oer-informatik.de/esp_ota
tl/dr; (ca. 5 min Lesezeit): Es ist lästig, einen ESP-Microprozessor, der in einem Projekt verbaut ist, für ein Softwareupdate auszubauen und an den USB-Port anzuschließen. Mit der Library für “Over the Air”-Updates gibt es jedoch eine elegante Möglichkeit, passwortgeschützt neue eigene Firmware aufzuspielen.
Der Arduino-Sketch muss in der üblichen Reihenfolge angepasst werden:
Die nötigen Bibliotheken müssen importiert werden.
Die Konfigurationsvariablen müssen gesetzt werden. Hierbei sollen Passwörter nicht im regulären Quelltext stehen. Die Instanzen der Bibliothek gebildet werden.
In der
setup()
müssen die erforderlichen Dienste gestartet werden und eine Option des Debuggings aktiviert werden.In der
loop()
müssen alle Operationen aufgerufen werden, die zyklisch nötig sind um die Verbindung aufrecht zu halten.Neue Funktionen, die die Verbindung (wieder-)herstellen müssen implementiert werden.
Fast-Track: Der gesamte Code zum copy/pasten
Für die Ungeduldigen: der komplette Beispielcode eingebettet in einen kleinen Sketch, der für ESP32 oder ESP8266 eine WiFi-Verbindung aufbaut, findet sich in diesem Repository zum Copy/Pasten. Einzig eine individuelle Datei secrets.h
muss dann noch nach dem Beispiel erstellt werden.
Der OTA-relevante Teil darin sind etwa die folgenden Zeilen:
#include <ArduinoOTA.h>
#include "secrets.h" // Passwords saved in this file to be hidden from versioncontrol / sharing
//-------------------------------------------------------------------------------------
// Over-the-Air Update (Upload new Software via WiFi)
// OTA-Password-Settings (if not defined in secrets.h replace your SSID/PW here)
//-------------------------------------------------------------------------------------
const char* OTA_HOSTNAME = SECRET_OTA_HOSTNAME; // Name of device for over-the-air-updates (OTA)
const char* OTA_PASSWORD = SECRET_OTA_PASSWORD; // Password for over-the-air-updates (OTA)
//-------------------------------------------------------------------------------------
// List of Input- and Output-devices and Pins
//-------------------------------------------------------------------------------------
// Datatype | Name of Variable | Pin No. connected | Name, Behaviour*/
const int PIN_UPDATE_ACTIVE = 4; // HIGH = Update active
const bool ENABLE_UPDATE_JUMPER = false;
void setup(){
Serial.begin(115200); // Activate debugging via serial monitor
//...
startOTA();
//...
}
void loop() {
if ((digitalRead(PIN_UPDATE_ACTIVE) == HIGH)||(!ENABLE_UPDATE_JUMPER)){
ArduinoOTA.handle();
}else{
//... normaler Programmablauf, der im Updatemodus nicht ausgeführt werden soll
}
//... normaler Programmablauf, der auch im Updatemodus ausgeführt werden soll
}
void startOTA() {
ArduinoOTA.onStart([]() {
String type;
if (ArduinoOTA.getCommand() == U_FLASH)
type = "sketch";
else // U_SPIFFS
type = "filesystem";
// NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
Serial.println("Start updating " + type);
});
ArduinoOTA.onEnd([]() {
Serial.println("\nEnd");
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
Serial.println("Progress: " + String(progress / (total / 100)));
});
ArduinoOTA.onError([](ota_error_t error) {
Serial.println("Error " + String(error));
if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
else if (error == OTA_END_ERROR) Serial.println("End Failed");
});
ArduinoOTA.setHostname(OTA_HOSTNAME);
ArduinoOTA.setPassword(OTA_PASSWORD);
ArduinoOTA.begin();
}
Okay, das sind ja nur paar Konfigurationen und eine Funktion startOTA()
. Was steckt darin im Einzelnen?
Die einzelnen Abschnitte zur Initialisierung des Over-the-Air-Updates
Import der Bibliotheken
Als Bibliothek wird nur ArduinoOTA
benötigt - und folglich zu Beginn des Arduino-Sketches importiert:
Konfigurationsvariablen setzen
Analog zu den WLAN-Zugangsdaten möchte das Passwort zum Aufspielen von neuem Code nicht im Arduino-Sketch speichern (da dieser in die Versionsverwaltung wandern würde). Ich gehe den gleichen Weg, mit dem ich bereits meine WLAN Credentials gesichert habe: Passwörter wandern in eine gesonderte Datei (secrets.h
), die nicht versioniert wird. Wer das noch nicht bei den WLAN-Zugangsdaten gemacht hat, der kann das jetzt zum Anlass nehmen mal aufzuräumen. Wer seinen Quellcode mit git versioniert sollte in der .gitignore
einen Eintrag mit *secrets.h
ergänzen.
Wir müssen eine neue Datei mit Namen secrets.h
erzeugen per Tastenkombination Ctrl
-Shift
-n
oder über das Punktemenü rechts in der Tableiste:

Die Datei secrets.h
definiert die Passwörter dann als Präprozessor Konstanten (vereinfacht: vor dem Kompilieren wird einmal Suchen/Ersetzen mit diesen Werten im Programmcode durchgeführt). Der OAT-relevante Inhalt der Datei secrets.h
sieht etwa so aus:
#pragma once // Only run once, even if included multiple times
#define SECRET_OTA_HOSTNAME "MeinESPProjekt"; // Name of device for over-the-air-updates (OTA)
#define SECRET_OTA_PASSWORD "superIndividuellesPasswort"; // Password for over-the-air-updates (OTA)
Im Arduino-Sketch selbst verweisen wir nur noch auf diese Konstanten:
// OTA-Password-Settings (if not defined in secrets.h replace your SSID/PW here)
const char* OTA_HOSTNAME = SECRET_OTA_HOSTNAME; // Name of device for over-the-air-updates (OTA)
const char* OTA_PASSWORD = SECRET_OTA_PASSWORD; // Password for over-the-air-updates (OTA)
Ich sichere meine Projekte gerne mit einer zusätzlichen Funktion ab: Updates per OTA sollen nur möglich sein, wenn ich einen “Update”-Schalter am Gerät aktiviert habe. Das sorgt zum einen für etwas Sicherheit - weil man physisch auf das Gerät zugreifen muss, um Updates zu aktivieren. Zum anderen sorgt es dafür, dass der normale Programmablauf nicht gestört wird durch die Überprüfung, ob Updates warten.
Ich habe also einen Taster (oder einen Jumper oder Schalter) an einem der Verfügbaren GPIOs angeschlossen. Wird dieser auf HIGH
(also 3,3V) gelegt, ist der Update-Mechanismus aktiv. Während der Entwicklung ist so ein Taster aber lästig. Daher habe ich eine Flag, mit der ich diese Prüfung umgehen kann: ENABLE_UPDATE_JUMPER
//-------------------------------------------------------------------------------------
// List of Input- and Output-devices and Pins
//-------------------------------------------------------------------------------------
// Datatype | Name of Variable | Pin No. connected | Name, Behaviour*/
const int PIN_UPDATE_ACTIVE = 4; // HIGH = Update active
const bool ENABLE_UPDATE_JUMPER = false;
Anpassungen in der setup()
-Funktion
In der setup()
sollte die serielle Verbindung für Debugging aktiviert werden. Das ist natürlich optional, denn eigentlich wollen wir ja genau keine serielle Verbindung mehr nutzen müssen. Wenn OTA erstmal läuft könnte das also wieder ’rausgenommen werden.
Darüber hinaus rufen wir nur die Funktion auf, die wir unten gleich implementieren werden (startOTA()
). Sie initialisiert den OTA-Prozess.
Natürlich steht in der setup()
noch der sonstige Code, der z.B. für die WiFi-Verbindung erforderlich ist und die sonstigen Programmbestandteile initialisiert. Aber darum geht es hier ja nicht.
void setup(){
Serial.begin(115200); // Activate debugging via serial monitor
//...
startOTA();
//...
}
Anpassungen in der loop()
-Funktion
Mit Einbindung der Library verfügen wir über ein Objekt der Klasse ArduinoOTAClass
, das wir über die Variable ArduinoOTA
ansprechen können. Dieses Objekt verfügt über eine Methode, die überprüft, ob neue Updates vorliegen: ArduinoOTA.handle();
Diese Methode muss möglichst häufig aufgerufen werden, z.B. zu Beginn der loop()
. Denn nur wenn diese Methode durchlaufen wird können neue Updates aufgespielt werden. Es kann jedoch eine gute Idee sein, nicht immer nach Updates zu suchen: Die Update-Methode handle()
selbst beansprucht auch Rechenzeit. Diese ist aber meistens gar nicht nötig.
Andererseits kann es eine gute Idee sein, die anderen Funktionalitäten des ESP zu stoppen, wenn nach Updates gesucht werden soll. Schließlich ist ungünstigsten Falls kein Update möglich, wenn der ESP gerade mit anderen Aufgaben beschäftigt ist, und die ArduinoOTA.handle()
nicht im richtigen Augenblich aufruft.
Daher schalten wir zwischen Update-Betrieb und Normalbetrieb hin und her. Wir haben oben bereits einen Pin konfiguriert, an dem ein Jumper/Schalter angebracht werden kann (PIN_UPDATE_ACTIVE
). Nur wenn dieser Pin auf HIGH
geschaltet wurde, wird nach Updates gesucht. Für Debugging-Zwecke kann ich die Überprüfung des Pins aber deaktivieren (ENABLE_UPDATE_JUMPER
).
void loop() {
if ((digitalRead(PIN_UPDATE_ACTIVE) == HIGH)||(!ENABLE_UPDATE_JUMPER)){
ArduinoOTA.handle();
}else{
//... normaler Programmablauf, der im Updatemodus nicht ausgeführt werden soll
}
//... normaler Programmablauf, der auch im Updatemodus ausgeführt werden soll
}
Unser Programm wird dadurch etwas performanter, aber auch sicherer, weil nur Leute mit physischem Zugriff auf unser Gerät Updates aufspielen können. Allerdings benötigen wir dazu noch unbelegte Pins.
Die Konfiguration von startOTA()
Eigentlich haben wir das Meiste schon implementiert. Jetzt hübschen wir nur noch die Benutzbarkeit ein bisschen auf in der startOTA()
-Funktion, setzten Passwort und Hostname und starten den Service.
startOTA()
nutzt wieder die Instanz der ArduinoOTAClass
, die über die Variable ArduinoOTA
angesprochen wird. In dem Codeabschnitt wird lediglich das Verhalten während des Updateprozesses spezifiziert. Hierzu werden Callback-Funktionen übergeben - also Code, der von ArduinoOTA
zu gegebener Zeit aufgerufen werden soll:
ArduinoOTA.onStart(...);
übergibt die Callback-Funktion , die zu Beginn des Updateprozesses ausgeführt werden soll. Hier wird festgelegt, ob das Update im Flash oder in eine Dateisystem gespeichert werden soll.Die Callback-Funktion, die bei
ArduinoOTA.onEnd(...)
undArduinoOTA.onError(...)
übergeben werden, enthalten lediglich Debugging-Ausgaben. Sie werden - wie der Name schon sagt - im Fehlerfall und am Ende des Updateprozesses aufgerufen.ArduinoOTA.onProgress(...)
übergibt die Callback-Funktion, die während des Update-Prozesses aufgerufen wird. Mit ihrer Hilfe wird der Update-Fortschritt ausgegeben.Im Anschluss setzen wir mit
ArduinoOTA.setHostname(OTA_HOSTNAME);
undArduinoOTA.setPassword(OTA_PASSWORD);
noch die Zugangsdaten, mit der wir den ESP zukünftig für Updates finden.
ArduinoOTA.begin();` schließlich startet den Service.
void startOTA() {
ArduinoOTA.onStart([]() {
String type;
if (ArduinoOTA.getCommand() == U_FLASH)
type = "sketch";
else // U_SPIFFS
type = "filesystem";
// NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
Serial.println("Start updating " + type);
});
ArduinoOTA.onEnd([]() {
Serial.println("\nEnd");
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
Serial.println("Progress: " + String(progress / (total / 100)));
});
ArduinoOTA.onError([](ota_error_t error) {
Serial.println("Error " + String(error));
if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
else if (error == OTA_END_ERROR) Serial.println("End Failed");
});
ArduinoOTA.setHostname(OTA_HOSTNAME);
ArduinoOTA.setPassword(OTA_PASSWORD);
ArduinoOTA.begin();
}
Hinweis: in der Bibliothek für den ESP32 ist die Schreibweise etwas anders: die Methoden onStart(...)
usw. geben beim ESP32 jeweils das ArduinoOTA
-Objekt zurück. Daher können die Aufrufe verkettet werden (onStart().onEnd().onError().onProgress()
). Da diese Schreibweise nicht kompatibel zur ESP8266 (nodeMCU)-Bibliothek ist habe ich sie nicht angewendet.
Weitere Literatur und Quellen
Weiter geht es im zweiten Teil, in dem digitale Ausgänge genutzt und externe LED angesteuert werden.
die Implementierung von Arduino OTA für ESP32 findet sich in diesem github repository
die Implementierung von Arduino OTA für ESP8266 findet sich in diesem github repository
Quellen und offene Ressourcen (OER)
Die Ursprungstexte (als Markdown), Grafiken und zugrunde liegende Diagrammquelltexte finden sich (soweit möglich in weiterbearbeitbarer Form) in folgendem git-Repository:
https://gitlab.com/oer-informatik/mcu/arduino-esp.
Sofern nicht explizit anderweitig angegeben sind sie zur Nutzung als Open Education Resource (OER) unter Namensnennung (H. Stein, oer-informatik.de) freigegeben gemäß der Creative Commons Namensnennung 4.0 International Lizenz (CC BY 4.0).