ESP32 und Arduino

ESP32 ArduinoJson MQTT: JSON-Payloads richtig senden und parsen

Serialisieren Sie Sensorwerte und parsen Sie eingehende JSON-Befehle mit ArduinoJson 7 auf einem ESP32, veröffentlichen Sie sie mit PubSubClient per MQTT und vermeiden Sie die Puffer- und Speicherfallen, durch die Nachrichten lautlos verloren gehen.

ESP32-Board, das in einem ESP32-ArduinoJson-MQTT-Tutorial ein mit ArduinoJson erstelltes JSON-Dokument veröffentlicht
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

Die Kombination aus ESP32, ArduinoJson und MQTT ist der gängigste Weg, strukturierte Sensordaten von einem Mikrocontroller zu senden. In diesem Tutorial erstellen Sie JSON-Telemetrie mit ArduinoJson 7, veröffentlichen sie effizient mit PubSubClient, parsen JSON-Befehle im Message-Callback und lernen die Puffergrenzen kennen, durch die Nachrichten ohne Fehlermeldung verschwinden.

Warum JSON-Payloads für MQTT

MQTT kümmert sich nicht darum, was in einem Payload steckt; es transportiert Bytes. Diese Freiheit bedeutet, dass Sie ein Format wählen müssen. JSON gewinnt bei den meisten ESP32-Projekten, weil jedes Dashboard, jeder Node-RED-Flow, jedes Python-Skript und jeder Datenbank-Importer es ohne eigenen Decoder lesen kann. Eine Nachricht wie {"temp":22.4,"hum":51,"rssi":-63} beschreibt sich selbst, lässt sich leicht um neue Felder erweitern und ist beim Debuggen in jedem MQTT-Client lesbar.

Die Kosten sind Größe und Parsing-Aufwand, beides bei typischen Sensorraten auf einem ESP32 vernachlässigbar. Wenn Sie Hunderte Nachrichten pro Sekunde senden oder mit einem knappen Batteriebudget arbeiten, helfen kompakte Schlüssel mehr als ein Formatwechsel.

ArduinoJson ist dafür die De-facto-Standardbibliothek. Version 7 hat die Notwendigkeit abgeschafft, eine Dokumentkapazität festzulegen: JsonDocument wächst bei Bedarf auf dem Heap. Der gesamte Code unten richtet sich an ArduinoJson 7; unter Version 6 ersetzen Sie JsonDocument durch StaticJsonDocument<N> oder DynamicJsonDocument. Die offizielle ArduinoJson-Dokumentation behandelt die Details der Migration.

Was Sie brauchen

  • Ein ESP32-Board und die Arduino IDE oder PlatformIO.
  • Bibliotheken: PubSubClient von Nick O'Leary und ArduinoJson von Benoit Blanchon (7.x).
  • mosquitto_pub und mosquitto_sub auf Ihrem Rechner, um Befehle zu senden und die Ausgabe zu beobachten.

Die Beispiele verwenden den öffentlichen MQTT-Broker broker.connectmq.com auf Port 1883 mit dem Topic-Präfix connectmq/demo/a8e2d1. Ändern Sie dieses Präfix in eine eigene Zufallszeichenfolge und denken Sie daran, dass öffentliche Topics für jeden lesbar sind.

Topic Richtung Payload
connectmq/demo/a8e2d1/telemetry ESP32 veröffentlicht {"device":"...","temp":22.4,"hum":51,"uptime":120}
connectmq/demo/a8e2d1/cmd ESP32 abonniert {"action":"set_interval","value":10}
connectmq/demo/a8e2d1/ack ESP32 veröffentlicht {"ok":true,"action":"set_interval"}

Der vollständige ESP32-ArduinoJson-MQTT-Sketch

Der Sketch veröffentlicht Telemetrie in einem konfigurierbaren Intervall und nimmt JSON-Befehle entgegen, um dieses Intervall zu ändern oder eine LED zu schalten.

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

const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* TOPIC_TELEMETRY = "connectmq/demo/a8e2d1/telemetry";
const char* TOPIC_CMD       = "connectmq/demo/a8e2d1/cmd";
const char* TOPIC_ACK       = "connectmq/demo/a8e2d1/ack";
const int LED_PIN = 2;

WiFiClient net;
PubSubClient mqtt(net);
String deviceId;

unsigned long intervalMs = 5000;
unsigned long lastPublish = 0;

void publishAck(const char* action, bool ok, const char* error = nullptr) {
  JsonDocument doc;
  doc["ok"] = ok;
  doc["action"] = action;
  if (error) doc["error"] = error;

  char buf[128];
  size_t n = serializeJson(doc, buf, sizeof(buf));
  mqtt.publish(TOPIC_ACK, (const uint8_t*)buf, n, false);
}

void onMessage(char* topic, byte* payload, unsigned int length) {
  JsonDocument doc;
  DeserializationError err = deserializeJson(doc, payload, length);
  if (err) {
    publishAck("unknown", false, err.c_str());
    return;
  }

  const char* action = doc["action"] | "";
  if (strcmp(action, "set_interval") == 0) {
    int seconds = doc["value"] | 0;
    if (seconds < 1 || seconds > 3600) {
      publishAck(action, false, "value out of range");
      return;
    }
    intervalMs = seconds * 1000UL;
    publishAck(action, true);
  } else if (strcmp(action, "led") == 0) {
    bool on = doc["value"] | false;
    digitalWrite(LED_PIN, on ? HIGH : LOW);
    publishAck(action, true);
  } else {
    publishAck(action, false, "unknown action");
  }
}

void publishTelemetry() {
  JsonDocument doc;
  doc["device"] = deviceId;
  doc["temp"] = serialized(String(20.0 + random(0, 50) / 10.0, 1));
  doc["hum"] = 45 + random(0, 15);
  doc["uptime"] = millis() / 1000;

  JsonObject network = doc["net"].to<JsonObject>();
  network["rssi"] = WiFi.RSSI();
  network["ip"] = WiFi.localIP().toString();

  size_t len = measureJson(doc);
  mqtt.beginPublish(TOPIC_TELEMETRY, len, false);
  serializeJson(doc, mqtt);       // direkt in das MQTT-Paket streamen
  mqtt.endPublish();
}

void connectMqtt() {
  while (!mqtt.connected()) {
    if (mqtt.connect(deviceId.c_str())) {
      mqtt.subscribe(TOPIC_CMD);
    } else {
      Serial.printf("MQTT failed, state=%d\n", mqtt.state());
      delay(3000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);

  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) delay(250);

  deviceId = "esp32-json-" + String((uint32_t)ESP.getEfuseMac(), HEX);
  mqtt.setServer("broker.connectmq.com", 1883);
  mqtt.setBufferSize(1024);       // Standard ist 256 Byte, zu klein für die meisten JSON-Nachrichten
  mqtt.setCallback(onMessage);
}

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

  if (millis() - lastPublish >= intervalMs) {
    lastPublish = millis();
    publishTelemetry();
  }
}

Beobachten Sie die Ausgabe und senden Sie Befehle von Ihrem Rechner aus:

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

mosquitto_pub -h broker.connectmq.com -p 1883 -i pc-a8e2d1-pub \
  -t connectmq/demo/a8e2d1/cmd -m '{"action":"set_interval","value":10}'

mosquitto_pub -h broker.connectmq.com -p 1883 -i pc-a8e2d1-pub \
  -t connectmq/demo/a8e2d1/cmd -m '{"action":"led","value":true}'

Serialisieren: Drei Wege, JSON in einen Publish zu bringen

In einen festen Puffer

serializeJson(doc, buf, sizeof(buf)) schreibt in ein char-Array und gibt die Anzahl der geschriebenen Bytes zurück. Das ist die einfachste Variante und funktioniert gut für kleine, vorhersehbare Nachrichten wie die obige Bestätigung. Übergeben Sie die zurückgegebene Länge an publish() und verwenden Sie dabei die Überladung mit Byte-Zeiger und Länge, statt sich auf strlen() zu verlassen.

Direkt in den MQTT-Client streamen

PubSubClient erbt von der Arduino-Klasse Print, sodass ArduinoJson direkt hineinschreiben kann. measureJson(doc) liefert zuerst die exakte Größe, beginPublish() schreibt den MQTT-Header mit dieser Größe, serializeJson(doc, mqtt) streamt den Inhalt und endPublish() schließt das Paket ab. Ein Zwischenpuffer ist nicht nötig, was diese Variante für größere Dokumente am speichereffizientesten macht. setBufferSize() brauchen Sie trotzdem für normale publish()-Aufrufe und für eingehende Nachrichten, die immer in den Puffer gelesen werden.

In einen String

serializeJson(doc, someString) ist bequem, allokiert aber für jede Nachricht Speicher auf dem Heap. Auf einem Gerät, das wochenlang läuft, können wiederholte String-Allokationen den Speicher fragmentieren. Bevorzugen Sie in langlaufender Firmware die ersten beiden Varianten.

Die Genauigkeit von Fließkommazahlen steuern

ArduinoJson gibt Fließkommazahlen mit so vielen Stellen aus, dass sie verlustfrei zurückgelesen werden können, was Werte wie 22.399999 ergeben kann. Ein mit serialized() umschlossener formatierter String wird unverändert eingefügt, wie in der Telemetriefunktion oben. Alternativ runden Sie die Zahl vor der Zuweisung: doc["temp"] = round(t * 10) / 10.0;.

JSON-Befehle sicher parsen

Der MQTT-Callback erhält einen Byte-Zeiger und eine Länge, und der Payload ist nicht nullterminiert. Übergeben Sie immer die Länge: deserializeJson(doc, payload, length). Ein Aufruf nur mit (char*)payload liest über das Ende der Nachricht hinaus.

Drei weitere Gewohnheiten machen das Parsen robust:

  1. Den Fehler prüfen. DeserializationError verrät, ob die Eingabe leer, unvollständig, ungültig oder zu tief verschachtelt war. Melden Sie ihn wie im Sketch auf einem Ack-Topic zurück, damit der Absender weiß, dass der Befehl gescheitert ist.
  2. Standardwerte mit dem Operator | verwenden. doc["value"] | 0 liefert 0, wenn das Feld fehlt oder den falschen Typ hat, sodass ein fehlerhafter Befehl das Gerät weder zum Absturz bringen noch eine Variable uninitialisiert lassen kann.
  3. Wertebereiche prüfen. Erfolgreiches JSON-Parsing bedeutet nicht, dass die Werte sinnvoll sind. Lehnen Sie Intervalle von null Sekunden oder Helligkeitswerte von 5000 ausdrücklich ab.

Wenn Sie aus einem großen eingehenden Dokument nur wenige Felder brauchen, verwenden Sie einen Filter, damit ArduinoJson den Rest ignoriert:

JsonDocument filter;
filter["action"] = true;
filter["value"] = true;

JsonDocument doc;
deserializeJson(doc, payload, length, DeserializationOption::Filter(filter));

Puffer- und Speicherfallen

Symptom Ursache Lösung
publish() gibt false zurück, nichts kommt an Paket größer als der 256-Byte-Standardpuffer von PubSubClient mqtt.setBufferSize(1024) vor connect()
Eingehendes JSON erreicht nie den Callback Eingehende Nachricht größer als der Puffer Ebenso: Puffer auf den größten erwarteten Befehl vergrößern
IncompleteInput-Fehler Parsen mit strlen() auf einem nicht terminierten Payload length an deserializeJson() übergeben
Zufällige Neustarts nach Tagen Heap-Fragmentierung durch String-basierte Serialisierung In feste Puffer serialisieren oder in den Client streamen
Werte mit langen Nachkommastellen Standardformatierung von Fließkommazahlen serialized() oder Runden vor der Zuweisung

Behalten Sie auch die Broker-Limits im Blick. Der öffentliche MQTT-Broker broker.connectmq.com akzeptiert bis zu 64 KB pro Nachricht, der kostenlose ConnectMQ-Serverless-Tarif bis zu 256 KB, weit mehr, als ein ESP32-Sketch in einer Nachricht senden sollte. Wird Ihr JSON groß, teilen Sie es auf Topics wie .../telemetry und .../diagnostics auf. Der Leitfaden zur PubSubClient-Puffergröße erklärt die Pufferrechnung im Detail.

Das JSON-Schema entwerfen

Einige Konventionen ersparen Ärger, sobald mehr als ein Gerät und mehr als ein Empfänger beteiligt sind:

  • Nehmen Sie eine Geräte-ID in den Payload oder in das Topic auf, aber nicht beides uneinheitlich.
  • Verwenden Sie Einheiten in Schlüsselnamen (temp_c, power_w) oder dokumentieren Sie sie einmal; mischen Sie keine Einheiten zwischen Geräten.
  • Fügen Sie einen Zeitstempel nur hinzu, wenn das Gerät die Uhrzeit synchronisiert hat. Andernfalls lassen Sie den Empfänger die Ankunftszeit stempeln; siehe NTP-Zeitstempel für ESP32-MQTT-Nachrichten.
  • Halten Sie Befehle und Telemetrie auf getrennten Topics, damit ein Gerät nie seine eigenen Nachrichten parst.
  • Versionieren Sie den Payload mit einem Feld wie "v":1, falls sich das Format ändern könnte.

In industriellen Projekten lohnt es sich, Topic und Payload gemeinsam zu denken: Die Anlagenhierarchie gehört ins Topic, etwa acme/werk-muenchen/montage/linie-1/zelle-3/telemetry, der Payload enthält nur Messwerte und Metadaten. So passen die JSON-Nachrichten Ihres ESP32 später ohne Umbau in einen Unified Namespace (UNS); Beispiele zeigt der Beitrag zu MQTT-Payload-Formaten.

Einsatz mit echten Sensoren

Ersetzen Sie die Zufallswerte durch echte Messwerte, der Rest des Sketches bleibt gleich. Das ESP32-DHT22-MQTT-Tutorial zeigt die Sensorseite, und das ESP32-IoT-Tutorial baut ein vollständiges Geräteprojekt um dasselbe JSON-Muster herum.

Senden Sie Ihre JSON-Telemetrie an ConnectMQ

Tests gegen den kostenlosen öffentlichen MQTT-Broker sind der schnellste Weg, ein Payload-Format zu verfeinern; für produktive Daten ist er nicht gedacht. Wenn es auf die Daten ankommt, wechseln Sie zu einem privaten Broker: ConnectMQ ist ein verwalteter MQTT-Broker, dessen Portal Ihre JSON-Nachrichten live im Topic-Explorer anzeigt und mit dem Sie Schwellenwert-Alarmregeln für numerische Felder anlegen können. Registrieren Sie sich für den kostenlosen Serverless-Tarif unter app.connectmq.com, ohne Kreditkarte, und stellen Sie den Sketch dann auf WiFiClientSecure an Port 8883 mit YOUR_CLIENT_ID, YOUR_USERNAME und YOUR_PASSWORD um.

Häufig gestellte Fragen

Warum kann PubSubClient mein JSON nicht veröffentlichen?

Der Standardpuffer beträgt 256 Byte, einschließlich Topic und Header. Rufen Sie vor dem Verbinden mqtt.setBufferSize() mit einem größeren Wert auf und prüfen Sie den Rückgabewert von publish().

Sollte ich ArduinoJson 6 oder 7 verwenden?

Verwenden Sie für neue Projekte Version 7. Sie ersetzt Dokumente mit fester Kapazität durch ein einziges JsonDocument, das seine Größe selbst bestimmt, und beseitigt damit eine häufige Ursache abgeschnittener Ausgaben.

Ist JSON zu schwergewichtig für einen ESP32 mit MQTT?

Nein. Bei typischen Sensorraten sind die Kosten für Parsen und Serialisieren winzig. Halten Sie Schlüssel kurz und Payloads fokussiert, wenn Sie mit Batterie arbeiten oder viele Nachrichten pro Sekunde senden.

Wie parse ich den Payload, wenn er nicht nullterminiert ist?

Übergeben Sie dem Parser die Länge: deserializeJson(doc, payload, length). ArduinoJson liest dann genau die Bytes der Nachricht.

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.