ESP32 und Arduino

ESP32 OTA Update über MQTT: HTTPS-Firmware-Updates auslösen und Status melden

Lösen Sie ESP32-Firmware-Updates per MQTT-Befehl aus, laden Sie das Binary mit HTTPUpdate über HTTPS herunter und veröffentlichen Sie Fortschritt und Ergebnis auf einem retained Status-Topic, damit Sie immer wissen, welche Version auf jedem Gerät läuft.

ESP32-Geräte, die in einem ESP32-OTA-Update-MQTT-Workflow mit HTTPS-Downloadserver einen Firmware-Befehl empfangen
Probieren Sie es auf Ihrem eigenen MQTT-Broker ausHolen Sie sich im Free-Tarif einen privaten, verwalteten MQTT-Broker und starten Sie mit fertigen Codebeispielen.
Kostenlos testen Codebeispiele Keine Kreditkarte erforderlich

Mit einem ESP32 OTA Update über MQTT aktualisieren Sie Geräte im Feld, ohne das Gehäuse zu öffnen: Sie veröffentlichen einen Befehl, das Gerät lädt neue Firmware über HTTPS herunter und meldet seinen Fortschritt auf einem Status-Topic zurück. Dieses Tutorial baut diesen Workflow Schritt für Schritt mit der HTTPUpdate-Bibliothek von Arduino-ESP32 auf und zeigt die Schutzmaßnahmen, die Sie brauchen, bevor Sie ihn auf echter Hardware einsetzen.

Warum MQTT und HTTPS für OTA kombinieren

Es ist verlockend, das Firmware-Binary selbst über MQTT zu schicken, aber selten eine gute Idee. Firmware-Images sind Hunderte Kilobyte bis einige Megabyte groß, viele Broker und Client-Bibliotheken begrenzen die Nachrichtengröße weit darunter, und ein in Stücke zerlegtes Binary bedeutet, dass Sie eigene Logik für Zusammensetzen, Reihenfolge und Prüfsummen schreiben müssen.

Sauberer ist es, jedes Protokoll das tun zu lassen, was es gut kann:

  • MQTT transportiert die kleinen Steuernachrichten: „auf Version 1.4.0 von dieser URL aktualisieren" sowie Fortschritts- und Ergebnismeldungen. Die Geräte sind bereits verbunden, sodass der Befehl sofort ankommt, selbst hinter NAT.
  • HTTPS transportiert das große Binary von einem Webserver oder Object Storage, wobei TLS, Range-Anfragen und Caching bereits gelöst sind.

Das Gerät braucht nie einen offenen eingehenden Port, und Sie haben auf dem Broker eine Live-Aufzeichnung jedes Update-Versuchs.

Das Topic-Design

Topic Richtung Payload Retained
connectmq/demo/e47b10/<device>/ota/cmd Betreiber veröffentlicht {"version":"1.4.0","url":"https://..."} Nein
connectmq/demo/e47b10/<device>/ota/status ESP32 veröffentlicht {"state":"downloading","progress":42} Ja
connectmq/demo/e47b10/<device>/info ESP32 veröffentlicht {"version":"1.3.2"} Ja

Da Status- und Info-Topics retained sind, sieht jedes Werkzeug, das später abonniert, etwa ein Dashboard oder ein Skript, das einen Rollout prüft, sofort die aktuelle Version und das letzte OTA-Ergebnis jedes Geräts.

Voraussetzungen

  • Ein ESP32 mit mindestens 4 MB Flash und einem Partitionsschema mit zwei OTA-App-Slots. Das Standardschema von Arduino hat sie; „Huge APP (no OTA)" nicht.
  • Arduino-ESP32-Core 2.x oder 3.x, der HTTPUpdate enthält.
  • Die Bibliotheken PubSubClient und ArduinoJson.
  • Ein HTTPS-Server, der Ihre .bin-Datei ausliefert. Jeder Host für statische Dateien funktioniert.

Exportieren Sie das Binary in der Arduino IDE mit Sketch > Kompilierte Binärdatei exportieren oder nehmen Sie in PlatformIO .pio/build/<env>/firmware.bin.

Der vollständige ESP32-OTA-Update-MQTT-Sketch

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPUpdate.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

#define FW_VERSION "1.3.2"

const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* ALLOWED_HOST = "https://firmware.example.com/";   // nur URLs unter diesem Präfix akzeptieren

// Root-CA Ihres Firmware-Hosts im PEM-Format
const char* FIRMWARE_CA = R"EOF(
-----BEGIN CERTIFICATE-----
...paste the root CA of your download server here...
-----END CERTIFICATE-----
)EOF";

WiFiClient mqttNet;
PubSubClient mqtt(mqttNet);
String deviceId, topicCmd, topicStatus, topicInfo;

volatile bool otaRequested = false;
String otaUrl, otaVersion;
int lastReported = -1;

void publishStatus(const char* state, int progress = -1, const char* error = nullptr) {
  JsonDocument doc;
  doc["state"] = state;
  doc["from"] = FW_VERSION;
  if (otaVersion.length()) doc["to"] = otaVersion;
  if (progress >= 0) doc["progress"] = progress;
  if (error) doc["error"] = error;
  char buf[192];
  size_t n = serializeJson(doc, buf, sizeof(buf));
  mqtt.publish(topicStatus.c_str(), (const uint8_t*)buf, n, true);
}

void onMessage(char* topic, byte* payload, unsigned int length) {
  JsonDocument doc;
  if (deserializeJson(doc, payload, length)) return;
  String url = doc["url"] | "";
  String version = doc["version"] | "";
  if (!url.startsWith(ALLOWED_HOST) || version.length() == 0) {
    publishStatus("rejected", -1, "url or version invalid");
    return;
  }
  if (version == FW_VERSION) {
    publishStatus("up_to_date");
    return;
  }
  otaUrl = url;
  otaVersion = version;
  otaRequested = true;              // Arbeit in loop() erledigen, nicht hier
}

void onProgress(int current, int total) {
  int pct = total > 0 ? (current * 100) / total : 0;
  if (pct / 10 != lastReported / 10) {   // alle 10 % melden
    lastReported = pct;
    publishStatus("downloading", pct);
    mqtt.loop();
  }
}

void runOta() {
  otaRequested = false;
  lastReported = -1;
  publishStatus("starting");

  WiFiClientSecure https;
  https.setCACert(FIRMWARE_CA);

  httpUpdate.rebootOnUpdate(false);
  httpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
  httpUpdate.onProgress(onProgress);

  t_httpUpdate_return result = httpUpdate.update(https, otaUrl);

  switch (result) {
    case HTTP_UPDATE_OK:
      publishStatus("success", 100);
      mqtt.loop();
      delay(500);
      ESP.restart();
      break;
    case HTTP_UPDATE_NO_UPDATES:
      publishStatus("no_update");
      break;
    case HTTP_UPDATE_FAILED:
    default:
      publishStatus("failed", -1, httpUpdate.getLastErrorString().c_str());
      break;
  }
}

void connectMqtt() {
  while (!mqtt.connected()) {
    if (mqtt.connect(deviceId.c_str(), topicStatus.c_str(), 1, true,
                     "{\"state\":\"offline\"}")) {
      mqtt.subscribe(topicCmd.c_str());
      String info = String("{\"version\":\"") + FW_VERSION + "\"}";
      mqtt.publish(topicInfo.c_str(), info.c_str(), true);
    } else {
      delay(3000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) delay(250);

  deviceId = "esp32-ota-" + String((uint32_t)ESP.getEfuseMac(), HEX);
  String base = "connectmq/demo/e47b10/" + deviceId;
  topicCmd = base + "/ota/cmd";
  topicStatus = base + "/ota/status";
  topicInfo = base + "/info";

  mqtt.setServer("broker.connectmq.com", 1883);
  mqtt.setBufferSize(512);
  mqtt.setCallback(onMessage);
  Serial.printf("Device %s running %s\n", deviceId.c_str(), FW_VERSION);
}

void loop() {
  if (!mqtt.connected()) connectMqtt();
  mqtt.loop();
  if (otaRequested) runOta();
}

Lesen Sie nach dem ersten Flashen über USB die Geräte-ID im seriellen Monitor ab und lösen Sie ein Update aus:

mosquitto_sub -h broker.connectmq.com -p 1883 -i pc-e47b10 -t 'connectmq/demo/e47b10/#' -v

mosquitto_pub -h broker.connectmq.com -p 1883 -i pc-e47b10-pub \
  -t connectmq/demo/e47b10/esp32-ota-1a2b3c4d/ota/cmd \
  -m '{"version":"1.4.0","url":"https://firmware.example.com/sensor-1.4.0.bin"}'

Sie sehen starting, eine Reihe von downloading-Nachrichten, success und danach eine info-Nachricht mit der neuen Version, sobald das Gerät neu gestartet und wieder verbunden ist.

Wie die Teile zusammenspielen

Der Callback merkt sich nur die Anfrage. httpUpdate.update() blockiert für die gesamte Downloaddauer. Würde es im MQTT-Callback laufen, würde es mqtt.loop() aus sich selbst heraus blockieren; der Sketch setzt daher ein Flag und lässt loop() das Update starten. So löst das Gerät auch den Task Watchdog nicht aus; ESP32-Task-Watchdog-Resets in MQTT-Projekten erklärt, warum.

Der Fortschritt hält die MQTT-Verbindung am Leben. Während eines langen Downloads veröffentlicht der Fortschritts-Callback alle 10 % und ruft mqtt.loop() auf, was auch Keepalive-Pings sendet, damit der Broker die Session nicht auf halbem Weg verwirft.

rebootOnUpdate(false) gibt Ihnen das letzte Wort. Standardmäßig startet HTTPUpdate neu, sobald das Image geschrieben ist. Wenn Sie das abschalten, kann das Gerät zuerst success veröffentlichen und sich dann selbst neu starten.

Der Last Will meldet Abstürze. Der Connect-Aufruf registriert auf dem Status-Topic eine retained offline-Will-Nachricht, sodass ein Gerät, das mitten im Update ausfällt, als offline erscheint, statt bei „downloading" hängen zu bleiben. Der Leitfaden zu ESP32 MQTT Last Will behandelt dieses Muster im Detail.

Schutzmaßnahmen, die Sie nicht auslassen dürfen

Fernupdates sind das Mächtigste, was Ihr Gerät tun kann, und das macht den Update-Befehl zum attraktivsten Angriffsziel. Bevor dies auch nur in die Nähe eines echten Einsatzes kommt:

  1. OTA in der Produktion nie über ein öffentliches Topic auslösen. Der öffentliche MQTT-Broker von ConnectMQ ist für jeden les- und schreibbar, was für die Demo mit Platzhalter-URL in Ordnung ist, nicht aber für echte Geräte. Verwenden Sie einen privaten Broker mit Zugangsdaten.
  2. Den Download-Host auf eine Allowlist setzen. Der Sketch lehnt jede URL ab, die nicht mit Ihrem eigenen HTTPS-Präfix beginnt, sodass ein bösartiger Befehl das Gerät nicht auf ein beliebiges Binary verweisen kann.
  3. Das TLS-Zertifikat prüfen. Laden Sie die Root-CA Ihres Firmware-Hosts mit setCACert(). Mit setInsecure() könnte jeder auf dem Netzwerkpfad das Image austauschen.
  4. Images signieren. Für stärkere Garantien unterstützt ESP-IDF Secure Boot und die Prüfung signierter Apps, sodass der Bootloader Firmware ablehnt, die Sie nicht signiert haben. Das erfordert einen ESP-IDF-Build oder eine angepasste Arduino-Konfiguration.
  5. Versionen prüfen. Ignorieren Sie Befehle für die bereits installierte Version und erwägen Sie, Downgrades abzulehnen, sofern sie nicht ausdrücklich gekennzeichnet sind.

Rollout auf eine Flotte

Schritt Was zu tun ist
1 Das neue Binary unter einem versionierten Dateinamen hochladen, das alte nie überschreiben
2 Ein Testgerät aktualisieren und sein Status-Topic beobachten, bis info die neue Version zeigt
3 Eine kleine Gruppe aktualisieren und auf Geräte achten, die offline bleiben
4 Den Rest in Chargen ausrollen, nicht alle auf einmal
5 Das vorherige Binary verfügbar halten, damit Sie es bei Bedarf zurückspielen können

Befehle Gerät für Gerät zu senden statt an ein gemeinsames Topic, das jedes Gerät abonniert, gibt Ihnen diese Kontrolle. Ein kleines Python- oder Node.js-Skript, das eine Liste von Geräte-IDs liest, den Befehl veröffentlicht und auf jedes success wartet, ist meist das einzige Werkzeug, das Sie brauchen. In der Fertigung stimmen Sie die Wellen zusätzlich mit Wartungsfenstern oder geplanten Stillständen der Linie ab, damit ein Neustart nie mitten in einen laufenden Auftrag fällt.

Für Produkte, die auf dem EU-Markt bereitgestellt werden, ist ein solcher Update-Pfad mehr als Komfort: Der EU Cyber Resilience Act erwartet, dass Hersteller Sicherheitsupdates über den Supportzeitraum bereitstellen können.

Fehlerbehebung

  • HTTP_UPDATE_FAILED mit „Not Enough Space": Das Binary ist größer als der OTA-Slot. Wählen Sie ein Partitionsschema mit größeren App-Partitionen.
  • „Wrong HTTP Code" mit einer Weiterleitung: Manche Hosts leiten auf ein CDN weiter. Der Sketch folgt Weiterleitungen strikt; stellen Sie sicher, dass die geladene CA auch zum endgültigen Host passt.
  • TLS-Handshake scheitert: Falsche CA oder zu wenig Heap. Geben Sie große Puffer frei, bevor Sie runOta() aufrufen.
  • Gerät startet mit der alten Firmware: Das neue Image ließ sich nicht starten. Testen Sie das Binary zuerst über USB.
  • Broker trennt das Gerät während des Downloads: Der Fortschritts-Callback läuft nicht oft genug, um mqtt.loop() aufzurufen. Verkleinern Sie den Meldeschritt.

Betreiben Sie Ihren OTA-Workflow mit ConnectMQ

Testen Sie den gesamten Ablauf mit dem öffentlichen MQTT-Broker unter broker.connectmq.com und verlegen Sie die Befehls-Topics dann hinter ConnectMQ-Zugangsdaten. ConnectMQ ist ein verwalteter MQTT-Broker mit Mandantentrennung, an ausgegebene Zugangsdaten gebundenen Client IDs und TLS auf 8883, sodass nur Ihre Werkzeuge Update-Befehle an Ihre Geräte senden können. Erstellen Sie einen Broker im kostenlosen Serverless-Tarif unter app.connectmq.com, ohne Kreditkarte, und verfolgen Sie während eines Rollouts mit dem Live-Topic-Explorer das Status-Topic jedes Geräts. Das vollständige Geräteprojekt rund um diesen Ablauf zeigt das ESP32-IoT-Tutorial.

Häufig gestellte Fragen

Kann ich das Firmware-Binary selbst über MQTT senden?

Das ist möglich, lohnt sich aber selten. Binaries sind groß, Nachrichtengrößen sind begrenzt, und Sie bräuchten eigene Logik für Zerlegung und Prüfung. Senden Sie stattdessen eine URL über MQTT und laden Sie das Binary über HTTPS herunter.

Was passiert, wenn während eines OTA-Updates der Strom ausfällt?

Der ESP32 schreibt das neue Image in den inaktiven OTA-Slot und wechselt die Boot-Partition erst nach einem vollständigen, geprüften Schreibvorgang. Ein Stromausfall mitten im Download lässt die alte Firmware unangetastet.

Warum zeigt mein Gerät dauerhaft „downloading" an?

Wahrscheinlich ist es abgestürzt oder hat ohne sauberes Trennen den Strom verloren. Mit einem retained Last Will auf dem Status-Topic ersetzt der Broker diesen Zustand nach Ablauf des Keepalive durch „offline".

Brauche ich QoS 1 für den OTA-Befehl?

QoS 1 ist für Befehle eine gute Wahl, weil es die Zustellung garantiert, solange das Gerät verbunden ist. Machen Sie den Handler idempotent, indem Sie Befehle für die bereits installierte Version ignorieren.

Verwandte Artikel

ESP32 NTP MQTT Zeitstempel: genaue Uhrzeit in jeder Nachricht

Synchronisieren Sie die ESP32-Uhr per NTP, formatieren Sie Zeitstempel in ISO 8601 und Epoch-Millisekunden und hängen Sie sie an MQTT-Payloads, damit Messwerte ihren echten Erfassungszeitpunkt behalten, auch wenn sie verspätet veröffentlicht werden.

8 Min. Lesezeit

Alle Artikel →

Ihr privater MQTT-Broker ist nur wenige Klicks entfernt

Konto anlegen, Broker-Zugangsdaten erhalten und das erste Gerät verbinden — ohne Server, die installiert, gepatcht oder skaliert werden müssen.