Hier der letzte Teil der ESP32 und CYD Reihe: Nun zeige ich abschließend wie das ganze mit dem kostenlose GUI Erstellungstool EEZ Studio zum laufen gebracht werden kann.
Im ersten Teil dieser CYD‑Reihe habe ich gezeigt, wie man mit TFT_eSPI bereits beeindruckende Grafiken, Buttons und Touch‑Elemente direkt per Pixelkoordinaten umsetzt. Im zweiten Teil ging es dann um LVGL, das diese manuelle Pixelarbeit durch ein modernes Widget‑System ersetzt – inklusive eigener Grafiken, Icons, Animationen und sauberer Layouts.

Dieser dritte Teil setzt beide Grundlagen voraus. Damit du hier starten kannst, sollten folgende Punkte bereits funktionieren:
- das CYD‑Display mit korrekt angepasster TFT_eSPI‑User_Setup.h,
- eine funktionierende LVGL‑Installation auf dem ESP32,
- grundlegende Kenntnisse aus Teil 2 (Widgets, Styles, Events, Animationen).
Wichtig: Du benötigst außerdem eine korrekt konfigurierte lv_conf.h, denn sie legt fest,
- welche LVGL‑Features aktiv sind,
- welche Widgets verfügbar sind,
- ob Animationen, Image‑Decoders, Fonts oder Themes eingeschaltet sind,
- und wie groß der interne Speicher für Grafiken und Buffers ist.
Erst wenn diese Basis steht, kannst du mit diesem dritten Teil beginnen. Hier zeige ich dir, wie du mit EEZ Studio komplette LVGL‑Oberflächen visuell erstellst, exportierst und auf dem CYD‑ESP32 zum Laufen bringst – ohne selbst UI‑Code schreiben zu müssen.
Das EEZ Studio Tool
EEZ Studio ist ein kostenloses Werkzeug zum Erstellen einer GUI (Graphical User Interface = grafische Benutzeroberfläche). Für den ESP32 nutzt EEZ Studio im Hintergrund meistens die LVGL (Light and Versatile Graphics Library). Das ist eine extrem mächtige Grafikbibliothek, die Widgets wie Tachos, Schieberegler, transparente Bilder und sanfte Animationen unterstützt.

Detailierte Dokumentation zum Tool –> EEZ Studio docs
Lade dir EEZ Studio herunter und installiere es.
Zusätzlich solltet ihr Python installieren: Falls noch nicht geschehen, ladet euch Python von python.org herunter und installiert es.Tipp für die Installation: Setzt im ersten Fenster unbedingt den Haken bei „Add python.exe to PATH“!
Erweiterungen hinzufügen: Öffnet die Windows-Eingabeaufforderung (CMD) und installiert die fehlenden Pakete pypng und lz4 mit diesem Befehl: pip install pypng lz4
Neustart: Sofern EEZ schon am laufen. Schließt EEZ Studio einmal komplett und startet es neu.
Ablauf EEZ Studio:
Hier nun eine Schritt für Schritt Anleitung wie ich erst mal eine einfache Wetteranzeige erstellt habe
Starte EEZ Studio und erstelle ein neues Projekt mit LVGL (Version 9.5).

Unter Settings/General setze die Auflösung auf 480 x 320 Pixel (Passend zum Display).

Nun unter Build den Ordner deines Sketches (später nochmal kontrollieren) eintragen und bei „LVGL include“ auf lvgl.h ändern.

Erstellung:
Um erstmal nur eine Überschrift mit abgerufener Temperatur aus dem Internet darzustellen, wählen wir nun den Reiter Main aus und links erscheint unter Properties nun eine Vielzahl an Möglichkeiten.
Dort nochmal die Größe des Display, in diesem Fall 480×320 einstellen. Hier erstelle ich auch erstmal eine andere Hintergrund Farbe unter Style/Default/Background mit dem Farbauswähler.

Unten Rechts aus Components Palette, ziehe ich mir zweimal das Label in die Oberfläche und benenne diese jeweils mit eindeutigen Namen.

Darunter unter Style im Feld Text ggf. Farbe und Schriftgröße auswählen.
In meinem Beispiel „Grün“ und bei FONT „MONTSERRAT_48“
Hier müsst ihr euch aber schon mal merken, das die gewählten Schriftgröße, auch später in der noch zu installierenden Library noch aktiviert werden müssen. Dazu weiter unten aber später mehr.

Ablauf Arduino IDE:
Neuen Sketch erstellen (dort wo im EEZ Studio unter Settings/Build eingetragen)
und als Board ESP32 Dev Module an eurem COM Port auswählen.
Dann unter Werkzeuge das Partition Scheme auf Huge App (3MB NoOTA/1MB SPIFFS) ändern

Angezeigten Sketchinhalt löschen
und diesen Sketch einfügen: (anklicken zum öffnen)
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <lvgl.h> //Anpassung kommt weiter unten
#include <TFT_eSPI.h> // Bereits im Teil 1 angepasst
#include "ui.h" // Binde hier die Start Datei ein, die EEZ Studio für dich generiert hat
// WLAN-Zugangsdaten (SSID = Service Set Identifier)
const char* ssid = "DEIN_WLAN_NAME";
const char* password = "DEIN_WLAN_PASSWORT";
// Koordinaten sind für Frechen/Köln eingestellt
String apiUrl = "http://api.open-meteo.com/v1/forecast?latitude=50.91&longitude=6.81¤t_weather=true";
// Timer-Variablen für das 15-Minuten-Intervall
unsigned long lastTime = 0;
// 15 Minuten in Millisekunden (15 * 60 * 1000)
unsigned long timerDelay = 900000;
// TFT-Display Setup
TFT_eSPI tft = TFT_eSPI();
#define screenWidth 480
#define screenHeight 320
// Speicherplatz (Buffer) für die Bildberechnung von LVGL reservieren
#define DRAW_BUF_SIZE (screenWidth * screenHeight / 10 * (LV_COLOR_DEPTH / 8))
uint8_t *draw_buf;
// --- LVGL HILFSFUNKTIONEN ---
// 1. Bilder auf den Bildschirm schieben
void my_disp_flush(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) {
uint32_t w = lv_area_get_width(area);
uint32_t h = lv_area_get_height(area);
tft.startWrite();
tft.setAddrWindow(area->x1, area->y1, w, h);
tft.pushColors((uint16_t *)px_map, w * h, true);
tft.endWrite();
lv_display_flush_ready(disp);
}
// 2. Berührungen (Touch) erkennen (Aktuell als Platzhalter)
void my_touch_read(lv_indev_t * indev, lv_indev_data_t * data) {
data->state = LV_INDEV_STATE_RELEASED;
}
// --- WETTER-FUNKTION ---
// Funktion für den Datenabruf per HTTP (Hypertext Transfer Protocol)
void fetchAndDisplayWeather() {
HTTPClient http;
http.begin(apiUrl);
int httpResponseCode = http.GET();
if (httpResponseCode > 0) {
String payload = http.getString();
// JSON (JavaScript Object Notation - ein strukturiertes Datenformat) auslesen
JsonDocument doc;
DeserializationError error = deserializeJson(doc, payload);
if (!error) {
// Temperatur aus den Daten auslesen
float temp = doc["current_weather"]["temperature"];
// Temperatur-Zahl in einen Text umwandeln (mit einer Nachkommastelle)
char tempString[15];
sprintf(tempString, "%.1f °C", temp);
// HIER FINDET DIE MAGIE STATT:
// Wir übergeben den neuen Text an das Textfeld aus deinem EEZ-Design.
// WICHTIG: Ersetze "ui_LabelTemperatur" durch den exakten Namen deines Textfeldes!
lv_label_set_text(objects.ui_LabelTemperatur, tempString);
} else {
Serial.println("Fehler beim Verarbeiten der Wetterdaten.");
}
} else {
Serial.println("Fehler beim Herunterladen der Daten.");
}
http.end(); // Verbindung sauber schließen
}
// --- HAUPTPROGRAMM ---
void setup() {
Serial.begin(115200);
// Hintergrundbeleuchtung einschalten
pinMode(27, OUTPUT);
digitalWrite(27, HIGH);
// Display starten
tft.begin();
tft.setRotation(1);
if (draw_buf == NULL) {
Serial.println("Hinweis: Kein PSRAM gefunden. Nutze normalen Speicher.");
draw_buf = (uint8_t *)malloc(DRAW_BUF_SIZE);
}
// LVGL (Grafik-Software) starten
lv_init();
lv_display_t * disp = lv_display_create(screenWidth, screenHeight);
lv_display_set_flush_cb(disp, my_disp_flush);
lv_display_set_buffers(disp, draw_buf, NULL, DRAW_BUF_SIZE, LV_DISPLAY_RENDER_MODE_PARTIAL);
lv_indev_t * indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(indev, my_touch_read);
// Die Benutzeroberfläche (GUI - Graphical User Interface) von EEZ Studio laden
ui_init();
// WLAN verbinden
Serial.print("Verbinde mit WLAN...");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(50);
// WICHTIG: Damit das Display nicht einfriert, während wir auf das WLAN warten,
// müssen wir LVGL auch hier regelmäßig aktualisieren.
lv_timer_handler();
lv_tick_inc(50);
}
Serial.println(" Verbunden!");
// Erster Abruf der Wetterdaten direkt nach dem Start
fetchAndDisplayWeather();
}
void loop() {
// LVGL Taktgeber und Aktualisierung
lv_tick_inc(5);
lv_timer_handler();
// Timer: Alle 15 Minuten das Wetter neu laden
if ((millis() - lastTime) > timerDelay) {
if (WiFi.status() == WL_CONNECTED) {
fetchAndDisplayWeather();
}
lastTime = millis();
}
// Kurze Pause zur Entlastung des Mikrocontrollers
delay(5);
}
Im Sketch nun WLAN und Koordinaten für eure Stadt schon mal anpassen. Zwischentest funktioniert:

Weitere Anpassung kommt weiter unten
Bibliothek installieren
Sofern noch nicht passiert nun die lgvl Library passend zur in EEZ Studio eingestellten Version installieren:

Nach der Installation der Bibliothek über die Arduino IDE, liegt der Ordner meist hier:
Dokumente / Arduino / libraries / lvgl
Siehe ggf. unter Arduino IDE Einstellungen Pfad für Sketchbook
Gehe nun in diesen Ordner und kopiere die lv_conf_template.h direkt in den Ordner libraries (eine ebene höher) und benenne diese auf lv_conf.h um.

Öffne diese neue lv_conf.h. Ändere ganz oben die Zeile #if 0 zu #if 1, um die Datei zu aktivieren.
Suche nach LV_COLOR_DEPTH und setze den Wert auf 16 (das ist die Standard-Farbtiefe für dein Display mit der TFT_eSPI Bibliothek).
Jetzt wie oben angekündigt, die im EEZ Tool verwendeten Schriftgrößen in dieser Datei aktivieren und danach speichern

Letzter Feinschliff:
Im EEZ Tool kann nun mit dem Haken – Symbol geprüft werden ob Fehler vorliegen.

Ist alles in Ordnung, dann mit dem Schraubenschlüssel Symbol die Erzeugung der benötigten Dateien ausführen.
Gehen wir wieder in die Arduino IDE. Ist dein Pfad im EEZ Studio korrekt eingestellt gewesen, dann siehst du neben deiner erzeugten *.INO Datei weitere Reiter:

Öffne dort die screens.h und du wirst die Felder finden, die gleich angesprochen werden mit den Temperaturwerten aus dem Internet. Bei mir ist das das Objekt „wettertemp“

Wenn du jetzt den Sketch auf dein ESP32 (CYD Display) hochlädst, dann solltest du die aktuelle Temperatur deiner Stadt sehen

Eigene Bilder und Logos in EEZ Studio einbinden:
Jetzt wird eine 100px/100px Logo hinzugefügt. Damit der kleine ESP32 auf dem CYD (der Mikrocontroller) beim Berechnen der Grafiken nicht überlastet wird, sollte vorab das Bild passend gemacht werden. Lade daher niemals riesige Fotos in das Tool, um sie später dort kleiner zu ziehen!
Die richtige Größe: Skaliere dein Bild vorher in einem Bildbearbeitungsprogramm exakt auf die Pixelgröße, die es später auf dem CYD-Display haben soll (zum Beispiel 100×100 Pixel).
Das richtige Format: Verwende am besten PNG-Dateien, besonders wenn dein Logo einen transparenten Hintergrund haben soll.
LVGL (die zugrundeliegende Grafik-Software) verarbeitet PNGs für kleine Displays deutlich besser als JPGs.
Habe zusätzlich um Speicherplatz zu sparen beim speichern unter Optionen auf 256 Farben (8Bit) reduziert.
Bilder in EEZ Studio importieren:
Klicke in EEZ Studio auf der linken, schmalen Leiste auf das Bitmaps-Symbol und dort auf das Plus-Symbol (+) und wähle deine vorbereitete PNG-Datei von der Festplatte aus.

Wechsle zurück zur normalen Ansicht.
Ziehe aus der Widget-Leiste ein Image-Widget (Bild-Baustein) auf deinen Bildschirm. Anfangs ist dort nur ein leeres Quadrat zu sehen.

Klicke das Quadrat an und schau rechts in die Eigenschaften (Properties). Klicke bei Source (Quelle) in das leere Feld und wähle dein zuvor importiertes Bild aus dem Dropdown-Menü aus und vergib unter General nach einen Namen.

Wenn ihr jetzt in EEZ Studio auf „Build“ klickt, greift das Programm auf Python zurück und übersetzt eure Logos fehlerfrei für das CYD-Display.

Wetterstation mit CYD ESP32 und EEZ Studio – Komplettes Tutorial
Ziel: Eine Wetterstation auf einem CYD-Display (ESP32-3248S035R, 3,5″, ST7796S) mit einer modernen UI, die bequem per Drag & Drop in EEZ Studio gepflegt wird. Am Ende hast du ein Gerät, das WLAN-Zeit, Wetterdaten von Open-Meteo und einen Tacho anzeigt, per Touch aktualisiert und sich nach 60 Sekunden schlafen legt.
1. Was du brauchst
Hardware
| Bauteil | Bemerkung |
|---|---|
| ESP32-3248S035R | 3,5″ Display, 480×320, ST7796S, resistiver Touch XPT2046 |
| USB-Kabel | Datenkabel, kein reines Ladekabel |
| WLAN | 2,4 GHz (ESP32 kann kein 5 GHz) |
| 3D Druck Gehäuse | Siehe Teil 2 |
| ggf. Akku | Siehe Teil 2 |
Software
- Arduino IDE 2.x (arduino.cc)
- EEZ Studio 0.29.0 oder neuer (github.com/eez-open/studio)
- Python 3 – für die von EEZ Studio benötigten Zusatzpakete
- LVGL 9.5.x – über den Bibliotheksmanager der Arduino IDE
- TFT_eSPI – über den Bibliotheksmanager
- ArduinoJson – über den Bibliotheksmanager
2. Grundinstallation
2.1 Arduino IDE und Bibliotheken
- Installiere die Arduino IDE.
- Öffne den Bibliotheksmanager (linke Seitenleiste, Bücher-Symbol).
- Suche und installiere:
lvgl(Version 9.5.x)- TFT_eSPI
- ArduinoJson
2.2 TFT_eSPI konfigurieren (aus Teil1)
Damit der Treiber dein Display kennt, muss die Konfigurationsdatei User_Setup.h angepasst werden:
- Finde den Ordner
libraries/TFT_eSPI/. - Öffne
User_Setup.hmit einem Editor. - Trage folgende Werte ein (kommentiere alte aus):
#define ST7796_DRIVER #define TFT_INVERSION_ON #define TFT_RGB_ORDER TFT_BGR #define SPI_FREQUENCY 40000000 #define TFT_MOSI 13 #define TFT_MISO 12 #define TFT_SCLK 14 #define TFT_CS 15 #define TFT_DC 2 #define TFT_RST -1 #define TFT_BL 27
- Speichern und die IDE schließen – die Änderung wird beim nächsten Start übernommen.
2.3 Python-Pakete für EEZ Studio
EEZ Studio braucht pypng und lz4, um Bilder zu konvertieren. Öffne eine Eingabeaufforderung (Windows) bzw. ein Terminal und führe aus:
pip install pypng lz4
Ohne diese Pakete bricht der Build in EEZ mit einer schwer verständlichen Fehlermeldung ab.
3. EEZ Studio – Projekt anlegen
3.1 Neues Projekt
- EEZ Studio starten.
- Create New Project → LVGL.
- In den Projekteinstellungen (Zahnrad-Symbol oben rechts):
- General → LVGL Version: 9.5
- General → Display → Width: 480
- General → Display → Height: 320
- Build → LVGL Include: lvgl.h (nicht
lvgl/lvgl.h!)
3.2 Die Oberfläche kennenlernen (siehe Video Teil3)
- Links: Widget-Baum und Widget-Palette.
- Mitte: Deine Leinwand.
- Rechts: Eigenschaften-Panel mit Abschnitten General, Layout, Style.
- Rechts oben: Reiter für Bitmaps, Fonts, Actions.
3.3 Hintergrund setzen
- Im Widget-Baum das oberste Objekt main (der Screen) anklicken.
- Style → Main → Background → Color: #E5EAE8
- Style → Main → Background → Opacity: 100%
4. Die Widgets bauen

4.1 Übersicht
| Element | Widget | Name in EEZ | Position |
|---|---|---|---|
| Logo | Image | logo_weather | oben links |
| Tacho | Scale | scale_main | Mitte rechts |
| Nadel | Line | scale_needle | Kind der Scale |
| Temperaturwert | Label | label_temp | unter Tacho |
| Uhrzeit | Label | label_time | oben rechts |
| Stadt | Label | label_stadt | unten links |
| Regenrisiko | Label | label_rain | unter Stadt |
| Wind | Label | label_wind | unter Regen |
| Luftfeuchtigkeit | Label | label_hum | unter Wind |
| Status | Label | label_status | unten links |
4.2 Logo einbinden
- Reiter Bitmaps öffnen.
+klicken → PNG auswählen. Als Farbe RGB565A8 wählen (unterstützt Transparenz).- Dem Bitmap einen Namen geben: logo_weather.
- Ein Image-Widget auf den Screen ziehen.
- Source: logo_weather, Width/Height entsprechend, Align: Top Left, Offset
20, 20.
4.3 Tacho-Skala
Ein Scale-Widget platzieren und folgende Eigenschaften setzen:
| Eigenschaft | Wert |
|---|---|
| Name | scale_main |
| Width / Height | 250 / 250 |
| Mode | ROUND_INNER |
| Range Min / Max | -20 / 50 |
| Total Tick Count | 71 |
| Major Tick Every | 10 |
| Angle Range | 270 |
| Rotation | 135 |
| Label Show | true |
| Align | Default |
Styling der Skala (Style → Main und Style → Indicator):
– Line Color: #FFFFFF
– Line Width: 2 (Main), 3 (Indicator)
– Text Color: #FFFFFF
– Text Font: Montserrat 14
4.4 Die Nadel
Das ist der wichtigste Teil:
Die Nadel muss als Kind der Skala angelegt werden, sonst funktioniert lv_scale_set_line_needle_value() in LVGL 9.5 nicht.
- Scale scale_main im Baum anklicken.
- Line-Widget als Kind hinzufügen (nicht daneben, sondern hineinziehen!).
- Eigenschaften:
| Eigenschaft | Wert |
|---|---|
| Name | scale_needle |
| Line Color | #13B594 |
| Line Width | 5 |
| Line Rounded | true |
4.5 Temperaturwert-Label
Label label_temp als Kind der Skala anlegen:
- Text: –.- C
- Font: Montserrat 28
- Text Color: #13B594
- Align: Center, Offset 0, 65
4.6 Uhrzeit-Label
Label label_time auf den Screen ziehen:
- Text: –.–.—- –:–
- Font: Montserrat 14
- Text Color: #727272
- Width: 180 (wichtig! nicht Auto)
- Align (Objekt): Top Right, X-Offset -20, Y-Offset 20
- Style → Text → Align: Right
- Style → Padding → All: 0
- Style → Text → Long Mode: CLIP
Anfängerfalle: Wenn Width auf „Auto“ steht, kann Text Align Right nicht wirken – die Box ist dann genau so breit wie der Text.
4.7 Die Textzeilen
Vier Labels untereinander, alle mit:
- Font: Montserrat 14
- Text Color: #727272
- Width: 260
- Style → Text → Align: Right
- Style → Padding → All: 0
- Style → Text → Long Mode: CLIP
- Align (Objekt): Top Right, X-Offset -20
| Name | Y-Offset | Start-Text |
|---|---|---|
| label_stadt | 160 | Stadt: Köln |
| label_rain | 190 | Regenrisiko: –% |
| label_wind | 220 | Wind: — km/h — |
| label_hum | 250 | Luftfeuchtigkeit: –% |
4.8 Status-Label
Label label_status:
- Text: WLAN…
- Font: Montserrat 14
- Text Color: #727272
- Align: Bottom Left, Offset 20, -20
5. Code aus EEZ exportieren
- Auf das Build-Symbol klicken (Schraubenschlüssel).
- EEZ generiert Dateien in den Ausgabeordner, u. a.:
- ui.c / ui.h
- screens.c / screens.h
- images.c / images.h
- actions.c / actions.h
- Diese Dateien in den Sketch-Ordner kopieren. Der Sketch-Ordner muss denselben Namen haben wie die .ino-Datei (z. B. Wetterstation/Wetterstation.ino).
6. Der Arduino-Sketch
Finaler vollständiger Sketch:
zum öffnen hier klicken
// ============================================================
// Wetterstation CYD ESP32-3248S035R
// UI erzeugt mit EEZ Studio (LVGL 9.5)
// Prilchen LABS 2026
// ============================================================
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <lvgl.h>
#include <TFT_eSPI.h>
#include <esp_sleep.h>
#include "driver/rtc_io.h"
// EEZ Studio: lvgl.h ohne Unterordner einbinden
#define LV_LVGL_H_INCLUDE_SIMPLE
#include "ui.h"
// ---------------- Zugangsdaten ----------------
const char* ssid = "xxx";
const char* password = "xxx";
String apiUrl = "http://api.open-meteo.com/v1/forecast?latitude=50.91&longitude=6.81¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,wind_direction_10m&hourly=precipitation_probability&forecast_days=1&timezone=Europe/Berlin";
// ---------------- Display ----------------
#define TFT_HOR_RES 480
#define TFT_VER_RES 320
// ---------------- Tacho-Parameter ----------------
#define TEMP_MIN (-20.0f)
#define TEMP_MAX ( 50.0f)
#define NEEDLE_LENGTH 100
// ---------------- Sleep ----------------
#define TOUCH_IRQ_PIN 36
#define SCREEN_TIMEOUT_MS 60000
TFT_eSPI tft = TFT_eSPI();
static lv_color_t draw_buf[TFT_HOR_RES * 10];
unsigned long lastTime = 0;
unsigned long timerDelay = 900000; // 15 min
unsigned long screenOnTime = 0;
// ============================================================
// Hilfsfunktion: Grad -> Himmelsrichtung
// ============================================================
static const char* wind_dir_text(int deg) {
deg = ((deg % 360) + 360) % 360;
if (deg < 22 || deg >= 338) return "N";
if (deg < 67) return "NO";
if (deg < 112) return "O";
if (deg < 157) return "SO";
if (deg < 202) return "S";
if (deg < 247) return "SW";
if (deg < 292) return "W";
return "NW";
}
// ============================================================
// LVGL Flush-Callback (TFT_eSPI)
// ============================================================
void my_disp_flush(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) {
uint32_t breite = lv_area_get_width(area);
uint32_t hoehe = lv_area_get_height(area);
tft.startWrite();
tft.setAddrWindow(area->x1, area->y1, breite, hoehe);
tft.pushColors((uint16_t *)px_map, breite * hoehe, true);
tft.endWrite();
lv_display_flush_ready(disp);
}
// ============================================================
// Tacho aktualisieren (Nadel + Zahlenwert)
// Die Widgets kommen aus der EEZ-generierten UI:
// objects.scale_main -> lv_scale
// objects.scale_needle-> lv_line (Kind der Scale)
// objects.label_temp -> lv_label
// ============================================================
static void update_gauge(float temp) {
if (temp < TEMP_MIN) temp = TEMP_MIN;
if (temp > TEMP_MAX) temp = TEMP_MAX;
lv_scale_set_line_needle_value(objects.scale_main,
objects.scale_needle,
NEEDLE_LENGTH,
(int32_t)temp);
lv_label_set_text_fmt(objects.label_temp, "%.1f C", temp);
}
// ============================================================
// Deep Sleep
// ============================================================
static void goToDeepSleep() {
Serial.println("Gehe in Deep Sleep - Touch zum Aufwachen");
// Backlight aus
digitalWrite(27, LOW);
// ST7796 in Sleep-Modus
tft.writecommand(0x10);
delay(120);
// RGB-LEDs aus (common anode)
pinMode(22, INPUT);
pinMode(16, INPUT);
pinMode(17, INPUT);
// Warten bis Finger weg
pinMode(TOUCH_IRQ_PIN, INPUT);
unsigned long t0 = millis();
while (digitalRead(TOUCH_IRQ_PIN) == LOW && millis() - t0 < 5000) {
delay(50);
}
delay(200);
// RTC-Pull-Up für IO36 aktivieren
rtc_gpio_pullup_en((gpio_num_t)TOUCH_IRQ_PIN);
rtc_gpio_pulldown_dis((gpio_num_t)TOUCH_IRQ_PIN);
// Wake bei fallender Flanke
esp_sleep_enable_ext0_wakeup((gpio_num_t)TOUCH_IRQ_PIN, 0);
esp_deep_sleep_start();
}
static void update_clock_from_meteo(const String& isoTime) {
// Erwartetes Format: "2026-09-13T14:35"
if (isoTime.length() < 16) {
lv_label_set_text(objects.label_time, "--.--.---- --:--");
return;
}
String jahr = isoTime.substring(0, 4); // "2026"
String monat = isoTime.substring(5, 7); // "09"
String tag = isoTime.substring(8, 10); // "13"
String stunde= isoTime.substring(11, 13); // "14"
String minute= isoTime.substring(14, 16); // "35"
lv_label_set_text_fmt(objects.label_time,
"%s.%s.%s %s:%s",
tag.c_str(), monat.c_str(), jahr.c_str(),
stunde.c_str(), minute.c_str());
}
// ============================================================
// Setup
// ============================================================
void setup() {
Serial.begin(115200);
esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
switch (cause) {
case ESP_SLEEP_WAKEUP_EXT0: Serial.println("Aufgewacht durch Touch"); break;
case ESP_SLEEP_WAKEUP_TIMER: Serial.println("Aufgewacht durch Timer"); break;
case ESP_SLEEP_WAKEUP_UNDEFINED: Serial.println("Kaltstart / Reset"); break;
default: Serial.println("Anderer Grund");
}
// Backlight an
pinMode(27, OUTPUT);
digitalWrite(27, HIGH);
tft.begin();
tft.setRotation(1);
// ---------- LVGL ----------
lv_init();
lv_tick_set_cb(millis);
lv_display_t *disp = lv_display_create(TFT_HOR_RES, TFT_VER_RES);
lv_display_set_buffers(disp, draw_buf, NULL, sizeof(draw_buf),
LV_DISPLAY_RENDER_MODE_PARTIAL);
lv_display_set_flush_cb(disp, my_disp_flush);
// ---------- UI aus EEZ Studio ----------
ui_init();
// Status initial setzen (Screen ist jetzt aktiv)
lv_label_set_text(objects.label_status, "WLAN...");
lv_obj_set_style_text_color(objects.label_status,
lv_color_hex(0xFFFFFF), LV_PART_MAIN);
lv_timer_handler();
// ---------- WLAN ----------
WiFi.begin(ssid, password);
Serial.print("Verbinde mit WLAN");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
lv_timer_handler();
}
Serial.println(" verbunden");
lv_label_set_text(objects.label_status, "online");
lv_obj_set_style_text_color(objects.label_status,
lv_color_hex(0x13b594), LV_PART_MAIN);
lv_timer_handler();
// ---------- Erste Wetterdaten ----------
fetchAndDisplayWeather();
// Timeout starten
screenOnTime = millis();
lastTime = millis();
}
// ============================================================
// Loop
// ============================================================
void loop() {
lv_timer_handler();
delay(5);
// Daten alle 15 min aktualisieren
if ((millis() - lastTime) > timerDelay) {
if (WiFi.status() == WL_CONNECTED) {
fetchAndDisplayWeather();
}
lastTime = millis();
}
// Nach SCREEN_TIMEOUT_MS in Deep Sleep
if (screenOnTime > 0 && (millis() - screenOnTime) > SCREEN_TIMEOUT_MS) {
goToDeepSleep();
}
}
// ------------------------------------------------------------
// Wetterdaten von Open-Meteo holen und anzeigen
// Rückgabe: true = Erfolg, false = alle Versuche gescheitert
// ------------------------------------------------------------
bool fetchAndDisplayWeather() {
for (int versuch = 1; versuch <= 3; versuch++) {
HTTPClient http;
http.setTimeout(5000);
http.begin(apiUrl);
int code = http.GET();
if (code == 200) {
String payload = http.getString();
JsonDocument doc;
DeserializationError error = deserializeJson(doc, payload);
if (!error) {
float temp = doc["current"]["temperature_2m"];
int hum = doc["current"]["relative_humidity_2m"];
float wind = doc["current"]["wind_speed_10m"];
int winddir = doc["current"]["wind_direction_10m"];
// ----- Uhrzeit / Datum -----
String nowStr = doc["current"]["time"].as<String>();
update_clock_from_meteo(nowStr);
// ----- Regenrisiko (aktuelle Stunde) -----
int rain = 0;
if (nowStr.length() >= 13) {
int hour = nowStr.substring(11, 13).toInt();
if (hour >= 0 && hour < 24) {
rain = doc["hourly"]["precipitation_probability"][hour];
}
}
// ----- Tacho -----
update_gauge(temp);
// ----- Textzeilen -----
lv_label_set_text_fmt(objects.label_hum,
"Luftfeuchtigkeit: %d%%", hum);
lv_label_set_text_fmt(objects.label_wind,
"Wind: %.0f km/h %s",
wind, wind_dir_text(winddir));
lv_label_set_text_fmt(objects.label_rain,
"Regenrisiko: %d%%", rain);
Serial.printf("OK: T=%.1f H=%d%% Wind=%.0f %s Regen=%d%% Zeit=%s\n",
temp, hum, wind, wind_dir_text(winddir),
rain, nowStr.c_str());
http.end();
return true;
} else {
Serial.print("JSON-Fehler: ");
Serial.println(error.c_str());
}
} else {
Serial.printf("HTTP-Code %d (Versuch %d/3)\n", code, versuch);
}
http.end();
delay(800); // kurz warten, dann erneut versuchen
}
// Alle Versuche gescheitert – dem Nutzer zeigen
lv_label_set_text(objects.label_status, "API-Fehler");
lv_obj_set_style_text_color(objects.label_status,
lv_color_hex(0x13B594), LV_PART_MAIN);
return false;
}
7. lv_conf.h anpassen
Öffne lv_conf.h (liegt im Sketch-Ordner oder im libraries-Ordner) und stelle sicher das ausgewählte Schriftgrößen mit 1 aktiviert wurden:
#define LV_COLOR_DEPTH 16 #define LV_USE_LABEL 1 #define LV_USE_IMAGE 1 #define LV_USE_LINE 1 #define LV_USE_SCALE 1 #define LV_USE_THEME_DEFAULT 1 #define LV_USE_FONT_PLACEHOLDER 1 #define LV_FONT_MONTSERRAT_14 1 #define LV_FONT_MONTSERRAT_28 1
LV_USE_THEME_DEFAULT ist der wichtigste Eintrag: Ohne ihn scheitert der Build mit implicit declaration of function ‚lv_theme_default_init‘.
8. Ergebnis
Nach dem Flashen zeigt dein CYD:
- Logo oben links
- Uhrzeit und Datum (Europe/Berlin, aus Open-Meteo) oben rechts
- Einen Tacho mit grüner Nadel, 270°-Sweep von -20 bis +50 °C
- Temperaturwert unter dem Tacho
- Stadt, Regenrisiko, Wind (mit Himmelsrichtung), Luftfeuchtigkeit
- Status „online“ / „WLAN…“ / „API-Fehler“
- Aktualisierung alle 15 Minuten und per Touch
- Deep Sleep nach 60 Sekunden ohne Interaktion
Das Ganze wird in EEZ Studio gepflegt – Layout-Änderungen sind zukünftig Drag & Drop, kein C-Code mehr.
10. Weiterführende Ideen
- Deutsche Wochentage: In update_clock_from_meteo() eine kleine Zuordnungstabelle einbauen (siehe nächster Abschnitt).
- Wetter-Icons statt Text: Als zusätzliche Bitmaps importieren und je nach Regenrisiko ein- und ausblenden.
- Zweifarbige Werte: Beschriftung und Wert auf zwei Labels aufteilen, dann kann der Wert
#13B594sein und der Titel bleibt #727272. - Hintergrundbeleuchtung dimmen: In der Wachphase nach 30 Sekunden Helligkeit reduzieren, statt sofort Deep Sleep.

Zum Schluss der drei Teile nochmal ein Gegenüberstellung der unterschiedlichen Möglickeiten:
| TFT_eSPI (Thin-Film-Transistor) | LVGL (Light and Versatile Graphics Library) | EEZ Studio (Envox Experimental Zone) |
|---|---|---|
|
|
|
|
Einsatzgebiet: Für einfache Anzeigen und direkte Kontrolle über jeden Bildpunkt. |
Einsatzgebiet: Für professionelle Oberflächen mit vielen verschiedenen Bildschirmen (Screens). |
Einsatzgebiet: Für schnelle erste Entwürfe (Prototypen) und leichtere Umsetzung von LVGL-Projekten. |
|
Vorteile: + Sehr schnell + Minimaler Ressourcenverbrauch (Overhead) Nachteile: – Alles muss manuell programmiert werden |
Vorteile: + Extrem flexibel und anpassbar + Schöne Animationen und Designs Nachteile: – Hoher Lernaufwand (steile Lernkurve) |
Vorteile: + Sehr verständlich (intuitiv) zu bedienen + Automatische Code-Erstellung spart Zeit Nachteile: – Weniger flexibel als reines Programmieren |
Youtube Video:
Kommt am 27.09.2026
