ESP32 und Arduino

ESP32 OLED MQTT: Live-MQTT-Nachrichten auf einem SSD1306-Display anzeigen

Machen Sie aus einem ESP32 und einem günstigen SSD1306-OLED ein Live-MQTT-Display, das Sensorwerte und Nachrichten beliebiger Geräte anzeigt, veraltete Messwerte markiert und seinen eigenen Verbindungsstatus darstellt.

Kleiner SSD1306-Bildschirm an einem ESP32, der in einem ESP32-OLED-MQTT-Display Temperaturwerte anzeigt
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

Ein ESP32-OLED-MQTT-Display ist der einfachste Weg, Live-Daten ohne Smartphone oder Browser auf den Schreibtisch, die Werkbank oder ein Maschinenpanel zu bringen. In diesem Tutorial abonnieren Sie mehrere Topics auf broker.connectmq.com, zeigen die neuesten Werte auf einem 128×64-SSD1306-Bildschirm an, markieren Messwerte als veraltet, wenn ein Sensor nicht mehr meldet, und zeigen den eigenen WLAN- und MQTT-Status des Displays. Derselbe Sketch funktioniert unverändert, wenn Sie auf private Zugangsdaten umsteigen.

Was das Display anzeigt

Ein gutes Mini-Dashboard beantwortet auf einen Blick drei Fragen: Wie lautet der Wert, wie alt ist er, und ist das Display selbst verbunden? Das hier verwendete Layout bringt alle drei auf einem 0,96-Zoll-Bildschirm unter:

+--------------------------------+
| MQTT  OK          RSSI -58     |
| Temp      23.4 C               |
| Humidity  48 %                 |
| Door      CLOSED       (12s)   |
| Msg: Coffee is ready           |
+--------------------------------+

Jede Zeile ist an ein Topic gebunden. Das Display abonniert mit einer einzigen Wildcard und ordnet eingehende Nachrichten anhand der letzten Topic-Ebene den Zeilen zu.

Topic Zeile Beispiel-Payload
connectmq/demo/<id>/temp Temp 23.4
connectmq/demo/<id>/humidity Humidity 48
connectmq/demo/<id>/door Door CLOSED
connectmq/demo/<id>/msg Msg Beliebiger kurzer Text

In einem echten Setup stammen die Werte von anderen Geräten: einem ESP32 mit DHT22-Sensor, der Temperatur und Luftfeuchtigkeit veröffentlicht, einem Türkontakt oder einem Skript, das Benachrichtigungen sendet. Dem Display ist egal, wer veröffentlicht, und genau das ist die größte Stärke des Publish/Subscribe-Modells. Ist diese Entkopplung neu für Sie, lesen Sie vor dem Start, was ein MQTT-Broker macht.

Hardware und Verdrahtung

  • ESP32-Entwicklungsboard.
  • SSD1306-OLED-Modul, 128×64 Pixel, I2C-Version (vier Pins: GND, VCC, SCL, SDA).
  • Vier Jumperkabel.
OLED-Pin ESP32-Pin
GND GND
VCC 3V3
SCL GPIO 22
SDA GPIO 21

GPIO 21 und 22 sind die Standard-I2C-Pins klassischer ESP32-DevKit-Boards. Andere Varianten wie ESP32-S3 oder C3 nutzen andere Standards, übergeben Sie Ihre Pins daher explizit an Wire.begin(SDA, SCL), wie im Sketch. Die meisten Module nutzen die I2C-Adresse 0x3C, einige wenige 0x3D. Bleibt der Bildschirm schwarz, ermitteln Sie die Adresse mit einem I2C-Scanner-Sketch.

Bibliotheken

Haben Sie noch nie einen ESP32 aus der Arduino IDE geflasht, folgen Sie zuerst der Board-Einrichtung im ESP32-IoT-Tutorial; es behandelt die Installation des esp32-Board-Pakets, die Wahl des richtigen Boards und Ports sowie das Lesen des seriellen Monitors, die Sie hier alle brauchen. Installieren Sie dann über den Arduino-Bibliotheksverwalter:

  • PubSubClient von Nick O'Leary.
  • Adafruit SSD1306 von Adafruit, die auch Adafruit GFX Library und Adafruit BusIO mitinstalliert.

Der ESP32-OLED-MQTT-Sketch

Die wichtigste Designregel: Zeichnen Sie niemals im MQTT-Callback. Der Callback speichert nur den Wert und einen Zeitstempel und setzt ein Flag. Gezeichnet wird in loop() mit fester Rate. So bleibt der Callback schnell, halb aktualisierte Bildschirme werden vermieden, und das Display kann die Alterszähler auch dann aktualisieren, wenn keine Nachrichten eintreffen.

#include <WiFi.h>
#include <Wire.h>
#include <PubSubClient.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

const char* WIFI_SSID     = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST     = "broker.connectmq.com";
const uint16_t MQTT_PORT  = 1883;
const char* CLIENT_ID     = "oled-5d20ab";             // eindeutig
const char* BASE          = "connectmq/demo/oled-5d20ab";

const unsigned long STALE_MS = 120000;                  // 2 Minuten

Adafruit_SSD1306 display(128, 64, &Wire, -1);
WiFiClient net;
PubSubClient mqtt(net);

struct Row {
  const char* key;       // letzte Topic-Ebene
  const char* label;
  const char* unit;
  String value;
  unsigned long at;      // millis() beim Empfang, 0 = nie
};

Row rows[] = {
  {"temp",     "Temp",     " C", "", 0},
  {"humidity", "Humidity", " %", "", 0},
  {"door",     "Door",     "",   "", 0},
  {"msg",      "Msg:",     "",   "", 0},
};
const int ROW_COUNT = sizeof(rows) / sizeof(rows[0]);
bool dirty = true;
unsigned long lastDraw = 0, lastAttempt = 0;

void onMessage(char* topic, byte* payload, unsigned int length) {
  const char* leaf = strrchr(topic, '/');
  if (!leaf) return;
  leaf++;
  for (int i = 0; i < ROW_COUNT; i++) {
    if (strcmp(leaf, rows[i].key) == 0) {
      String v;
      for (unsigned int j = 0; j < length && j < 40; j++) v += (char)payload[j];
      rows[i].value = v;
      rows[i].at = millis();
      dirty = true;
      return;
    }
  }
}

String ageText(unsigned long at) {
  unsigned long s = (millis() - at) / 1000;
  if (s < 60) return String(s) + "s";
  if (s < 3600) return String(s / 60) + "m";
  return String(s / 3600) + "h";
}

void draw() {
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);

  // Statusleiste
  display.setCursor(0, 0);
  display.print("MQTT ");
  display.print(mqtt.connected() ? "OK" : (WiFi.status() == WL_CONNECTED ? "..." : "NO WIFI"));
  if (WiFi.status() == WL_CONNECTED) {
    display.setCursor(80, 0);
    display.print("RSSI ");
    display.print(WiFi.RSSI());
  }
  display.drawFastHLine(0, 10, 128, SSD1306_WHITE);

  int y = 14;
  for (int i = 0; i < ROW_COUNT; i++) {
    display.setCursor(0, y);
    display.print(rows[i].label);
    display.setCursor(i == 3 ? 30 : 60, y);
    if (rows[i].at == 0) {
      display.print("--");
    } else {
      bool stale = millis() - rows[i].at > STALE_MS;
      String text = rows[i].value + rows[i].unit;
      if (i == 3) text = text.substring(0, 16);         // an die Nachrichtenzeile anpassen
      display.print(text);
      if (stale && i != 3) {
        display.setCursor(104, y);
        display.print(ageText(rows[i].at));             // Alter nur bei veralteten Werten zeigen
      }
    }
    y += 12;
  }
  display.display();
}

bool connectMqtt() {
  if (!mqtt.connect(CLIENT_ID)) {
    Serial.printf("MQTT failed, state=%d\n", mqtt.state());
    return false;
  }
  String filter = String(BASE) + "/+";
  mqtt.subscribe(filter.c_str(), 1);
  Serial.println("Subscribed to " + filter);
  return true;
}

void setup() {
  Serial.begin(115200);
  Wire.begin(21, 22);
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println("SSD1306 not found");
    while (true) delay(1000);
  }
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.print("Connecting...");
  display.display();

  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setSocketTimeout(5);
}

void loop() {
  if (WiFi.status() == WL_CONNECTED) {
    if (!mqtt.connected()) {
      if (millis() - lastAttempt > 5000) { lastAttempt = millis(); connectMqtt(); }
    } else {
      mqtt.loop();
    }
  }
  // Bei neuen Daten neu zeichnen und einmal pro Sekunde, um Status und Alter zu aktualisieren
  if (dirty || millis() - lastDraw > 1000) {
    draw();
    dirty = false;
    lastDraw = millis();
  }
}

Warum Retained Messages für ein Display wichtig sind

Beim Booten weiß der ESP32 nichts. Veröffentlichen die Sensoren ihre Messwerte als Retained Messages, übergibt der Broker dem Display im Moment des Abonnierens den neuesten Wert jedes Topics, und der Bildschirm ist innerhalb einer Sekunde gefüllt. Ohne Retained Messages zeigen die Zeilen --, bis jeder Sensor das nächste Mal veröffentlicht, was bei einem Batteriegerät Minuten dauern kann. Wenn Sie die Publisher selbst kontrollieren, setzen Sie Retain bei sich langsam ändernden Werten wie Temperatur, Türzustand und der letzten Benachrichtigung.

Über die Kommandozeile testen

Veröffentlichen Sie einige Werte, als wären Sie die Sensoren:

mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-5d20ab -r -t "connectmq/demo/oled-5d20ab/temp" -m "23.4"
mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-5d20ab -r -t "connectmq/demo/oled-5d20ab/humidity" -m "48"
mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-5d20ab -r -t "connectmq/demo/oled-5d20ab/door" -m "CLOSED"
mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-5d20ab -t "connectmq/demo/oled-5d20ab/msg" -m "Coffee is ready"

Drücken Sie nun den Reset-Knopf des ESP32. Da die ersten drei mit -r gesendet wurden, erscheinen sie fast sofort nach dem Reconnect wieder; die Nachrichtenzeile bleibt leer, weil sie nicht retained war. Warten Sie zwei Minuten, ohne zu veröffentlichen, und neben jedem veralteten Wert erscheint die Altersanzeige.

Um einen retained Wert zu löschen, veröffentlichen Sie eine leere Retained Message auf diesem Topic:

mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-5d20ab -r -n -t "connectmq/demo/oled-5d20ab/door"

Retained Daten auf dem öffentlichen MQTT-Broker, der für Tests und Prototypen gedacht ist, sind für jeden sichtbar, der abonniert, beschränken Sie sich dort also auf harmlose Demowerte.

Topics für ein gemeinsam genutztes Display wählen

Die Demo legt jede Zeile unter das eigene Präfix des Displays, damit das Beispiel in sich geschlossen bleibt. In einer echten Installation besitzen meist die Sensoren ihre Topics, zum Beispiel home/livingroom/temp von einem Gerät und home/garage/door von einem anderen. In einer Fertigungshalle folgen die Topics idealerweise einem Unified Namespace (UNS), dessen Struktur die ISA-95-Hierarchie abbildet, etwa acme/werk-nuernberg/montage/linie-1/zelle-2/temperatur; ein Display an der Linie abonniert dann einfach den Zweig seiner Zelle. Sie haben zwei Optionen. Die erste: jedes Topic explizit abonnieren und vollständige Topic-Strings den Zeilen zuordnen. Das ist der berechenbarste Ansatz und der richtige, wenn Topics aus verschiedenen Teilen des Baums stammen. Die zweite: sich auf eine Konvention einigen, etwa einen Zweig display/, in den Sensoren oder ein kleiner Node-RED-Flow erneut veröffentlichen. Das hält die Display-Firmware generisch, und eine neue Zeile wird zur Konfigurationsänderung statt zur Codeänderung. Was Sie auch wählen, denken Sie daran, dass der öffentliche MQTT-Broker 10 Subscriptions pro Client erlaubt: Eine Handvoll expliziter Topics ist in Ordnung, Dutzende nicht.

Tipps für gute Lesbarkeit auf kleinen Bildschirmen

  1. Verwenden Sie Textgröße 2 für die wichtigste Zahl. Ein 128×64-Bildschirm fasst etwa vier Zeilen Text in Größe 2, genug für einen großen Messwert plus eine Statuszeile.
  2. Richten Sie Zahlen rechtsbündig aus, damit Ziffern bei Wertänderungen nicht hin und her springen. Messen Sie die Textbreite mit display.getTextBounds().
  3. Runden Sie beim Publisher, nicht im Display. 23.4 statt 23.41678 zu senden spart Bytes und Bildschirmplatz.
  4. Zeigen Sie veraltete Werte explizit an. Ein eingefrorener Wert, der aktuell aussieht, ist schlimmer als ein leerer. Der Alterszähler ist der günstigste ehrliche Indikator.
  5. Achten Sie auf Einbrennen. OLED-Pixel altern mit der Nutzung. Senken Sie bei dauerhaft eingeschalteten Panels den Kontrast mit display.dim(true) oder verschieben Sie das Layout alle paar Minuten um einen Pixel.

Fehlerbehebung

  • Schwarzer Bildschirm, keine Fehler. Falsche I2C-Adresse oder vertauschte SDA/SCL. Führen Sie einen I2C-Scanner aus.
  • "SSD1306 not found" auf einem S3- oder C3-Board. Die Standard-Pins unterscheiden sich; übergeben Sie die richtigen an Wire.begin().
  • Verstümmelter Text mit verschobenem Bild. Möglicherweise haben Sie ein 128×32-Modul; ändern Sie die Höhe im Konstruktor.
  • Werte aktualisieren sich nie, aber der Status zeigt OK. Topics passen nicht zusammen. Die Wildcard + passt auf genau eine Ebene, connectmq/demo/oled-5d20ab/sensors/temp würde also nicht passen. Abonnieren Sie beim Debuggen #.
  • Display friert sekundenlang ein. Ein blockierender Reconnect. Senken Sie den Socket-Timeout oder nutzen Sie das nicht blockierende Backoff-Muster aus ESP32-MQTT-Reconnect-Logik.

Vom Demo-Panel zum Dauerdisplay

Ein Display, das an einer Werkstattwand oder als Statusanzeige an einer Fertigungslinie hängt, sollte nicht vom öffentlichen MQTT-Broker abhängen. Legen Sie im ConnectMQ-Portal einen Broker an, verbinden Sie sich mit den generierten Werten für Benutzername, Passwort und Client ID über TLS auf Port 8883 und lassen Sie alle Ihre Sensoren auf denselben Endpunkt zeigen. Mit einem Cloud-MQTT-Broker zeigt das Panel zu Hause Messwerte aus einem Gewächshaus am anderen Ende der Stadt, ganz ohne Portweiterleitung.

ConnectMQ erleichtert auch das Debugging: Der Live-Topic-Explorer zeigt genau, welche Topics existieren und was auf jedem retained ist, was die Frage "Warum ist diese Zeile leer?" meist in Sekunden beantwortet. Für eine visuellere Ausgabe neben dem Text kombinieren Sie das OLED mit einem per MQTT gesteuerten WS2812B-LED-Streifen als farbcodierte Statusleuchte.

Starten Sie mit ConnectMQ

Öffnen Sie app.connectmq.com, wählen Sie den kostenlosen Serverless-Plan (ohne Kreditkarte) und legen Sie einen Broker an. Display, Sensoren und Smartphone teilen sich dann einen sicheren Endpunkt, und Sie können während des Aufbaus jede Nachricht im Portal verfolgen.

Häufig gestellte Fragen

Kann das OLED-Display Topics anderer Geräte abonnieren?

Ja. Genau darum geht es bei MQTT: Jedes Gerät kann auf ein Topic veröffentlichen, und beliebig viele Subscriber, darunter dieses Display, empfangen es. Nur die Topic-Namen müssen übereinstimmen.

Warum sollte ich im MQTT-Callback nicht auf das OLED zeichnen?

Zeichnen über I2C dauert mehrere Millisekunden, und es können mehrere Nachrichten in schneller Folge eintreffen. Werte im Callback zu speichern und in loop() mit fester Rate zu zeichnen, hält den Client reaktionsfähig und den Bildschirm konsistent.

Funktioniert das mit SH1106-Displays?

Die 1,3-Zoll-Module nutzen oft stattdessen den SH1106-Controller. Verwenden Sie eine Bibliothek, die ihn unterstützt, etwa U8g2 oder einen SH1106-Fork des Adafruit-Treibers; der MQTT-Teil des Sketches bleibt gleich.

Wie zeige ich einen Wert aus einer JSON-Payload an?

Parsen Sie die Payload im Callback mit ArduinoJson, extrahieren Sie das benötigte Feld und speichern Sie es als Zeilenwert. Halten Sie das Parsen im Callback kurz und das Zeichnen in loop().

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.