In dieser Anleitung lernst du ePaper-Displays mit Arduino-Code anzusteuern und per MQTT sogar mit dynamischen Inhalten zu versorgen. ePaper-Displays brauchen nur dann Strom, wenn sie die Anzeige ändern. Dazwischen behalten sie über lange Zeiträume die Anzeige, ohne Strom zu verbrauchen.

Hardware-Voraussetzungen

die abgebildeten Preise sind tagesaktuell am 17. August 2021
Als Amazon-Partner verdiene ich an qualifizierten Verkäufen, wenn du die Links verwendest und die Produkte kaufst. Das kostet nicht mehr als wenn du sie einfach so kaufst und hilft mir mehr Zeit für coole Tutorials aufzubringen. Wenn du das nicht möchtest, sind daneben jeweils die Links ohne Partnerprogramm gelistet.

Software-Voraussetzungen

Falls nicht klar ist, wie diese ganzen Voraussetzungen installiert werden müssen, schau doch mal im vierten Blogpost meiner Mosquitto MQTT Reihe nach, dort ist es erklärt.

Generisches ESP32-MQTT-template

Der heruntergeladene (https://github.com/sisch/esp_mqtt_rfid_tutorial) Ordner ESP-template enthält alles, was du benötigst, um mit deinem ESP loszulegen. Der Code ist so geschrieben, dass du nur 3 Stellen anpassen musst und der Rest sofort lauffähig ist. Denn der ESP startet nach dem Upload einen Access Point mit einem Konfigurationsportal. Damit kannst du den ESP in dein Heim-WiFi einbinden und sobald das passiert ist, verbindet sich der ESP immer automatisch neu. Sollte das mal nicht klappen, wird wieder der Konfigurationsaccesspoint gestartet.

Die erste Stelle, die du anschauen solltest ist Zeile 16. Möchtest du mit Zertifikaten arbeiten und deine Kommunikation mit dem MQTT-Broker verschlüsseln, musst du hier nichts ändern, aber deine Zertifikate in der Datei certs_available.h im gleichen Ordner einfügen (Im Template sind hier nur Platzhalter drin). Möchtest du auf Verschlüsselung verzichten, kannst du Zeile 16 auskommentieren.

Die zweite Stelle ist Zeile 152. Hier findest du einen auskommentierten Beispielcode um auf einem MQTT-Topic zu subscriben (Wo kommt das Topic her? Es ist konfigurierbar im Konfigurationsportal vom ESP). Wenn du auf ein Topic reagieren möchtest, musst du nur die Kommentarzeichen in Zeile 153 löschen und irgendwo eine Funktion messageArrived mit der Signatur void messageArrived(MQTT::MessageData& md){} anlegen, die bestimmt, was passieren soll, wenn eine Nachricht ankommt.

Die dritte Stelle ist Zeile 264. Der hier folgende Code erlaubt dir Nachrichten vom ESP an den MQTT-Broker zu publishen. Um das auszuprobieren, lösche einfach die Kommentarzeichen in Zeile 265. Der ESP wird dann "Hello World!" unter dem im Konfigurationsportal angegebenen Topic publishen.

Verkabelung des ePaper-Displays

Abb. 1: Verkabelung des ESP32 mit dem ePaper-Display
ESP32-Pin ePaper-Pin
D4 BUSY
D16 RST
D17 DC
D5 CS
D18 CLK
D23 DIN
GND GND
3V3 VCC

Achtung: Nicht jedes ESP32 Dev Modul hat genau die gleichen Pinbelegungen wie die Abbildung oben. Maßgeblich für korrekte Verkabelung ist die Tabelle!

ePaper-Display Code

Um das ePaper-Display komfortabel mit Bildern oder Text zu bespielen, musst du ein paar Funktionen per #include importieren. Weil das für jedes Display etwas anders ist, wäre es hilfreich immer alle Optionen auskommentiert zur Hand zu haben und nur das Display einzukommentieren, was du gerade benutzt.

Daher ist der erste Schritt, dass du eine Textdatei mit folgendem Inhalt erstellst. Das sind alle kompatiblen Schwarzweiß-Displays mit dem oben verlinkten 2,9 Zoll Display voreingestellt

Datei: epaper_board.h

// ePaper Board used, uncomment the one you are using

//GxEPD2_BW<GxEPD2_154, GxEPD2_154::HEIGHT> display(GxEPD2_154(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEP015OC1 no longer available
//GxEPD2_BW<GxEPD2_154_D67, GxEPD2_154_D67::HEIGHT> display(GxEPD2_154_D67(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEH0154D67
//GxEPD2_BW<GxEPD2_154_T8, GxEPD2_154_T8::HEIGHT> display(GxEPD2_154_T8(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEW0154T8 152x152
//GxEPD2_BW<GxEPD2_154_M09, GxEPD2_154_M09::HEIGHT> display(GxEPD2_154_M09(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEW0154M09 200x200
//GxEPD2_BW<GxEPD2_154_M10, GxEPD2_154_M10::HEIGHT> display(GxEPD2_154_M10(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEW0154M10 152x152
//GxEPD2_BW<GxEPD2_213, GxEPD2_213::HEIGHT> display(GxEPD2_213(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDE0213B1, phased out
//GxEPD2_BW<GxEPD2_213_B72, GxEPD2_213_B72::HEIGHT> display(GxEPD2_213_B72(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEH0213B72
//GxEPD2_BW<GxEPD2_213_B73, GxEPD2_213_B73::HEIGHT> display(GxEPD2_213_B73(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEH0213B73
//GxEPD2_BW<GxEPD2_213_B74, GxEPD2_213_B74::HEIGHT> display(GxEPD2_213_B74(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEM0213B74
//GxEPD2_BW<GxEPD2_213_flex, GxEPD2_213_flex::HEIGHT> display(GxEPD2_213_flex(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEW0213I5F
//GxEPD2_BW<GxEPD2_213_M21, GxEPD2_213_M21::HEIGHT> display(GxEPD2_213_M21(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEW0213M21
//GxEPD2_BW<GxEPD2_213_T5D, GxEPD2_213_T5D::HEIGHT> display(GxEPD2_213_T5D(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEW0213T5D
GxEPD2_BW<GxEPD2_290, GxEPD2_290::HEIGHT> display(GxEPD2_290(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEH029A1
//GxEPD2_BW<GxEPD2_290_T5, GxEPD2_290_T5::HEIGHT> display(GxEPD2_290_T5(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEW029T5
//GxEPD2_BW<GxEPD2_290_T5D, GxEPD2_290_T5D::HEIGHT> display(GxEPD2_290_T5D(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEW029T5D
//GxEPD2_BW<GxEPD2_290_T94, GxEPD2_290_T94::HEIGHT> display(GxEPD2_290_T94(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEM029T94
//GxEPD2_BW<GxEPD2_290_T94_V2, GxEPD2_290_T94_V2::HEIGHT> display(GxEPD2_290_T94_V2(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEM029T94, Waveshare 2.9" V2 variant
//GxEPD2_BW<GxEPD2_290_M06, GxEPD2_290_M06::HEIGHT> display(GxEPD2_290_M06(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEW029M06
//GxEPD2_BW<GxEPD2_260, GxEPD2_260::HEIGHT> display(GxEPD2_260(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEW026T0
//GxEPD2_BW<GxEPD2_260_M01, GxEPD2_260_M01::HEIGHT> display(GxEPD2_260_M01(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEW026M01
//GxEPD2_BW<GxEPD2_270, GxEPD2_270::HEIGHT> display(GxEPD2_270(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEW027W3
//GxEPD2_BW<GxEPD2_371, GxEPD2_371::HEIGHT> display(GxEPD2_371(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEW0371W7
//GxEPD2_BW<GxEPD2_420, GxEPD2_420::HEIGHT> display(GxEPD2_420(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEW042T2
//GxEPD2_BW<GxEPD2_420_M01, GxEPD2_420_M01::HEIGHT> display(GxEPD2_420_M01(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEW042M01
//GxEPD2_BW<GxEPD2_583, GxEPD2_583::HEIGHT> display(GxEPD2_583(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEW0583T7
//GxEPD2_BW<GxEPD2_583_T8, GxEPD2_583_T8::HEIGHT> display(GxEPD2_583_T8(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEW0583T8  648x480
//GxEPD2_BW<GxEPD2_750, GxEPD2_750::HEIGHT> display(GxEPD2_750(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEW075T8   640x384
//GxEPD2_BW<GxEPD2_750_T7, GxEPD2_750_T7::HEIGHT> display(GxEPD2_750_T7(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEW075T7 800x480
//GxEPD2_BW < GxEPD2_1160_T91, GxEPD2_1160_T91::HEIGHT / 2> display(GxEPD2_1160_T91(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEH116T91 960x640

Außerdem will ich dir zeigen, wie du Bilder anzeigen kannst, deshalb bereite bitte eine Datei bitmaps.h mit folgendem Inhalt vor, darin findet sich ein Beispielbild, das mit dem Generator image2cpp erstellt wurde:  

https://pastebin.com/aRCNaLaw

Include Anweisungen

Mit all den Dateien vorbereitet, kannst du dich dem Sketch widmen und alles importieren, was du benötigst um mit dem ePaper zu arbeiten. Öffne den Sketch in der Arduino IDE und füge die folgenden Zeilen im Kopfbereich ans Ende der includes ein:

// ePaper includes
#include <GxEPD2_BW.h>
#include "epaper_board.h"
#include "bitmaps.h"

// font gfx include
#include <Fonts/FreeMonoBold9pt7b.h>

Zeile 23 bindet die Bibliothek zur Steuerung des Displays ein
Zeile 24 liefert die displayVariable je nachdem welchen Displaytyp du in der Datei epaper_boards.h einkommentiert hast
Zeile 25 bindet das Beispielbild in einer Variable mit dem Namen epd_bitmap_image ein.
Zeile 28 lädt eine Schriftart, damit du Text schreiben kannst. NB: Für eine Auswahl der Schriftarten siehe erstes Code-Listing unter https://learn.adafruit.com/adafruit-gfx-graphics-library/using-fonts

Initialisierung

Die display-Variable ist schon durch den include definiert, das heißt du kannst zur ersten Anzeige in der setup-Methode ganz am Ende Code einfügen, der zum Beispiel das oben definierte Bild anzeigt.

NB: Wenn du die Anzeige machen möchtest, BEVOR das Konfigurationsportal startest, kannst du den Code auch am Anfang von setup einfügen
  // epaper Init
  display.init();
  display.setRotation(1);
  display.drawBitmap(0, 0, epd_bitmap_image, 296, 128, GxEPD_BLACK, GxEPD_WHITE);
  display.display();

Zeile 2 initialisiert die Kommunikation (für Interessierte: es nutzt SPI) zwischen ESP und Display.
Zeile 3 gibt die Rotation an (Werte 0-3 geben 90 Grad Rotationen an). 1 bedeutet Querformat mit ESP-Kabeln auf der linken Seite.
Zeile 4 ändert jetzt die Pixel, die in bitmaps.h angegebenen sind auf schwarz. drawBitmap bekommt dafür die Position x, y, das Bitmap epd_bitmap_image, Breite und Höhe w, h, und schließlich die Vordergrund- und Hintergrundfarbe die gesetzt werden sollen GxEPD_BLACK, GxEPD_BLACK. Achtung: Da ich das Bild gerne weiß auf schwarz haben wollte und das Bitmap invertiert war, habe ich Vordergrundfarbe und Hintergrundfarbe vertauscht. Das drawBitmap alleine schreibt nur in den Bildpuffer, einen internen Speicher, der auf dem Display erst einmal angezeigt werden muss.
Zeile 5 sagt dem Display, dass es sich aktualisieren soll. Das heißt der Bildpuffer wird tatsächlich auf dem Display angezeigt. Es gibt noch Möglichkeiten nur Teile des Bilds zu aktualisieren und in sogenannte Pages aufzuteilen, das ist aber nicht Teil dieser Anleitung.

Das ist schon alles, je nachdem, ob du es vor oder nach dem Netzwerk-Initialisierungs-Code eingefügt hast, siehst du das Ergebnis nach dem Upload sofort oder spätestens nach der WiFi-Konfiguration

Auf MQTT reagieren

Um jetzt deine eigenen MQTT Nachrichten auf dem Display anzuzeigen, benötigst du zwei Änderungen im Code.

  1. Muss der MQTT-Client auf einem Topic subscriben
  2. Muss es eine Funktion geben, die aufgerufen werden kann, wenn eine Nachricht über MQTT ankommt.

Für Punkt 1 gehst du im Sketch in der connect-Funktion an die Stelle "// Insert MQTT subscription code here" (im Original Template Zeile 152) und löschst einfach die Kommentarzeichen /* in der nächsten Zeile.

  // insert MQTT subscription code here
  
  rc = client.subscribe(mqtt_topic, MQTT::QOS1, messageArrived);   
  if (rc != 0)
  {
    Serial.print("rc from MQTT subscribe is ");
    Serial.println(rc);
  }
  Serial.print("MQTT subscribed to ");
  Serial.println(mqtt_topic); 
  // */

Nur Zeile 3 ist relevant für MQTT, der Rest sind Ausgaben auf dem Seriellen Monitor, um Fehlersuche zu erleichtern. Jetzt lauscht der MQTT-Client auf das Topic, welches du im Konfigurationsportal einstellst / eingestellt hast. NB: Das hängt auch mit der Zeile client.yield() im loop zusammen. Ohne diese Zeile werden eingehende Nachrichten nicht verarbeitet.

Wenn eine Nachricht ankommt, wird die Funktion messageArrived aufgerufen. Diese Funktion kannst du neu hinzufügen, zum Beispiel direkt oberhalb von void setup()

void messageArrived(MQTT::MessageData& md)
{
  Serial.println("received Message");
  char payload[1024];
  memset(payload, '\00', 1024);
  MQTT::Message &message = md.message;
  strncpy(payload, (char*)message.payload, message.payloadlen);
  
  display.setFont(&FreeMonoBold9pt7b);
  display.setTextColor(GxEPD_BLACK);
  display.fillScreen(GxEPD_WHITE);
  uint16_t x = 10;
  uint16_t y = 10;
 
  display.setCursor(x, y);
  display.print(payload);
  display.display();
}

Die empfangene Nachricht befindet sich im MessageData Objekt als message Eigenschaft. Da wir hauptsächlich am Payload der Nachricht interessiert sind, wird in Zeile 6 die Message extrahiert und in Zeile 7 in die Variable "payload" kopiert. ABER.... In Arduino Code sind Texte (char*) nur Zeiger auf Speicheradressen und wenn du mehrere Nachrichten empfängst, bleiben Inhalte von längeren im Speicher, selbst wenn du danach kürzere Nachrichten anzeigen möchtest. Deshalb muss die Variable in Zeile 4 deklariert und in Zeile 5 jedesmal auf leeren Speicher reinitialisiert werden. Normalerweise hätte es sonst einfacher, nämlich ungefähr so ausgesehen char* payload = (char*)message.payload;.

Zeilen 9-11 sorgen dafür, dass eine Schriftart verwendet wird (die hast du oben per #include importiert), die Schriftfarbe auf Schwarz gesetzt und der Hintergrund mit weiß reinitialisiert wird.
Zeilen 12&13 geben die Position an, an der der Text platziert werden soll. Hierbei gilt x ist bei der oben gewählten Rotation ein Wert zwischen 0 (links) und 296 (rechts) und y zwischen 0 (oben) und 128 (unten). Der Text wird an der angegebenen Position mit der oberen linken Ecke (der rechteckigen Texthülle) platziert.

Zeile 15 setzt den Cursor an die in x und y angegebene Position.
Zeile 16 schreibt den Text der als Payload angekommen ist in den Bildpuffer des ePaper-Displays.
Und Zeile 17 aktualisiert schließlich das Display, sodass der Text sichtbar wird.

Bonus-Code

Wenn du den Text mittig zentrieren möchtest, geht das auch. Der Code ist nicht von mir, sondern aus dem Beispiel der GxEDP2-Bibliothek kopiert. Füge den folgenden Code zwischen Zeile 11 und 13 ein und die Werte für x und y werden so überschrieben, dass der Text mittig zentriert erscheint.

  int16_t tbx, tby; uint16_t tbw, tbh;
  display.getTextBounds(msg, 0, 0, &tbx, &tby, &tbw, &tbh);
  // center the bounding box by transposition of the origin:
  x = ((display.width() - tbw) / 2) - tbx;
  y = ((display.height() - tbh) / 2) - tby;
  // Hier geht es mit setCursor von oben weiter ...

getTextBounds versucht hierbei für den gegebenen Text die rechteckige Hülle zu berechnen und bestimmt dann den Mittelpunkt des Bildschirms abzüglich der halben Hülle.

Funktionsübersicht

Funktions Beschreibung
display.drawPixel(x, y, Farbe) Einzelne Pixel setzen
display.fillScreen(Farbe) kompletten Bildschirm einfärben
display.setCursor(x, y) Cursor an bestimmte Stelle setzen
display.print(char*) Text an Cursorposition drucken
display.SetTextColor(Farbe) Textfarbe festlegen
display.setFont(&FontName) Schriftart festlegen
display.display() Zeigt die vorbereiteten Daten an
display.clearScreen( ) Leert den Screen (auf weiß)
display.powerOff() Schaltet Display Spannugn aus (dadurch verblasst das Bild langsamer
display.hibernate() Wie powerOff, Unterschied unklar

Anwendungsfälle

Was kannst du mit dem Display alles machen? Im Prinzip alles, aber weil das nicht hilft, hier ein paar Ideen:

  • Eine Auswahl von Bildern in bitmaps.h, die per MQTT-Nachricht (ID) aufgerufen werden können.
  • Nachrichten als Text anzeigen (Motivationssprüche, ToDos, Wetter, ...)
  • Einzelne Pixel zeichnen per MQTT, zum Beispiel payload "110 15 1" um den Pixel bei 110, 15 auf Schwarz zu setzen. Dafür muss die Nachricht geparst werden.
  • Falls ein öffentlicher MQTT-Broker vorhanden ist, kannst du den ESP samt Display an jemand anders verschicken und hin und wieder Nachrichten dorthin schicken. NB: Vergiss dann nicht eine kurze Anleitung beizulegen, wie der ESP konfiguriert sein muss.
  • Für Home-Automation könntest du auf dem ePaper anzeigen, welche Geräte gerade an-/ausgeschaltet sind bzw. welchen Zustand sie haben.

Bei mir wird das Display vermutlich im Badezimmer neben dem Spiegel Platz finden und mir morgens zwei Nachrichten-Headlines und das Wetter für den Tag anzeigen. Ob ich dafür MQTT benutze, weiß ich noch nicht.

Ich hoffe es hat dir Spaß gemacht, mit dem ePaper zu arbeiten. Dadurch, dass die Displays zwischen den Aktualisierungen ausgeschaltet werden können, ist es eine sehr batteriesparende Anwendung und ein ESP32 läuft an einer herkömmlichen Powerbank ein paar Tage. Falls du noch Fragen und oder Anmerkungen hast, schreib mir unter info@ oder auf Twitter.