{"id":7442,"date":"2026-09-20T07:07:50","date_gmt":"2026-09-20T07:07:50","guid":{"rendered":"https:\/\/technodidact.de\/?p=7442"},"modified":"2026-09-13T07:08:48","modified_gmt":"2026-09-13T07:08:48","slug":"vom-prototyp-zur-praxis-beispielcode-fuer-drei-cyd-displays","status":"publish","type":"post","link":"https:\/\/technodidact.de\/en\/vom-prototyp-zur-praxis-beispielcode-fuer-drei-cyd-displays\/","title":{"rendered":"Vom Prototyp zur Praxis: Beispielcode f\u00fcr drei CYD-Displays"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\">Einleitung<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Die Idee klingt bestechend: Drei identische ESP32-2432S028-Boards \u2013 liebevoll \u201eCheap Yellow Display&#8220; oder kurz CYD genannt \u2013 an einer gemeinsamen 5-Volt-Versorgung, jedes mit einer eigenen Aufgabe. Das erste zeigt Uhrzeit, Datum und Au\u00dfentemperatur, das zweite \u00fcberwacht die Signalst\u00e4rke von drei WLAN-Netzen, das dritte pr\u00e4sentiert die n\u00e4chsten Termine aus dem Google Kalender.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Doch wer bereits erste Gehversuche mit dem CYD unternommen hat, kennt die Ern\u00fcchterung: Ein Beispielsketch aus dem Internet, der auf dem einen Board fehlerfrei l\u00e4uft, produziert auf dem n\u00e4chsten nur einen schwarzen Bildschirm. Die Ursachen sind vielf\u00e4ltig \u2013 unterschiedliche Display-Treiber, abweichende Pin-Belegungen bei neueren Revisionen, knapper Arbeitsspeicher oder schlicht fehlende Bibliotheken. Der folgende Beitrag erg\u00e4nzt die bereits dokumentierte Hardware-Konfiguration um konkreten, nachvollziehbaren Beispielcode f\u00fcr alle drei geplanten Displays. Dabei steht nicht der Anspruch im Vordergrund, fertige L\u00f6sungen zu pr\u00e4sentieren, sondern die einzelnen Funktionsbl\u00f6cke so zu zeigen, dass sie sich unabh\u00e4ngig voneinander testen und anschlie\u00dfend kombinieren lassen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Warum Beispielcode in der CYD-Welt selten \u201eeinfach funktioniert&#8220;<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Bevor der eigentliche Code vorgestellt wird, lohnt ein Blick auf die strukturellen H\u00fcrden, die dieses Projekt begleiten. Wer sie kennt, spart sich manche frustrierende Stunde.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Die erste H\u00fcrde ist die <strong>Hardware-Fragmentierung<\/strong>. Das urspr\u00fcngliche CYD-2432S028 verwendete einen ILI9341-Controller. Neuere Varianten mit zwei USB-Anschl\u00fcssen (Micro-USB plus USB-C) setzen gelegentlich auf den ST7789. Ein franz\u00f6sischer Technikblog dokumentiert genau diesen Fall: Der Autor musste die Treiberdefinition in der <code>User_Setup.h<\/code> von ILI9341 auf ST7789 umstellen und die SPI-Frequenz reduzieren, bevor das Display \u00fcberhaupt ein Bild zeigte. Die bereits vorliegende Konfigurationsdatei deckt beide F\u00e4lle ab.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Die zweite H\u00fcrde ist der <strong>Arbeitsspeicher<\/strong>. Der ESP32-WROOM-32 verf\u00fcgt \u00fcber rund 520 Kilobyte SRAM. Sobald WLAN-Verbindung, HTTP-Anfragen, JSON-Verarbeitung und TFT-Rendering gleichzeitig stattfinden, wird es eng. Ein bekanntes Wi-Fi-Analyzer-Projekt auf GitHub wechselte aus diesem Grund von TFT_eSPI zur Arduino_GFX-Bibliothek, die weniger Speicher belegt. Ein chinesisches Wetteruhr-Projekt wiederum musste im Arduino-IDE das Partitionsschema auf \u201eHuge APP&#8220; umstellen, damit der kompilierte Sketch \u00fcberhaupt in den Flash-Speicher passte.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Die dritte H\u00fcrde ist die <strong>Datenanbindung<\/strong>. W\u00e4hrend WLAN-Scan und NTP-Zeitabfrage direkt auf dem ESP32 funktionieren, ist der Google Kalender ein anderes Kaliber. Die offizielle Google Calendar API erfordert OAuth-2.0-Authentifizierung mit Token-Erneuerung und TLS-verschl\u00fcsselten Anfragen \u2013 f\u00fcr einen ESP32 ohne PSRAM eine echte Herausforderung. Ein E-Paper-Kalenderprojekt, das die Google-API tats\u00e4chlich nutzt, setzt konsequenterweise auf einen ESP32-S3 mit 8 MB PSRAM und das ESP-IDF-Framework. F\u00fcr die CYD-Klasse ist der Umweg \u00fcber einen ICS-Feed oder einen selbst gehosteten JSON-Zwischendienst der realistischere Weg.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Display 1: Uhrzeit, Datum und Au\u00dfentemperatur<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Das erste Display \u00fcbernimmt die Rolle einer kompakten Wetterstation. Die Zeit wird per NTP synchronisiert, die Temperatur \u00fcber die OpenWeatherMap-API abgerufen. Beide Datenquellen sind kostenlos und f\u00fcr den ESP32 gut handhabbar.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Der vollst\u00e4ndige Beispielsketch<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">cpp<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><em>\/*<\/em>\n * CYD Display 1 \u2013 Wetterstation\n * Zeigt Uhrzeit, Datum und Au\u00dfentemperatur\n * Hardware: ESP32-2432S028 (CYD)\n *\/\n\n#include &lt;TFT_eSPI.h&gt;\n#include &lt;WiFi.h&gt;\n#include &lt;WiFiUdp.h&gt;\n#include &lt;NTPClient.h&gt;\n#include &lt;HTTPClient.h&gt;\n#include &lt;ArduinoJson.h&gt;\n#include &lt;time.h&gt;\n\nTFT_eSPI tft = TFT_eSPI();\n\n<em>\/\/ ---- Konfiguration ----<\/em>\nconst char* WLAN_SSID     = \"DEIN_WLAN_NAME\";\nconst char* WLAN_PASSWORT = \"DEIN_WLAN_PASSWORT\";\n\nconst char* OPENWEATHER_API_KEY = \"DEIN_API_KEY\";\nconst char* STADT        = \"Berlin\";\nconst char* LAND         = \"de\";\n\n<em>\/\/ Zeitzone: MEZ\/MESZ (Europa\/Berlin)<\/em>\nconst char* NTP_SERVER   = \"pool.ntp.org\";\nconst long  GMT_OFFSET_SEK = 3600;   <em>\/\/ UTC+1<\/em>\nconst int   SOMMERZEIT_OFFSET = 3600; <em>\/\/ +1h w\u00e4hrend Sommerzeit<\/em>\n\nWiFiUDP ntpUDP;\nNTPClient timeClient(ntpUDP, NTP_SERVER, GMT_OFFSET_SEK, 60000);\n\n<em>\/\/ Backlight-Pin des CYD<\/em>\n#define TFT_BL 21\n\n<em>\/\/ Aktualisierungsintervalle<\/em>\nconst unsigned long WETTER_INTERVALL_MS = 600000; <em>\/\/ 10 Minuten<\/em>\nunsigned long letzteWetterAbfrage = 0;\nfloat aktuelleTemperatur = NAN;\n\nvoid setup() {\n  Serial.begin(115200);\n  delay(200);\n\n  <em>\/\/ Backlight einschalten \u2013 h\u00e4ufigste Fehlerquelle!<\/em>\n  pinMode(TFT_BL, OUTPUT);\n  digitalWrite(TFT_BL, HIGH);\n\n  <em>\/\/ Display initialisieren<\/em>\n  tft.init();\n  tft.setRotation(1);          <em>\/\/ Querformat (320x240)<\/em>\n  tft.fillScreen(TFT_BLACK);\n  tft.setTextColor(TFT_WHITE, TFT_BLACK);\n\n  <em>\/\/ Startmeldung<\/em>\n  tft.setTextSize(2);\n  tft.setCursor(10, 10);\n  tft.println(\"Wetterstation\");\n  tft.setTextSize(1);\n  tft.setCursor(10, 40);\n  tft.println(\"Verbinde mit WLAN...\");\n\n  <em>\/\/ WLAN verbinden<\/em>\n  WiFi.mode(WIFI_STA);\n  WiFi.begin(WLAN_SSID, WLAN_PASSWORT);\n  unsigned long startVersuch = millis();\n  while (WiFi.status() != WL_CONNECTED &amp;&amp; millis() - startVersuch &lt; 20000) {\n    delay(500);\n    Serial.print(\".\");\n  }\n\n  if (WiFi.status() == WL_CONNECTED) {\n    tft.fillScreen(TFT_BLACK);\n    tft.setCursor(10, 40);\n    tft.print(\"Verbunden: \");\n    tft.println(WiFi.localIP());\n\n    <em>\/\/ NTP starten<\/em>\n    timeClient.begin();\n    timeClient.update();\n    <em>\/\/ Interne Uhr des ESP32 mit NTP-Zeit synchronisieren<\/em>\n    configTime(GMT_OFFSET_SEK, SOMMERZEIT_OFFSET, NTP_SERVER);\n  } else {\n    tft.fillScreen(TFT_BLACK);\n    tft.setCursor(10, 40);\n    tft.println(\"WLAN fehlgeschlagen!\");\n  }\n}\n\nvoid loop() {\n  <em>\/\/ NTP-Zeit aktualisieren<\/em>\n  timeClient.update();\n\n  <em>\/\/ Temperatur regelm\u00e4\u00dfig abrufen<\/em>\n  if (millis() - letzteWetterAbfrage &gt; WETTER_INTERVALL_MS\n      || letzteWetterAbfrage == 0) {\n    if (WiFi.status() == WL_CONNECTED) {\n      float neueTemp = temperaturAbrufen();\n      if (!isnan(neueTemp)) {\n        aktuelleTemperatur = neueTemp;\n      }\n    }\n    letzteWetterAbfrage = millis();\n  }\n\n  <em>\/\/ Anzeige aktualisieren<\/em>\n  anzeigeAktualisieren();\n\n  delay(1000); <em>\/\/ jede Sekunde neu zeichnen<\/em>\n}\n\n<em>\/\/ ---------- Hilfsfunktionen ----------<\/em>\n\nvoid anzeigeAktualisieren() {\n  <em>\/\/ Nur den Bereich l\u00f6schen, der sich \u00e4ndert \u2013 reduziert Flackern<\/em>\n  tft.fillRect(0, 0, 320, 240, TFT_BLACK);\n\n  <em>\/\/ Uhrzeit gro\u00df darstellen<\/em>\n  String zeit = timeClient.getFormattedTime(); <em>\/\/ Format HH:MM:SS<\/em>\n  tft.setTextColor(TFT_CYAN, TFT_BLACK);\n  tft.setTextSize(4);\n  tft.setCursor(20, 20);\n  tft.print(zeit);\n\n  <em>\/\/ Datum aus der internen Uhr<\/em>\n  struct tm zeitInfo;\n  if (getLocalTime(&amp;zeitInfo)) {\n    char datumPuffer[32];\n    strftime(datumPuffer, sizeof(datumPuffer),\n             \"%d.%m.%Y\", &amp;zeitInfo);\n    tft.setTextColor(TFT_WHITE, TFT_BLACK);\n    tft.setTextSize(2);\n    tft.setCursor(20, 80);\n    tft.print(datumPuffer);\n\n    <em>\/\/ Wochentag<\/em>\n    char wochentag[16];\n    strftime(wochentag, sizeof(wochentag), \"%A\", &amp;zeitInfo);\n    tft.setCursor(20, 110);\n    tft.print(wochentag);\n  }\n\n  <em>\/\/ Temperatur<\/em>\n  tft.setTextSize(2);\n  tft.setCursor(20, 170);\n  tft.setTextColor(TFT_YELLOW, TFT_BLACK);\n  if (!isnan(aktuelleTemperatur)) {\n    tft.print(\"Aussen: \");\n    tft.print(aktuelleTemperatur, 1);\n    tft.print(\" C\");\n  } else {\n    tft.print(\"Aussen: --.- C\");\n  }\n}\n\nfloat temperaturAbrufen() {\n  String url = String(\"http:\/\/api.openweathermap.org\/data\/2.5\/weather?q=\")\n             + STADT + \",\" + LAND\n             + \"&amp;units=metric&amp;lang=de&amp;appid=\" + OPENWEATHER_API_KEY;\n\n  HTTPClient http;\n  http.begin(url);\n  int httpCode = http.GET();\n\n  float ergebnis = NAN;\n  if (httpCode == 200) {\n    String payload = http.getString();\n    <em>\/\/ JSON-Gr\u00f6\u00dfe: OpenWeatherMap-Antwort ist ca. 500-800 Bytes<\/em>\n    DynamicJsonDocument doc(1024);\n    DeserializationError fehler = deserializeJson(doc, payload);\n    if (!fehler) {\n      ergebnis = doc[\"main\"][\"temp\"].as&lt;float&gt;();\n      const char* beschreibung = doc[\"weather\"][0][\"description\"];\n      Serial.print(\"Wetter: \");\n      Serial.print(ergebnis);\n      Serial.print(\" C, \");\n      Serial.println(beschreibung);\n    } else {\n      Serial.print(\"JSON-Fehler: \");\n      Serial.println(fehler.c_str());\n    }\n  } else {\n    Serial.print(\"HTTP-Fehler: \");\n    Serial.println(httpCode);\n  }\n  http.end();\n  return ergebnis;\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Hinweise zu diesem Sketch<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Die <strong>Sommerzeitumschaltung<\/strong> ist im gezeigten Code nicht automatisch. Der NTPClient unterst\u00fctzt keine Zeitzonendatenbank. Wer die Umstellung nicht manuell pflegen m\u00f6chte, kann auf die Funktion <code>configTime()<\/code> mit dem POSIX-Zeitzonenstring <code>\"CET-1CEST,M3.5.0,M10.5.0\/3\"<\/code> zur\u00fcckgreifen \u2013 diese Variante ist im Code bereits vorbereitet, wird aber nur f\u00fcr <code>getLocalTime()<\/code> genutzt, nicht f\u00fcr den NTPClient. Eine konsequente L\u00f6sung w\u00e4re, ganz auf <code>getLocalTime()<\/code> zu setzen und den NTPClient nur zur Erstinitialisierung zu verwenden.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Die <strong>OpenWeatherMap-API<\/strong> liefert in der kostenlosen Stufe ausreichend Abfragen (60 pro Minute, 1 Million pro Monat). Ein Abruf alle zehn Minuten ist unkritisch. Der API-Key muss nach der Registrierung einige Minuten aktiviert werden \u2013 ein h\u00e4ufiger Stolperstein.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Display 2: RSSI-Werte von drei WLAN-Netzen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Das zweite Display ist technisch das unkomplizierteste, weil es keine externe API ben\u00f6tigt. Der ESP32 scannt die Umgebung, filtert die drei Zielnetzwerke heraus und stellt deren Signalst\u00e4rke grafisch dar.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Der Beispielsketch<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">cpp<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><em>\/*<\/em>\n * CYD Display 2 \u2013 WLAN-Signalmonitor\n * Zeigt RSSI von drei konfigurierbaren Netzwerken\n *\/\n\n#include &lt;TFT_eSPI.h&gt;\n#include &lt;WiFi.h&gt;\n\nTFT_eSPI tft = TFT_eSPI();\n\n<em>\/\/ ---- Zu \u00fcberwachende Netzwerke ----<\/em>\nconst int ANZAHL_NETZE = 3;\nconst char* ZIEL_NETZE[ANZAHL_NETZE] = {\n  \"FRITZ!Box 7590\",\n  \"WLAN-Gast\",\n  \"Nachbar_AP\"\n};\n\nint rssiWerte[ANZAHL_NETZE];\nbool netzGefunden[ANZAHL_NETZE];\n\n#define TFT_BL 21\nconst unsigned long SCAN_INTERVALL_MS = 5000;\nunsigned long letzterScan = 0;\n\n<em>\/\/ Farbverlauf f\u00fcr Signalst\u00e4rke<\/em>\nuint16_t rssiFarbe(int rssi) {\n  if (rssi &gt;= -50) return TFT_GREEN;\n  if (rssi &gt;= -65) return TFT_YELLOW;\n  if (rssi &gt;= -80) return TFT_ORANGE;\n  return TFT_RED;\n}\n\nvoid setup() {\n  Serial.begin(115200);\n  pinMode(TFT_BL, OUTPUT);\n  digitalWrite(TFT_BL, HIGH);\n\n  tft.init();\n  tft.setRotation(1);\n  tft.fillScreen(TFT_BLACK);\n\n  WiFi.mode(WIFI_STA);\n  WiFi.disconnect(); <em>\/\/ nicht verbinden, nur scannen<\/em>\n\n  for (int i = 0; i &lt; ANZAHL_NETZE; i++) {\n    rssiWerte[i] = -100;\n    netzGefunden[i] = false;\n  }\n\n  tft.setTextColor(TFT_WHITE, TFT_BLACK);\n  tft.setTextSize(2);\n  tft.setCursor(10, 10);\n  tft.println(\"WLAN-Monitor\");\n}\n\nvoid loop() {\n  if (millis() - letzterScan &gt; SCAN_INTERVALL_MS || letzterScan == 0) {\n    scanDurchfuehren();\n    letzterScan = millis();\n    anzeigeZeichnen();\n  }\n  delay(100);\n}\n\nvoid scanDurchfuehren() {\n  Serial.println(\"Starte WLAN-Scan...\");\n  int gefundene = WiFi.scanNetworks();\n\n  <em>\/\/ Z\u00e4hler zur\u00fccksetzen<\/em>\n  for (int i = 0; i &lt; ANZAHL_NETZE; i++) {\n    netzGefunden[i] = false;\n  }\n\n  for (int i = 0; i &lt; gefundene; i++) {\n    String ssid = WiFi.SSID(i);\n    int rssi = WiFi.RSSI(i);\n    for (int j = 0; j &lt; ANZAHL_NETZE; j++) {\n      if (ssid == ZIEL_NETZE[j]) {\n        rssiWerte[j] = rssi;\n        netzGefunden[j] = true;\n      }\n    }\n  }\n  WiFi.scanDelete();\n}\n\nvoid anzeigeZeichnen() {\n  tft.fillScreen(TFT_BLACK);\n  tft.setTextColor(TFT_WHITE, TFT_BLACK);\n  tft.setTextSize(2);\n  tft.setCursor(10, 10);\n  tft.println(\"WLAN-Signalstaerke\");\n\n  for (int i = 0; i &lt; ANZAHL_NETZE; i++) {\n    int y = 50 + i * 60;\n\n    <em>\/\/ Netzwerkname<\/em>\n    tft.setTextSize(1);\n    tft.setTextColor(TFT_WHITE, TFT_BLACK);\n    tft.setCursor(10, y);\n    tft.print(ZIEL_NETZE[i]);\n\n    <em>\/\/ Wert<\/em>\n    tft.setCursor(10, y + 15);\n    if (netzGefunden[i]) {\n      tft.setTextColor(rssiFarbe(rssiWerte[i]), TFT_BLACK);\n      tft.print(rssiWerte[i]);\n      tft.print(\" dBm\");\n    } else {\n      tft.setTextColor(TFT_DARKGREY, TFT_BLACK);\n      tft.print(\"nicht gefunden\");\n    }\n\n    <em>\/\/ Balken<\/em>\n    balkenZeichnen(150, y + 5, rssiWerte[i], netzGefunden[i]);\n  }\n}\n\nvoid balkenZeichnen(int x, int y, int rssi, bool gefunden) {\n  const int MAX_BREITE = 150;\n  const int HOEHE = 20;\n\n  <em>\/\/ Rahmen<\/em>\n  tft.drawRect(x, y, MAX_BREITE, HOEHE, TFT_WHITE);\n\n  if (!gefunden) return;\n\n  <em>\/\/ RSSI (-100 bis -30 dBm) auf Breite abbilden<\/em>\n  int breite = map(rssi, -100, -30, 0, MAX_BREITE - 4);\n  breite = constrain(breite, 0, MAX_BREITE - 4);\n\n  tft.fillRect(x + 2, y + 2, breite, HOEHE - 4, rssiFarbe(rssi));\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Interpretation der Werte<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Die folgende Tabelle hilft bei der Einordnung der gemessenen RSSI-Werte:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">RSSI (dBm)<\/th><th class=\"has-text-align-left\" data-align=\"left\">Qualit\u00e4t<\/th><th class=\"has-text-align-left\" data-align=\"left\">Typische Situation<\/th><\/tr><\/thead><tbody><tr><td>-30 bis -50<\/td><td>Ausgezeichnet<\/td><td>Sichtverbindung, wenige Meter<\/td><\/tr><tr><td>-50 bis -65<\/td><td>Gut<\/td><td>Normale Nutzung im selben Raum<\/td><\/tr><tr><td>-65 bis -75<\/td><td>Brauchbar<\/td><td>Ein bis zwei W\u00e4nde dazwischen<\/td><\/tr><tr><td>-75 bis -85<\/td><td>Schwach<\/td><td>Mehrere W\u00e4nde, Streaming kritisch<\/td><\/tr><tr><td>-85 bis -100<\/td><td>Sehr schwach<\/td><td>Verbindungsabbr\u00fcche wahrscheinlich<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Ein wichtiger Hinweis: Der ESP32 kann im Station-Modus nicht gleichzeitig scannen und mit einem Netzwerk verbunden sein, ohne die Verbindung kurz zu unterbrechen. F\u00fcr ein reines Monitoring-Display ist das unproblematisch. Wer parallel eine WLAN-Verbindung f\u00fcr andere Zwecke ben\u00f6tigt, sollte den Scan-Intervall gro\u00dfz\u00fcgig w\u00e4hlen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Display 3: Google-Kalender-Daten<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Das dritte Display ist das anspruchsvollste. Die offizielle Google Calendar API ist f\u00fcr den ESP32 ungeeignet. Der pragmatische Weg f\u00fchrt \u00fcber einen <strong>ICS-Feed<\/strong> oder einen <strong>selbst gehosteten JSON-Dienst<\/strong>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Variante A: ICS-Feed direkt parsen<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Jeder Google Kalender bietet in den Einstellungen unter \u201eKalender integrieren&#8220; eine geheime ICS-Adresse. Diese kann der ESP32 per HTTPS abrufen und grob parsen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">cpp<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><em>\/*<\/em>\n * CYD Display 3 \u2013 Kalender (ICS-Variante)\n * Liest einen \u00f6ffentlichen ICS-Feed und zeigt die n\u00e4chsten Termine\n *\/\n\n#include &lt;TFT_eSPI.h&gt;\n#include &lt;WiFi.h&gt;\n#include &lt;HTTPClient.h&gt;\n#include &lt;WiFiClientSecure.h&gt;\n\nTFT_eSPI tft = TFT_eSPI();\n\nconst char* WLAN_SSID     = \"DEIN_WLAN\";\nconst char* WLAN_PASSWORT = \"DEIN_PASSWORT\";\n\n<em>\/\/ Geheime ICS-Adresse aus Google Kalender-Einstellungen<\/em>\nconst char* ICS_URL = \"https:\/\/calendar.google.com\/calendar\/ical\/DEINE_KALENDER_ID\/basic.ics\";\n\n#define TFT_BL 21\nconst unsigned long KALENDER_INTERVALL_MS = 900000; <em>\/\/ 15 Minuten<\/em>\nunsigned long letzteAbfrage = 0;\n\nstruct Termin {\n  String start;\n  String titel;\n};\n\nconst int MAX_TERMINE = 5;\nTermin termine[MAX_TERMINE];\nint anzahlTermine = 0;\n\nvoid setup() {\n  Serial.begin(115200);\n  pinMode(TFT_BL, OUTPUT);\n  digitalWrite(TFT_BL, HIGH);\n\n  tft.init();\n  tft.setRotation(1);\n  tft.fillScreen(TFT_BLACK);\n  tft.setTextColor(TFT_WHITE, TFT_BLACK);\n  tft.setTextSize(2);\n  tft.setCursor(10, 10);\n  tft.println(\"Kalender\");\n\n  WiFi.begin(WLAN_SSID, WLAN_PASSWORT);\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(500);\n    Serial.print(\".\");\n  }\n  Serial.println(\"\\nWLAN verbunden.\");\n}\n\nvoid loop() {\n  if (millis() - letzteAbfrage &gt; KALENDER_INTERVALL_MS || letzteAbfrage == 0) {\n    if (WiFi.status() == WL_CONNECTED) {\n      kalenderAbrufen();\n      anzeigeZeichnen();\n    }\n    letzteAbfrage = millis();\n  }\n  delay(1000);\n}\n\nvoid kalenderAbrufen() {\n  WiFiClientSecure client;\n  client.setInsecure(); <em>\/\/ Vereinfachung \u2013 f\u00fcr Produktion Zertifikat setzen!<\/em>\n\n  HTTPClient http;\n  http.begin(client, ICS_URL);\n  int httpCode = http.GET();\n\n  if (httpCode != 200) {\n    Serial.print(\"HTTP-Fehler: \");\n    Serial.println(httpCode);\n    http.end();\n    return;\n  }\n\n  String ics = http.getString();\n  http.end();\n\n  <em>\/\/ Grobes Parsen: nach VEVENT-Bl\u00f6cken suchen<\/em>\n  anzahlTermine = 0;\n  int pos = 0;\n  while (anzahlTermine &lt; MAX_TERMINE) {\n    int startIdx = ics.indexOf(\"BEGIN:VEVENT\", pos);\n    if (startIdx &lt; 0) break;\n    int endIdx = ics.indexOf(\"END:VEVENT\", startIdx);\n    if (endIdx &lt; 0) break;\n\n    String block = ics.substring(startIdx, endIdx);\n\n    <em>\/\/ DTSTART parsen<\/em>\n    int dtStartIdx = block.indexOf(\"DTSTART\");\n    String startWert = \"\";\n    if (dtStartIdx &gt;= 0) {\n      int doppelpunkt = block.indexOf(':', dtStartIdx);\n      int zeilenende = block.indexOf('\\n', doppelpunkt);\n      startWert = block.substring(doppelpunkt + 1, zeilenende);\n      startWert.trim();\n    }\n\n    <em>\/\/ SUMMARY parsen<\/em>\n    int sumIdx = block.indexOf(\"SUMMARY:\");\n    String titel = \"\";\n    if (sumIdx &gt;= 0) {\n      int zeilenende = block.indexOf('\\n', sumIdx);\n      titel = block.substring(sumIdx + 8, zeilenende);\n      titel.trim();\n    }\n\n    if (startWert.length() &gt; 0) {\n      termine[anzahlTermine].start = formatIcsDatum(startWert);\n      termine[anzahlTermine].titel = titel;\n      anzahlTermine++;\n    }\n\n    pos = endIdx + 10;\n  }\n\n  Serial.print(\"Termine gefunden: \");\n  Serial.println(anzahlTermine);\n}\n\nString formatIcsDatum(String icsDatum) {\n  <em>\/\/ ICS-Format: 20260315T143000Z oder 20260315<\/em>\n  <em>\/\/ Ausgabe: 15.03. 14:30<\/em>\n  icsDatum.trim();\n  if (icsDatum.length() &lt; 8) return icsDatum;\n\n  String tag   = icsDatum.substring(6, 8);\n  String monat = icsDatum.substring(4, 6);\n  String jahr  = icsDatum.substring(0, 4);\n  String zeit  = \"\";\n\n  if (icsDatum.length() &gt;= 13) {\n    zeit = icsDatum.substring(9, 11) + \":\" + icsDatum.substring(11, 13);\n  }\n\n  String ergebnis = tag + \".\" + monat + \".\";\n  if (zeit.length() &gt; 0) ergebnis += \" \" + zeit;\n  return ergebnis;\n}\n\nvoid anzeigeZeichnen() {\n  tft.fillScreen(TFT_BLACK);\n  tft.setTextColor(TFT_WHITE, TFT_BLACK);\n  tft.setTextSize(2);\n  tft.setCursor(10, 10);\n  tft.println(\"Naechste Termine:\");\n\n  tft.setTextSize(1);\n  int y = 45;\n  if (anzahlTermine == 0) {\n    tft.setCursor(10, y);\n    tft.println(\"Keine Termine gefunden.\");\n    return;\n  }\n\n  for (int i = 0; i &lt; anzahlTermine; i++) {\n    tft.setTextColor(TFT_CYAN, TFT_BLACK);\n    tft.setCursor(10, y);\n    tft.print(termine[i].start);\n\n    tft.setTextColor(TFT_WHITE, TFT_BLACK);\n    tft.setCursor(10, y + 12);\n    String titel = termine[i].titel;\n    if (titel.length() &gt; 40) titel = titel.substring(0, 40) + \"...\";\n    tft.print(titel);\n\n    y += 35;\n    if (y &gt; 220) break;\n  }\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Variante B: JSON-Zwischendienst<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Die ICS-Variante ist einfach, hat aber Grenzen: Sie sortiert Termine nicht nach Aktualit\u00e4t und st\u00fcrzt sich auf den ersten Block im Feed, der nicht unbedingt der n\u00e4chste Termin ist. Wer mehr Kontrolle m\u00f6chte, betreibt auf einem Raspberry Pi oder einem kleinen Heimserver ein Python-Skript, das die Google Calendar API nutzt und die n\u00e4chsten Termine als schlankes JSON bereitstellt. Der ESP32 ruft dann nur noch diese eine URL ab.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ein solches Zwischenskript k\u00f6nnte mit der <code>google-api-python-client<\/code>-Bibliothek und einem Service-Account-Konto arbeiten. Der ESP32-Seitencode reduziert sich dann auf einen HTTP-Abruf und eine JSON-Verarbeitung, wie sie bereits im Wetter-Display gezeigt wurde.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Stromversorgung der drei Displays<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Die gemeinsame 5-Volt-Versorgung ist elektrisch unkompliziert, verlangt aber Aufmerksamkeit bei der Dimensionierung. Die folgende Tabelle fasst den Bedarf zusammen:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Zustand<\/th><th class=\"has-text-align-left\" data-align=\"left\">Strom pro CYD<\/th><th class=\"has-text-align-left\" data-align=\"left\">Strom f\u00fcr 3 CYDs<\/th><\/tr><\/thead><tbody><tr><td>Nur Display, Backlight an<\/td><td>ca. 150 mA<\/td><td>ca. 450 mA<\/td><\/tr><tr><td>WLAN aktiv, Empfang<\/td><td>ca. 220 mA<\/td><td>ca. 660 mA<\/td><\/tr><tr><td>WLAN-Scan (Spitzen)<\/td><td>ca. 300 mA<\/td><td>ca. 900 mA<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Ein Netzteil mit <strong>5 V und mindestens 2 A<\/strong> bietet ausreichend Reserve. Wichtig ist, dass alle drei Boards <strong>parallel<\/strong> an dieselbe 5-V-Schiene angeschlossen werden \u2013 nicht in Reihe. Die VIN- beziehungsweise 5V-Pins der Boards werden miteinander verbunden, ebenso alle GND-Pins. Die 3,3-V-Pins d\u00fcrfen nicht f\u00fcr die Versorgung genutzt werden.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ein Hinweis zur <strong>W\u00e4rmeentwicklung<\/strong>: Der AMS1117-Spannungsregler auf dem CYD erw\u00e4rmt sich bei 5 V Eingangsspannung sp\u00fcrbar. Bei drei Boards in einem geschlossenen Geh\u00e4use sollte f\u00fcr Luftzirkulation gesorgt werden.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Fazit und Ausblick<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Die drei vorgestellten Beispielsketches zeigen, dass jedes der geplanten Displays mit \u00fcberschaubarem Codeaufwand realisierbar ist \u2013 vorausgesetzt, man akzeptiert die Grenzen des ESP32 und sucht keine L\u00f6sungen, die f\u00fcr leistungsf\u00e4higere Hardware gedacht sind. Das Wetter-Display ist mit NTP und OpenWeatherMap in einer halben Stunde zum Laufen zu bringen. Der WLAN-Monitor ist noch simpler. Der Kalender ist der anspruchsvollste Teil, und hier entscheidet die Wahl des Datenwegs \u00fcber den Aufwand: Der ICS-Feed ist schnell eingerichtet, aber in der Auswertung grob. Der JSON-Umweg \u00fcber einen eigenen Server kostet mehr Einrichtungszeit, liefert aber saubere Daten.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00fcr die Zukunft lohnt sich ein Blick auf die <strong>LVGL-Bibliothek<\/strong>. Sie bietet fertige Widgets und eine deutlich komfortablere Oberfl\u00e4chengestaltung als die direkte Pixelarbeit mit TFT_eSPI. Der Preis ist ein h\u00f6herer Speicherbedarf \u2013 hier st\u00f6\u00dft der ESP32-WROOM-32 ohne PSRAM schnell an seine Grenzen. Wer mehr m\u00f6chte, sollte beim n\u00e4chsten CYD-Kauf auf eine Variante mit ESP32-S3 achten.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Die Dokumentation der Hardware-Konfiguration, die diesem Projekt zugrunde liegt, bleibt g\u00fcltig. Der hier erg\u00e4nzte Code ist als Ausgangspunkt gedacht: Er l\u00e4uft, wenn die WLAN-Zugangsdaten und API-Schl\u00fcssel eingetragen sind, und er l\u00e4sst sich modular erweitern. Wer alle drei Displays nebeneinander betreibt, hat am Ende ein kleines Informationssystem, das ohne Cloud-Zwang und ohne Abonnement funktioniert \u2013 und das ist bei aller Bastelarbeit ein durchaus befriedigendes Ergebnis.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Quellen:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>witnessmenow: ESP32-Cheap-Yellow-Display \u2013 GitHub-Repository mit Hardware-Dokumentation und Beispielprojekten<\/li>\n\n\n\n<li>Bodmer: TFT_eSPI \u2013 Bibliotheksdokumentation und User_Setup-Konfiguration<\/li>\n\n\n\n<li>OpenWeatherMap: Aktuelle Wetterdaten-API, Dokumentation und Preisgestaltung<\/li>\n\n\n\n<li>NTPClient-Bibliothek von Fabrice Weinberg \u2013 Zeitsynchronisation f\u00fcr Arduino\/ESP32<\/li>\n\n\n\n<li>ArduinoJson von Beno\u00eet Blanchon \u2013 JSON-Verarbeitung auf Mikrocontrollern<\/li>\n\n\n\n<li>Google Calendar Hilfe: \u201eKalender integrieren&#8220; \u2013 ICS-Feed-Adressen und deren Verwendung<\/li>\n\n\n\n<li>Random Nerd Tutorials: ESP32 NTP-Client und Wetterstation-Projekte<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Einleitung Die Idee klingt bestechend: Drei identische ESP32-2432S028-Boards \u2013 liebevoll \u201eCheap Yellow Display&#8220; oder kurz CYD genannt \u2013 an einer gemeinsamen 5-Volt-Versorgung, jedes mit einer eigenen Aufgabe. Das erste zeigt Uhrzeit, Datum und Au\u00dfentemperatur, das zweite \u00fcberwacht die Signalst\u00e4rke von drei WLAN-Netzen, das dritte pr\u00e4sentiert die n\u00e4chsten Termine aus dem Google Kalender. Doch wer bereits [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[41,42,17,26],"tags":[1170,1353,2225,10959,10962,10961,6966,10960],"class_list":["post-7442","post","type-post","status-publish","format-standard","hentry","category-digitalkultur","category-elektrotechnik","category-im-herz","category-mit-den-handen","tag-cheap-yellow-display","tag-cyd","tag-esp32","tag-google-kalender","tag-ics-feed","tag-ntp","tag-tft_espi","tag-wlan-rssi"],"_links":{"self":[{"href":"https:\/\/technodidact.de\/en\/wp-json\/wp\/v2\/posts\/7442","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/technodidact.de\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/technodidact.de\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/technodidact.de\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/technodidact.de\/en\/wp-json\/wp\/v2\/comments?post=7442"}],"version-history":[{"count":3,"href":"https:\/\/technodidact.de\/en\/wp-json\/wp\/v2\/posts\/7442\/revisions"}],"predecessor-version":[{"id":7445,"href":"https:\/\/technodidact.de\/en\/wp-json\/wp\/v2\/posts\/7442\/revisions\/7445"}],"wp:attachment":[{"href":"https:\/\/technodidact.de\/en\/wp-json\/wp\/v2\/media?parent=7442"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/technodidact.de\/en\/wp-json\/wp\/v2\/categories?post=7442"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/technodidact.de\/en\/wp-json\/wp\/v2\/tags?post=7442"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}