ESP32 + CYD Teil 3: komplette LVGL‑GUI‑Erstellung mit EEZ Studio

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&current_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

BauteilBemerkung
ESP32-3248S035R3,5″ Display, 480×320, ST7796S, resistiver Touch XPT2046
USB-KabelDatenkabel, kein reines Ladekabel
WLAN2,4 GHz (ESP32 kann kein 5 GHz)
3D Druck GehäuseSiehe Teil 2
ggf. AkkuSiehe 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

  1. Installiere die Arduino IDE.
  2. Öffne den Bibliotheksmanager (linke Seitenleiste, Bücher-Symbol).
  3. 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:

  1. Finde den Ordner libraries/TFT_eSPI/.
  2. Öffne User_Setup.h mit einem Editor.
  3. 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
  1. 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

  1. EEZ Studio starten.
  2. Create New Project → LVGL.
  3. 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

  1. Im Widget-Baum das oberste Objekt main (der Screen) anklicken.
  2. Style → Main → Background → Color: #E5EAE8
  3. Style → Main → Background → Opacity: 100%

4. Die Widgets bauen

4.1 Übersicht

ElementWidgetName in EEZPosition
LogoImagelogo_weatheroben links
TachoScalescale_mainMitte rechts
NadelLinescale_needleKind der Scale
TemperaturwertLabellabel_tempunter Tacho
UhrzeitLabellabel_timeoben rechts
StadtLabellabel_stadtunten links
RegenrisikoLabellabel_rainunter Stadt
WindLabellabel_windunter Regen
LuftfeuchtigkeitLabellabel_humunter Wind
StatusLabellabel_statusunten links

4.2 Logo einbinden

  1. Reiter Bitmaps öffnen.
  2. + klicken → PNG auswählen. Als Farbe RGB565A8 wählen (unterstützt Transparenz).
  3. Dem Bitmap einen Namen geben: logo_weather.
  4. Ein Image-Widget auf den Screen ziehen.
  5. Source: logo_weather, Width/Height entsprechend, Align: Top Left, Offset 20, 20.

4.3 Tacho-Skala

Ein Scale-Widget platzieren und folgende Eigenschaften setzen:

EigenschaftWert
Namescale_main
Width / Height250 / 250
ModeROUND_INNER
Range Min / Max-20 / 50
Total Tick Count71
Major Tick Every10
Angle Range270
Rotation135
Label Showtrue
AlignDefault

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.

  1. Scale scale_main im Baum anklicken.
  2. Line-Widget als Kind hinzufügen (nicht daneben, sondern hineinziehen!).
  3. Eigenschaften:
EigenschaftWert
Namescale_needle
Line Color#13B594
Line Width5
Line Roundedtrue

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
NameY-OffsetStart-Text
label_stadt160Stadt: Köln
label_rain190Regenrisiko: –%
label_wind220Wind: — km/h —
label_hum250Luftfeuchtigkeit: –%

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

  1. Auf das Build-Symbol klicken (Schraubenschlüssel).
  2. EEZ generiert Dateien in den Ausgabeordner, u. a.:
    • ui.c / ui.h
    • screens.c / screens.h
    • images.c / images.h
    • actions.c / actions.h
  3. 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&current=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 #13B594 sein 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)
  • Direkte Grafik-API (Application Programming Interface / Programmierschnittstelle)
  • Zeichnet einfache Formen, Text und Bilder (Bitmaps)
  • Sehr schnell durch SPI (Serial Peripheral Interface / serieller Datenbus) und DMA (Direct Memory Access / direkter Speicherzugriff)
  • Berührungserkennung (Touch) optional
  • Konfiguration zwingend über die Datei User_Setup.h
  • Vollwertiges GUI-System (Graphical User Interface / grafische Benutzeroberfläche)
  • Fertige Bedienelemente (Widgets), Designs (Themes) und Stile (Styles)
  • Unterstützt Animationen und Anordnungen (Layouts)
  • Verarbeitet Touch-Gesten und Ereignisse (Events)
  • Hohe Komplexität
  • Visueller Editor am Computer
  • Oberfläche einfach mit der Maus zusammenstellen (Drag-and-Drop)
  • Generiert automatisch den fertigen LVGL-Code
  • Ideal für Einsteiger in die Gestaltung
  • Wichtig: Programmierkenntnisse (C/C++) zwingend erforderlich, um echte Hardware (wie Sensoren) mit der Anzeige zu verbinden
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

,
Datenschutz-Übersicht

Diese Website verwendet Cookies, damit wir dir die bestmögliche Benutzerkomfort bieten können. Cookie-Informationen werden in deinem Browser gespeichert und führen Funktionen aus, wie das Wiedererkennen von dir, wenn du auf unsere Website zurückkehrst und hilft uns zu verstehen, welche Abschnitte der Website für dich am interessantesten und nützlichsten sind.