ESP32 + CYD Teil 2: mobile Wetterstation mit LVGL

Nach dem wir im Teil 1, in erste Grafiken, Buttons und Touch‑Elemente auf dem CYD‑Display umgesetzt haben, steigen wir nun in Teil 2 deutlich tiefer ein:

Gezeigt wird, wie sich auf dem ESP32‑3248S035R mit ESP32‑WROOM (4 MB Flash) mithilfe von LVGL 9.x moderne GUIs, Animationen, Icons und eigene Bild‑Assets realisieren lassen – trotz der begrenzten Ressourcen.

Dieses Kapitel setzt voraus, dass die angepasste User_Setup.h der TFT_eSPI‑Bibliothek bereits korrekt für dein CYD‑Board eingerichtet ist.

Falls du direkt hier einsteigst, solltest du sicherstellen, dass Display‑Treiber, Pins und Backlight‑Konfiguration aus Teil 1 bereits funktionieren.

Was ist LVGL und wie funktioniert es

LVGL ist eine Abkürzung und steht für Light and Versatile Graphics Library, zu deutsch: Leichte und vielseitige Grafikbibliothek. Die GUI‑Bibliothek ist sehr vielseitig – und wirklich leicht wird sie, sobald man ein wenig Zeit mit ihr verbracht hat.

Wer im ersten Teil auf dem ESP32 (Espressif Systems 32-bit Mikrocontroller) mit der Bibliothek TFT_eSPI gearbeitet hat, kennt das Prinzip:
Jedes Rechteck für einen Schalter, jeder Text und jede Farbe muss mit genauen Koordinaten auf der X- und Y-Achse (den einzelnen Bildpunkten) auf dem Bildschirm platziert werden. Auch die Berührungen (Touch) müssen mühsam berechnet werden.

Genau hier setzt LVGL an und löst viele dieser manuellen Schritte ab:
Statt Pixelkoordinaten direkt zu setzen, arbeitet man mit Widgets, Layouts, Events und Styles, ähnlich wie in modernen UI‑Frameworks. Buttons, Labels, Slider oder ganze Container werden logisch angeordnet, automatisch skaliert und sauber gerendert. Touch‑Eingaben werden über LVGLs Input‑Device‑System abstrahiert, sodass man sich nicht mehr um Rohwerte oder Koordinaten kümmern muss. Animationen, Themes und eigene Icons lassen sich ebenfalls ohne manuelles Neuzeichnen realisieren.

1. Bausteine (Widgets) statt Pixel

Anstatt jeden Bildpunkt einzeln zu steuern, nutzt man bei LVGL fertige Bausteine, sogenannte Widgets. Ein Widget kann ein Schalter (Button), ein Schieberegler (Slider) oder ein Textfeld (Label) sein.

2. Ereignisse (Events) statt ständiger Abfragen

Im bisherigen Code (wie beim Bau einer einfachen Sensor-Station) musste in der Endlosschleife des Programms (loop) ständig abgefragt werden, ob der Bildschirm berührt wurde. Dann wurden die Rohdaten des Touch-Sensors umgerechnet, um zu prüfen, ob der Finger den gemalten Button getroffen hat.
Diese Arbeit übernimmt LVGL vollständig. Touch‑Signale werden über ein registriertes Eingabegerät verarbeitet, Widgets kennen ihre eigene Position, und du reagierst nur noch auf Ereignisse.
Das macht den Code deutlich schlanker, zuverlässiger und moderner.

3. Die Arbeitsteilung mit der Hardware

LVGL ersetzt nicht die bekannten Bibliotheken wie TFT_eSPI und oder Touchscreen, die beim CYD (Cheap Yellow Display) zum Einsatz kommen. Es setzt sich vielmehr als Schicht darüber und koordiniert:

Eingabe: Die Touch-Bibliothek liest weiterhin aus, wo der Bildschirm berührt wird. In LVGL 9.5 wird dafür ein Eingabegerät angemeldet (lv_indev_t – Input Device). Diese Schnittstelle nimmt die rohen Touch-Signale entgegen und reicht sie an LVGL weiter.

Ausgabe: LVGL berechnet im Hintergrund exakt, wie das fertige Bild aussehen muss. Die eigentliche Anzeige läuft über die Bildschirmanbindung (lv_display_t). LVGL übergibt das fertig berechnete Bild an TFT_eSPI, welches dann die Farbinformationen an das physische Display sendet.

4. Der Herzschlag des Systems

Damit Animationen (wie ein weich gleitender Schieberegler) flüssig laufen und Tastendrücke sofort erkannt werden, braucht LVGL zwei Dinge im Hintergrund:

Den Taktgeber: Eine Funktion teilt LVGL kontinuierlich mit, wie viele Millisekunden vergangen sind. Das ist wichtig für das Timing von Animationen.

Den Zeichner: Im Hauptprogramm wird regelmäßig ein Befehl aufgerufen (lv_timer_handler). Dieser sagt dem System: „Prüfe jetzt, ob sich etwas verändert hat, und zeichne es neu.“

Ein enormer Vorteil: Im Gegensatz zur alten Methode, bei der man veraltete Texte oft mit einem schwarzen Kasten übermalen musste (tft.fillScreen), merkt sich LVGL ganz genau, welcher kleine Bereich sich geändert hat (zum Beispiel wenn sich ein Temperaturwert eines DHT11-Sensors – Digital Humidity and Temperature 11 – aktualisiert). Es zeichnet dann völlig automatisch nur diese wenigen Pixel neu. Das verhindert das lästige Flackern des Bildschirms komplett.

Zusammenfassend: LVGL übernimmt das Zeichnen, Aktualisieren und Organisieren der UI‑Elemente, während du dich auf die Logik deiner Anwendung konzentrierst.“

Bevor wir den Code anschauen, hier ein Überblick, wie LVGL, der Callback und TFT_eSPI zusammenspielen:

Quelle und mehr Details: Introduction | LVGL Open

Sehr hilfreich dabei sind die DOC Seiten von lvgl.io.
Hier zum Beispiel ist sehr gut beschrieben, wie ich zu dieser Tacho Lösung gekommen bin und wir diese umgesetzt werden kann:
Scale (lv_scale) – LVGL 9.5 documentation



Erstes Beispiele mit LVGL

Vorbereitung Arduino IDE:


Als Board ESP32 Dev Module und COM Port auswählen.
Dann unter Werkzeuge das Partition Scheme auf Huge App (3MB NoOTA/1MB SPIFFS) ändern

Die Bibliothek lvgl von kisvegabor, in der Vers 9.5 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).
Benötigte Schriftgrößen, zum Beispiel: #define LV_FONT_MONTSERRAT_48 mit 1 in dieser Datei aktivieren und danach speichern



Erster Sketch zum testen erstellen

Erstes Beispiel nur erstmal Text, damit Funktionen deutlicher werden.

Angezeigten Sketchinhalt löschen und diesen einfügen:

Zum öffnen anklicken
// Beispiel Sketch für ESP32‑3248S035R (NUR TEXT TEMPERATUR OHNE FUNKTION)

#include <lvgl.h>
#include <TFT_eSPI.h> // Thin-Film-Transistor über Serial Peripheral Interface

// Bildschirmauflösung für das 3,5 Zoll CYD (Cheap Yellow Display)
#define TFT_HOR_RES 480
#define TFT_VER_RES 320

// Das TFT-Objekt für die Hardware-Ansteuerung
TFT_eSPI tft = TFT_eSPI(); 

/*
  LVGL (Light and Versatile Graphics Library) zeichnet nicht den ganzen Bildschirm auf einmal, 
  sondern nutzt einen Zwischenspeicher (Puffer). Das spart wertvollen Arbeitsspeicher. 
  Wir reservieren hier Platz für ein Zehntel des Bildschirms in 16-Bit Farben.
*/
#define DRAW_BUF_SIZE (TFT_HOR_RES * TFT_VER_RES / 10 * 2)
uint8_t draw_buf[DRAW_BUF_SIZE];


/* 
  Der "Lieferant": Diese Funktion reicht die fertig berechneten Bild-Häppchen 
  von LVGL an die TFT_eSPI Bibliothek und damit an das physische Display weiter.
*/
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);

    // Sende die Daten an das Display
    tft.startWrite();
    tft.setAddrWindow(area->x1, area->y1, breite, hoehe);
    tft.pushColors((uint16_t *)px_map, breite * hoehe, true);
    tft.endWrite();

    // WICHTIG: LVGL mitteilen, dass wir mit diesem Häppchen fertig sind!
    lv_display_flush_ready(disp);
}


void setup() {
    Serial.begin(115200);

    // Hintergrundbeleuchtung für das ESP32-3248S035R einschalten
    pinMode(27, OUTPUT);
    digitalWrite(27, HIGH);

    // Display-Hardware starten
    tft.begin();
    tft.setRotation(1); // Querformat (Landscape)

    // LVGL System starten
    lv_init();

    // WICHTIG für LVGL 9.5: Dem System sagen, woher es die Zeit für Animationen bekommt.
    // Wir nutzen dafür den internen Arduino-Taktgeber (millis).
    lv_tick_set_cb(millis);

    // 1. Ein neues LVGL-Display anlegen mit unserer Auflösung
    lv_display_t * disp = lv_display_create(TFT_HOR_RES, TFT_VER_RES);
    
    // 2. LVGL sagen, wo der Zwischenspeicher (Puffer) liegt
    lv_display_set_buffers(disp, draw_buf, NULL, DRAW_BUF_SIZE, LV_DISPLAY_RENDER_MODE_PARTIAL);
    
    // 3. LVGL sagen, welche Funktion zum Zeichnen genutzt werden soll
    lv_display_set_flush_cb(disp, my_disp_flush);


    // --- HIER BEGINNT DEIN BEISPIEL FÜR DIE OBERFLÄCHE (GUI - Graphical User Interface) ---
    
    // Den Hintergrund des aktuellen Bildschirms dunkelblau färben
    lv_obj_set_style_bg_color(lv_screen_active(), lv_color_hex(0x2B4B8A), LV_PART_MAIN);
    lv_obj_set_style_bg_opa(lv_screen_active(), LV_OPA_COVER, LV_PART_MAIN);

    // 1. Den Titel-Text (Label) erstellen
    lv_obj_t * label_titel = lv_label_create(lv_screen_active());
    lv_label_set_text(label_titel, "Aktuelle Temperatur in Koeln:");
    lv_obj_set_style_text_color(label_titel, lv_color_hex(0xFFFFFF), LV_PART_MAIN); // Schriftfarbe Weiß
    
    // Ausrichtung: Mittig am oberen Rand, dann 80 Pixel nach unten verschoben
    lv_obj_align(label_titel, LV_ALIGN_TOP_MID, 0, 80);

    // 2. Den Temperatur-Text (Label) erstellen
    lv_obj_t * label_temp = lv_label_create(lv_screen_active());
    lv_label_set_text(label_temp, "18.8 c"); 
    lv_obj_set_style_text_color(label_temp, lv_color_hex(0xADFF2F), LV_PART_MAIN); // Schriftfarbe Hellgrün
    
    // Hier weisen wir dem Text die große Schriftart "Montserrat 48" zu 
    // (Voraussetzung: in der lv_conf.h aktiviert!)
    lv_obj_set_style_text_font(label_temp, &lv_font_montserrat_48, LV_PART_MAIN);
    
    // Ausrichtung: Rechtsbündig, 40 Pixel vom Rand weg und 150 Pixel nach unten
    lv_obj_align(label_temp, LV_ALIGN_TOP_RIGHT, -170, 150);
}


void loop() {
    // LVGL arbeiten lassen (Zeichnen, Änderungen prüfen, Animationen abspielen)
    lv_timer_handler();
    
    // Eine kurze Pause entlastet den Mikrocontroller (ESP32 - Espressif Systems 32-bit)
    delay(5);
}


Ergebnis:

Wenn man sich nun mit der Funktionalität im „Nur Text Sketch“ auseinander gesetzt hat,
können nun die tatsächlichen Wetter Werte mit diesem zweiten Sketch aus dem Internet gezogen werden:

Zum öffnen hier anklicken
#include <WiFi.h>
#include <HTTPClient.h> // Hypertext Transfer Protocol
#include <ArduinoJson.h> // JavaScript Object Notation
#include <lvgl.h>
#include <TFT_eSPI.h> // Thin-Film-Transistor über Serial Peripheral Interface

// WLAN-Zugangsdaten (SSID = Service Set Identifier)
const char* ssid = "DEIN_WLAN_NAME";
const char* password = "DEIN_WLAN_PASSWORT";

// Open-Meteo API URL (Koordinaten: 50.91, 6.81)
String apiUrl = "http://api.open-meteo.com/v1/forecast?latitude=50.91&longitude=6.81&current_weather=true";

// TFT-Display Setup
#define TFT_HOR_RES 480
#define TFT_VER_RES 320
TFT_eSPI tft = TFT_eSPI();

// LVGL Puffer
#define DRAW_BUF_SIZE (TFT_HOR_RES * TFT_VER_RES / 30 * 2)
uint8_t draw_buf[DRAW_BUF_SIZE];

// GLOBALE LVGL Widgets: 
// Diese müssen hier oben stehen, damit alle Funktionen darauf zugreifen können
lv_obj_t * label_status;
lv_obj_t * label_temp;

// Timer-Variablen: Wir wollen die API nicht blockieren oder überlasten
unsigned long lastTime = 0;
// Intervall auf 15 Minuten setzen (15 Minuten * 60 Sekunden * 1000 Millisekunden)
unsigned long timerDelay = 900000;

/* 
  Der "Lieferant": Diese Funktion reicht die fertig berechneten Bild-Häppchen 
  von LVGL an das physische Display weiter.
*/
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);
}


void setup() {
    Serial.begin(115200);

    // Hintergrundbeleuchtung des Displays einschalten (Pin 27)
    pinMode(27, OUTPUT);
    digitalWrite(27, HIGH);

    // Display initialisieren
    tft.begin();
    tft.setRotation(1); // Querformat

    // LVGL System starten
    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, DRAW_BUF_SIZE, LV_DISPLAY_RENDER_MODE_PARTIAL);
    lv_display_set_flush_cb(disp, my_disp_flush);

    // --- GUI (Graphical User Interface) Aufbau ---
    
    // Hintergrundfarbe setzen (Schwarz wie in deinem alten Sketch)
    lv_obj_set_style_bg_color(lv_screen_active(), lv_color_hex(0x000000), LV_PART_MAIN);
    lv_obj_set_style_bg_opa(lv_screen_active(), LV_OPA_COVER, LV_PART_MAIN);

    // 1. Label für Statusmeldungen (WLAN etc.)
    label_status = lv_label_create(lv_screen_active());
    lv_obj_set_style_text_color(label_status, lv_color_hex(0xFFFFFF), LV_PART_MAIN);
    lv_obj_align(label_status, LV_ALIGN_CENTER, 0, 0);
    lv_label_set_text(label_status, "Verbinde mit WLAN...");

    // 2. Den Titel-Text für die Temperatur erstellen
    lv_obj_t * label_titel = lv_label_create(lv_screen_active());
    lv_label_set_text(label_titel, "Aktuelle Temperatur in Koeln:");
    lv_obj_set_style_text_color(label_titel, lv_color_hex(0xD3D3D3), LV_PART_MAIN); // Hellgrau
    lv_obj_align(label_titel, LV_ALIGN_TOP_MID, 0, 80);

    // 3. Den Temperatur-Text erstellen (Startet leer)
    label_temp = lv_label_create(lv_screen_active());
    lv_label_set_text(label_temp, ""); // Noch kein Wert vorhanden
    lv_obj_set_style_text_color(label_temp, lv_color_hex(0xFFFF00), LV_PART_MAIN); // Gelb
    lv_obj_set_style_text_font(label_temp, &lv_font_montserrat_48, LV_PART_MAIN); // Große Schrift
    lv_obj_align(label_temp, LV_ALIGN_TOP_RIGHT, -170, 150); // Dein angepasster Wert


    // --- WLAN Verbindung ---
    WiFi.begin(ssid, password);
    Serial.print("Verbinde mit WLAN");
    
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
        // LVGL muss auch während der Wartezeit weiterlaufen, damit der Text gerendert wird
        lv_timer_handler(); 
    }

    // Erfolgsmeldung anzeigen
    lv_obj_set_style_text_color(label_status, lv_color_hex(0x00FF00), LV_PART_MAIN); // Grün
    lv_label_set_text(label_status, "WLAN verbunden!");
    
    // Bildschirm einmal aktualisieren und kurz warten, damit man die Meldung lesen kann
    lv_timer_handler();
    delay(2000);
    
    // Status-Text danach einfach löschen (verstecken)
    lv_label_set_text(label_status, "");

    // Ersten API-Aufruf direkt beim Start ausführen
    fetchAndDisplayWeather();
}

void loop() {
    // LVGL arbeiten lassen
    lv_timer_handler();
    delay(5);

    // API nur alle 15 Minuten abfragen
    if ((millis() - lastTime) > timerDelay) {
        if (WiFi.status() == WL_CONNECTED) {
            fetchAndDisplayWeather();
        }
        lastTime = millis();
    }
}

// Funktion für den HTTP-Request und die Anzeige
void fetchAndDisplayWeather() {
    HTTPClient http;

    http.begin(apiUrl);
    int httpResponseCode = http.GET(); // GET-Anfrage an den Server senden

    if (httpResponseCode > 0) {
        String payload = http.getString();

        // Neues JSON-Dokument vorbereiten (ArduinoJson Version 7)
        JsonDocument doc;
        DeserializationError error = deserializeJson(doc, payload);

        if (!error) {
            // Temperatur aus dem JSON extrahieren
            float temp = doc["current_weather"]["temperature"];

            // --- NEU: LVGL Temperatur aktualisieren ---
            // Wir nutzen lv_label_set_text_fmt, um Text und Variable direkt zu mischen.
            // %.1f bedeutet: Eine Kommazahl mit einer Nachkommastelle.
            // Zahl mit einer Nachkommastelle in Text umwandeln und " C" anhängen
            String tempString = String(temp, 1) + " C"; 
            // Den fertigen Arduino-String als klassischen C-String (.c_str) an LVGL übergeben
            lv_label_set_text(label_temp, tempString.c_str());
            
        } else {
            Serial.print("JSON parsen fehlgeschlagen: ");
            Serial.println(error.c_str());
        }
    } else {
        Serial.print("Fehler beim HTTP-Request. Code: ");
        Serial.println(httpResponseCode);
    }

    http.end(); // Verbindung sauber schließen, um Speicher freizugeben
}

Ergebnis: Eigentlich wie das Bild oben drüber, jedoch nun echte aktuelle Außentemperatur, die alle 15 Minuten aktualisiert wird.



In drei Schritten nun ein Logo auf dem Display ergänzen

Bilder können nicht in den bekannten Dateiformaten verwendet werden und müssen vorab in → C-Array konvertiert werden

  • Bild umwandeln:
    Ich hatte mein Logo (Bild oder Grafik) schon in der gewünschten Pixel Größe mit Photopea der kostenlose Online Photo Editor vorbereitet. Gff. auch freistellen (siehe Bild) und am besten als PNG mit Alpha (Portable Network Graphics) abspeichern.
    LVGL bietet hier für einen Image Converter — LVGL an.
    Wähle dort als Version „V9“, als Farbtiefe „RGB565A8“ (das entspricht den 16-Bit deines Displays) und als Ausgabeformat „C array“. Lade dein Bild dann hoch und klicke auf Konvertieren.
  • Datei ablegen: Du erhältst eine Textdatei (zum Beispiel logo.c). Speichere diese Datei exakt in demselben Ordner auf deiner Festplatte ab, in dem auch dein aktueller Arduino-Sketch (.ino Datei) liegt.
  • Lege zusätzlich eine logo.h Datei neben den beiden schon vorhandenen Dateien und trage das hier ein: (ggf. Dateiname anpassen)
// logo.h
#pragma once
#include "lvgl.h"

#ifdef __cplusplus
extern "C" {
#endif

extern const lv_image_dsc_t logo;

#ifdef __cplusplus
}
#endif

Im Sketch einbauen: Nun musst du dem Code nur noch sagen, mit
#include „logo.h“  
dass es dieses Bild gibt und wo es angezeigt werden soll. Hier ein Beispiel Sketch:

Zum öffnen anklicken
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <lvgl.h>
#include <TFT_eSPI.h>
#include "logo.h"                 

const char* ssid = "DEIN_WLAN_NAME";
const char* password = "DEIN_WLAN_PASSWORT";

String apiUrl = "http://api.open-meteo.com/v1/forecast?latitude=50.91&longitude=6.81&current_weather=true";

#define TFT_HOR_RES 480
#define TFT_VER_RES 320

TFT_eSPI tft = TFT_eSPI();

// Draw-Buffer: 10 KB DRAM. Reicht für WLAN-Betrieb mit Luft nach oben.
static lv_color_t draw_buf[TFT_HOR_RES * 10];

lv_obj_t *label_status;
lv_obj_t *label_temp;

unsigned long lastTime    = 0;
unsigned long timerDelay  = 900000;

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);
}

void setup() {
  Serial.begin(115200);

  pinMode(27, OUTPUT);
  digitalWrite(27, HIGH);

  tft.begin();
  tft.setRotation(1);

  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);

  lv_obj_t *scr = lv_screen_active();
  lv_obj_set_style_bg_color(scr, lv_color_hex(0x000000), LV_PART_MAIN);
  lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, LV_PART_MAIN);

  // --- Logo oben links ---
  lv_obj_t *logo_img = lv_image_create(scr);
  lv_image_set_src(logo_img, &logo);
  lv_obj_align(logo_img, LV_ALIGN_TOP_LEFT, 10, 10);

  // --- Status ---
  label_status = lv_label_create(scr);
  lv_obj_set_style_text_color(label_status, lv_color_hex(0xFFFFFF), LV_PART_MAIN);
  lv_obj_align(label_status, LV_ALIGN_CENTER, 0, 0);
  lv_label_set_text(label_status, "Verbinde mit WLAN...");

  // --- Titel ---
  lv_obj_t *label_titel = lv_label_create(scr);
  lv_label_set_text(label_titel, "Aktuelle Temperatur in Koeln:");
  lv_obj_set_style_text_color(label_titel, lv_color_hex(0xD3D3D3), LV_PART_MAIN);
  lv_obj_align(label_titel, LV_ALIGN_TOP_MID, 0, 80);

  // --- Temperatur ---
  label_temp = lv_label_create(scr);
  lv_label_set_text(label_temp, "");
  lv_obj_set_style_text_color(label_temp, lv_color_hex(0xFFFF00), LV_PART_MAIN);
  lv_obj_set_style_text_font(label_temp, &lv_font_montserrat_48, LV_PART_MAIN);
  lv_obj_align(label_temp, LV_ALIGN_TOP_RIGHT, -170, 150);

  // --- WLAN ---
  WiFi.begin(ssid, password);
  Serial.print("Verbinde mit WLAN");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    lv_timer_handler();          // damit der Text sichtbar bleibt
  }
  lv_obj_set_style_text_color(label_status, lv_color_hex(0x00FF00), LV_PART_MAIN);
  lv_label_set_text(label_status, "WLAN verbunden!");
  lv_timer_handler();
  delay(2000);
  lv_label_set_text(label_status, "");

  fetchAndDisplayWeather();
}

void loop() {
  lv_timer_handler();
  delay(5);

  if ((millis() - lastTime) > timerDelay) {
    if (WiFi.status() == WL_CONNECTED) {
      fetchAndDisplayWeather();
    }
    lastTime = millis();
  }
}

void fetchAndDisplayWeather() {
  HTTPClient http;
  http.begin(apiUrl);
  int httpResponseCode = http.GET();

  if (httpResponseCode > 0) {
    String payload = http.getString();
    JsonDocument doc;
    DeserializationError error = deserializeJson(doc, payload);

    if (!error) {
      float temp = doc["current_weather"]["temperature"];
      String tempString = String(temp, 1) + " C";
      lv_label_set_text(label_temp, tempString.c_str());
    } else {
      Serial.print("JSON parsen fehlgeschlagen: ");
      Serial.println(error.c_str());
    }
  } else {
    Serial.print("Fehler beim HTTP-Request. Code: ");
    Serial.println(httpResponseCode);
  }
  http.end();
}

Ergebnis:


Finale Version:

Damit wir einmal sehen können, was uns LVGL 9.x grafisch auf dem 3,5″-Display so alles bietet, möchte ich die bisherigen Wetterdaten nun deutlich erweitern. Bisher haben wir lediglich die Temperatur in Celsius angezeigt – aber eine moderne kleine Wetterstation kann natürlich viel mehr.

Ich möchte zusätzlich:

  • das Regenrisiko sehen,
  • die Windstärke,
  • die Windrichtung,
  • die Luftfeuchtigkeit,
  • und die Temperatur soll nicht einfach nur als Zahl erscheinen, sondern als grafischer Tacho mit Nadel und Skala.

Beim Stöbern in der LVGL‑Dokumentation bin ich auf ein Widget gestoßen, das genau dafür gedacht ist: eine gebogene Skala (lv_scale), die sich wie ein klassischer Tachometer nutzen lässt. Mit einer Nadel können wir dort wunderbar den aktuellen Temperaturwert anzeigen – optisch ansprechend und intuitiv.

Genau das möchte ich in diesem Abschnitt umsetzen.

Zusätzlich richte ich einen seriellen Monitor ein. Falls auf dem Display einmal nichts erscheinen sollte, können wir dort jederzeit sehen, was der ESP gerade macht, welche Daten ankommen und ob die API‑Abfrage korrekt funktioniert. Das erleichtert die Fehlersuche enorm.

Ausgabe der Konsole:

Zum Schluss habe ich das gesamte System noch optimiert: Die Wetterstation läuft jetzt im stromsparenden Deep‑Sleep‑Modus und kann per Touch‑Wakeup wieder aufgeweckt werden. Das spart enorm Energie und macht das Gerät alltagstauglich – ideal für mobile oder akkubetriebene Einsätze.
Strombedarf während Anzeige: 0,161A und im DeepSleep Mode: 0,003A, wenn am USB Port angeschlossen.
Im Akku Betrieb habe ich nicht gemessen.

Und das auf einem CYD-Board, wo Treiber, Pins und Speicher gerne mal zicken. Das läuft jetzt rund, yeah 🙂

Hier der Finale Sketch:

Damit jeder nachvollziehen kann, wie der Sketch aufgebaut ist, werde ich die einzelnen Bereiche im Code ausführlich kommentieren. So ist klar ersichtlich, welche Zeile welchen Zweck erfüllt und wie die Daten verarbeitet werden.

Zum Aufklappen anklicken
/* ============================================================================
 *  WETTERSTATION MIT TACHO-ANZEIGE
 *  ---------------------------------------------------------------------------
 *  Hardware : CYD ESP32-3248S035R (3.5", ST7796S, XPT2046 Touch)
 *  Display  : TFT_eSPI, 480x320, 4-Wire SPI
 *  Grafik   : LVGL 9.5 mit eigenem Flush-Callback
 *  Daten    : Open-Meteo API (Frechen, NRW)
 *
 *  Funktion :
 *    - WLAN verbinden, Wetterdaten alle 15 min aktualisieren
 *    - Temperatur als Tacho (-20..+50 C), Werte als Textzeilen
 *    - Nach 60 s Anzeige -> Deep Sleep (3 mA)
 *    - Aufwachen durch Beruehren des Touchscreens
 *
 *  Speicher : Partition "Huge APP", draw_buf absichtlich klein (480*10)
 *             Logo als C-Array im Flash (RGB565A8 wegen Alpha)
 * ==========================================================================*/

#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"
#include "logo.h"          // vom LVGL-Image-Converter erzeugtes C-Array

/* ---------------------------------------------------------------------------
 *  KONFIGURATION
 * -------------------------------------------------------------------------*/

// WLAN-Zugangsdaten (bitte anpassen!)
const char* ssid     = "DEIN_WLAN_NAME";
const char* password = "DEIN_WLAN_PASSWORT";

// Open-Meteo API - aktuelle Werte 
// latitude/longitude = Frechen (50.91, 6.81) *** anpassen auf eure Stadt
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";

// Display-Aufloesung (im Querformat)
#define TFT_HOR_RES 480
#define TFT_VER_RES 320

// Tacho-Parameter: Wertebereich und Geometrie
#define TEMP_MIN          (-20.0f)   // linker Anschlag der Skala
#define TEMP_MAX          ( 50.0f)   // rechter Anschlag
#define SCALE_START_ANGLE 135        // Startwinkel (7:30-Position)
#define SCALE_ANGLE_RANGE 270        // Sweep bis 4:30
#define NEEDLE_LENGTH     100        // Nadellaenge in Pixel

// Deep-Sleep-Konfiguration
#define TOUCH_IRQ_PIN     36         // XPT2046 IRQ: LOW = beruehrt
#define SCREEN_TIMEOUT_MS 60000      // Anzeigedauer, dann Sleep

/* ---------------------------------------------------------------------------
 *  GLOBALE OBJEKTE
 * -------------------------------------------------------------------------*/

TFT_eSPI tft = TFT_eSPI();

// Draw-Buffer absichtlich klein (9,4 KB DRAM), damit WLAN-Stack
// und HTTPClient noch genug RAM haben. Partial-Render-Mode reicht,
// da sich die Anzeige nur alle 15 min aendert.
static lv_color_t draw_buf[TFT_HOR_RES * 10];

// Widget-Zeiger - global, damit fetchAndDisplayWeather() sie erreicht
lv_obj_t *label_status;
lv_obj_t *label_temp;
lv_obj_t *label_hum;
lv_obj_t *label_wind;
lv_obj_t *label_rain;
lv_obj_t *gauge_scale;      // Referenz auf die Skala (fuer Nadel-Update)
lv_obj_t *needle_line;      // Referenz auf die Nadel-Linie

// Timer
unsigned long lastTime     = 0;      // letzter API-Abruf
unsigned long timerDelay   = 900000; // 15 min in ms
unsigned long screenOnTime = 0;      // Startzeitpunkt der aktuellen Anzeige

/* ---------------------------------------------------------------------------
 *  HILFSFUNKTIONEN
 * -------------------------------------------------------------------------*/

/**
 * Wandelt eine Windrichtung in Grad in eine Himmelsrichtung um.
 * 0/360=N, 90=O, 180=S, 270=W. Zwischenwerte als Kombination (NO, SO, ...).
 */
static const char* wind_dir_text(int deg) {
  deg = ((deg % 360) + 360) % 360;   // sicherstellen: 0..359
  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 <-> TFT_eSPI BRUECKE
 * -------------------------------------------------------------------------*/

/**
 * Flush-Callback: LVGL uebergibt hier fertig gerenderte Bild-Chunks.
 * Wir leiten sie an TFT_eSPI weiter und melden "fertig" zurueck.
 *
 * Ablauf:
 *   1. Empfangenen Bereich (area) an TFT_eSPI melden
 *   2. Pixel per SPI ans Display schicken
 *   3. LVGL mitteilen, dass der naechste Chunk kommen darf
 */
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();

  // WICHTIG: Ohne diesen Aufruf blockiert LVGL beim naechsten Chunk.
  lv_display_flush_ready(disp);
}

/* ---------------------------------------------------------------------------
 *  TACHO-AUFBAU
 * -------------------------------------------------------------------------*/

/**
 * Baut die Skala samt Nadel und Zahlenwert in der Mitte auf.
 *
 * LVGL 9 bietet lv_scale_set_line_needle_value() - diese Funktion
 * positioniert Nadel UND Drehpunkt korrekt relativ zur Skala.
 * Eine manuelle Rotation per transform_rotation ist damit unnoetig.
 */
static void build_gauge(lv_obj_t *parent) {
  lv_obj_t *scale = lv_scale_create(parent);
  lv_obj_set_size(scale, 250, 250);
  lv_obj_align(scale, LV_ALIGN_CENTER, 80, 0);   // rechte Bildhaelfte

  lv_scale_set_mode(scale, LV_SCALE_MODE_ROUND_INNER);
  lv_scale_set_range(scale, (int32_t)TEMP_MIN, (int32_t)TEMP_MAX);

  // 71 Ticks fuer -20..+50 (1 Tick = 1 C), Major-Label alle 10 Grad
  lv_scale_set_total_tick_count(scale, 71);
  lv_scale_set_major_tick_every(scale, 10);

  // 270-Grad-Sweep, Start bei 135 Grad (7:30)
  lv_scale_set_angle_range(scale, SCALE_ANGLE_RANGE);
  lv_scale_set_rotation(scale, SCALE_START_ANGLE);
  lv_scale_set_label_show(scale, true);

  // Styling: weisse Skala auf schwarzem Hintergrund
  lv_obj_set_style_line_color(scale, lv_color_white(), LV_PART_MAIN);
  lv_obj_set_style_line_width(scale, 2, LV_PART_MAIN);
  lv_obj_set_style_line_color(scale, lv_color_white(), LV_PART_INDICATOR);
  lv_obj_set_style_line_width(scale, 3, LV_PART_INDICATOR);
  lv_obj_set_style_text_color(scale, lv_color_white(), LV_PART_INDICATOR);
  lv_obj_set_style_text_font(scale, &lv_font_montserrat_14, LV_PART_INDICATOR);

  // Nadel als lv_line - wird von LVGL selbst gedreht
  needle_line = lv_line_create(scale);
  lv_obj_set_style_line_width(needle_line, 5, 0);
  lv_obj_set_style_line_color(needle_line, lv_color_hex(0xFF3030), 0);
  lv_obj_set_style_line_rounded(needle_line, true, 0);

  gauge_scale = scale;
  lv_scale_set_line_needle_value(scale, needle_line, NEEDLE_LENGTH,
                                 (int32_t)TEMP_MIN);

  // Zahlenwert unterhalb der Nadelmitte
  label_temp = lv_label_create(scale);
  lv_obj_set_style_text_color(label_temp, lv_color_hex(0xFFFF00), LV_PART_MAIN);
  lv_obj_set_style_text_font(label_temp, &lv_font_montserrat_28, LV_PART_MAIN);
  lv_obj_align(label_temp, LV_ALIGN_CENTER, 0, 65);
  lv_label_set_text(label_temp, "--.- C");
}

/**
 * Aktualisiert Nadelposition und Zahlenwert im Tacho.
 * Wertebereich wird auf TEMP_MIN/TEMP_MAX begrenzt.
 */
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(gauge_scale, needle_line, NEEDLE_LENGTH,
                                 (int32_t)temp);
  lv_label_set_text_fmt(label_temp, "%.1f C", temp);
}

/* ---------------------------------------------------------------------------
 *  DEEP SLEEP
 * -------------------------------------------------------------------------*/

/**
 * Versetzt den ESP32 in Deep Sleep. Aufwachen durch Touch auf IRQ-Pin.
 *
 * Vor dem Schlafengehen werden alle stromziehenden Peripheriegeraete
 * abgeschaltet, um von 17 mA auf ~3 mA zu kommen.
 */
static void goToDeepSleep() {
  Serial.println("Gehe in Deep Sleep - Touch zum Aufwachen");

  // Backlight aus (Pin 27, LOW = aus) - groesster Einzelverbraucher
  digitalWrite(27, LOW);

  // ST7796 in Sleep-Modus (spart ~2-5 mA)
  tft.writecommand(0x10);
  delay(120);    // laut Datenblatt mind. 120 ms warten

  // RGB-LEDs ausschalten (common anode, LOW = an -> auf INPUT = hochohmig)
  pinMode(22, INPUT);   // Rot
  pinMode(16, INPUT);   // Gruen
  pinMode(17, INPUT);   // Blau

  // Warten, bis der Finger weg ist - sonst wacht der ESP sofort wieder auf
  pinMode(TOUCH_IRQ_PIN, INPUT);
  unsigned long t0 = millis();
  while (digitalRead(TOUCH_IRQ_PIN) == LOW && millis() - t0 < 5000) {
    delay(50);
  }
  delay(200);

  // GPIO 36 ist input-only und hat keinen internen Pull-Up.
  // RTC-Pull-Up aktivieren, damit die IRQ-Leitung im Sleep HIGH bleibt.
  rtc_gpio_pullup_en((gpio_num_t)TOUCH_IRQ_PIN);
  rtc_gpio_pulldown_dis((gpio_num_t)TOUCH_IRQ_PIN);

  // Aufwachen bei fallender Flanke (0 = LOW = beruehrt)
  esp_sleep_enable_ext0_wakeup((gpio_num_t)TOUCH_IRQ_PIN, 0);

  esp_deep_sleep_start();
}

/* ---------------------------------------------------------------------------
 *  SETUP
 * -------------------------------------------------------------------------*/

void setup() {
  Serial.begin(115200);

  // Aufwachgrund ausgeben (hilfreich beim Debuggen)
  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);

  // Display initialisieren
  tft.begin();
  tft.setRotation(1);            // Querformat

  // LVGL initialisieren
  lv_init();
  lv_tick_set_cb(millis);        // millis() als Tick-Quelle

  // Display-Treiber registrieren
  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);

  // Hintergrund schwarz
  lv_obj_t *scr = lv_screen_active();
  lv_obj_set_style_bg_color(scr, lv_color_hex(0x000000), LV_PART_MAIN);
  lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, LV_PART_MAIN);

  // ---- UI AUFBAUEN ----

  // Logo oben links (transparentes PNG -> RGB565A8)
  lv_obj_t *logo_img = lv_image_create(scr);
  lv_image_set_src(logo_img, &logo);
  lv_obj_align(logo_img, LV_ALIGN_TOP_LEFT, 50, 20);

  // Stadtname (statisch)
  lv_obj_t *label_stadt = lv_label_create(scr);
  lv_label_set_text(label_stadt, "Stadt: Frechen");
  lv_obj_set_style_text_color(label_stadt, lv_color_hex(0xD3D3D3), LV_PART_MAIN);
  lv_obj_align(label_stadt, LV_ALIGN_TOP_LEFT, 20, 160);

  // Regenrisiko (dynamisch)
  label_rain = lv_label_create(scr);
  lv_label_set_text(label_rain, "Regenrisiko: --%");
  lv_obj_set_style_text_color(label_rain, lv_color_hex(0xD3D3D3), LV_PART_MAIN);
  lv_obj_align(label_rain, LV_ALIGN_TOP_LEFT, 20, 190);

  // Wind (dynamisch)
  label_wind = lv_label_create(scr);
  lv_label_set_text(label_wind, "Wind: -- km/h --");
  lv_obj_set_style_text_color(label_wind, lv_color_hex(0xD3D3D3), LV_PART_MAIN);
  lv_obj_align(label_wind, LV_ALIGN_TOP_LEFT, 20, 220);

  // Luftfeuchtigkeit (dynamisch)
  label_hum = lv_label_create(scr);
  lv_label_set_text(label_hum, "Luftfeuchtigkeit: --%");
  lv_obj_set_style_text_color(label_hum, lv_color_hex(0xD3D3D3), LV_PART_MAIN);
  lv_obj_align(label_hum, LV_ALIGN_TOP_LEFT, 20, 250);

  // Status unten links
  label_status = lv_label_create(scr);
  lv_obj_set_style_text_color(label_status, lv_color_hex(0xFFFFFF), LV_PART_MAIN);
  lv_obj_align(label_status, LV_ALIGN_BOTTOM_LEFT, 20, -20);
  lv_label_set_text(label_status, "WLAN...");

  // Tacho aufbauen
  build_gauge(scr);

  // Sofort rendern, damit "WLAN..." sichtbar wird, bevor wir blockieren
  lv_timer_handler();

  // ---- WLAN VERBINDEN ----
  WiFi.begin(ssid, password);
  Serial.print("Verbinde mit WLAN");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    lv_timer_handler();    // LVGL am Leben halten, sonst friert die Anzeige ein
  }
  lv_obj_set_style_text_color(label_status, lv_color_hex(0x00FF00), LV_PART_MAIN);
  lv_label_set_text(label_status, "online");
  lv_timer_handler();

  // Ersten Werte holen
  fetchAndDisplayWeather();

  // Timer fuer Anzeigedauer starten
  screenOnTime = millis();
}

/* ---------------------------------------------------------------------------
 *  LOOP
 * -------------------------------------------------------------------------*/

void loop() {
  // LVGL arbeiten lassen
  lv_timer_handler();
  delay(5);

  // Alle 15 min neue Wetterdaten (nur solange wir wach sind)
  if ((millis() - lastTime) > timerDelay) {
    if (WiFi.status() == WL_CONNECTED) {
      fetchAndDisplayWeather();
    }
    lastTime = millis();
  }

  // Nach Ablauf der Anzeigedauer schlafen gehen
  if (screenOnTime > 0 && (millis() - screenOnTime) > SCREEN_TIMEOUT_MS) {
    goToDeepSleep();
  }
}

/* ---------------------------------------------------------------------------
 *  WETTERDATEN HOLEN UND ANZEIGEN
 * -------------------------------------------------------------------------*/

/**
 * HTTP-Request an Open-Meteo, JSON parsen, UI aktualisieren.
 *
 * Regenrisiko: precipitation_probability wird stuendlich geliefert,
 * der Array-Index entspricht der aktuellen Stunde (0 = 00:00 Uhr).
 */
void fetchAndDisplayWeather() {
  HTTPClient http;
  http.begin(apiUrl);
  int httpResponseCode = http.GET();

  if (httpResponseCode > 0) {
    String payload = http.getString();
    JsonDocument doc;
    DeserializationError error = deserializeJson(doc, payload);

    if (!error) {
      // ---- Aktuelle Werte ----
      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"];

      // ---- Regenrisiko der aktuellen Stunde ----
      int rain = 0;
      String nowStr = doc["current"]["time"].as<String>();   // "2026-09-13T14:30"
      if (nowStr.length() >= 13) {
        int hour = nowStr.substring(11, 13).toInt();          // "14" -> 14
        if (hour >= 0 && hour < 24) {
          rain = doc["hourly"]["precipitation_probability"][hour];
        }
      }

      // ---- UI aktualisieren ----
      update_gauge(temp);
      lv_label_set_text_fmt(label_hum,  "Luftfeuchtigkeit: %d%%", hum);
      lv_label_set_text_fmt(label_wind, "Wind: %.0f km/h %s", wind,
                            wind_dir_text(winddir));
      lv_label_set_text_fmt(label_rain, "Regenrisiko: %d%%", rain);

      // Debug-Ausgabe
      Serial.printf("T=%.1f  H=%d%%  Wind=%.0f km/h %s  Regen=%d%%\n",
                    temp, hum, wind, wind_dir_text(winddir), rain);
    } else {
      Serial.print("JSON parsen fehlgeschlagen: ");
      Serial.println(error.c_str());
    }
  } else {
    Serial.print("Fehler beim HTTP-Request. Code: ");
    Serial.println(httpResponseCode);
  }

  // Verbindung schliessen, um RAM freizugeben
  http.end();
}


So findest du die passende Wetter-URL für deinen Ort:


Siehe im Projekt https://prilchen.de/esp32-projekt-smart-weather-e-paper-display/



Was Ärger bereitete und was Abhilfe geschaffen hat:

Plege der Library „lv_conf.h“ ist mit entscheidend

Die lv_conf.h war mehrfach Ursache für Fehler und Speicherprobleme:

Fehler 1: LV_USE_SCALE 1 ohne LV_USE_LINE 1 → Compilerabbruch in lv_scale.c
Fehler 2: Default-lv_conf.h aktiviert alle Widgets, Fonts, Decoder, Demos → hunderte KB Flash und DRAM verschwendet
Fehler 3: LV_USE_LODEPNG 1 gesetzt, obwohl nur eingebettete C-Arrays genutzt werden → unnötiger Decoder-Ballast
Fehler 4: Zwei verschiedene lv_conf.h-Kopien im Projekt → Änderungen wirkten nicht

Die Regeln, die sich bewährt haben

RegelWarum
Nur was du wirklich nutzt, aktiviere mit 1Jedes Widget, jede Font, jeder Decoder kostet Flash und teils DRAM
Fonts einzeln aktivieren (Bei mir: MONTSERRAT_14, MONTSERRAT_28)Jede Schriftgröße ist ein eigener ~20–40 KB Block
LV_USE_LOG 0, ASSERT 0, PERF_MONITOR 0Debug-Zeug zur Analye kostet nur
Abhängigkeiten prüfenSCALE braucht LINE, CHART braucht LINE, TABLE braucht teils LABEL
Änderungen kommentierenZ. B. // für Tacho neben LV_USE_SCALE 1 – beim Aufräumen hilft das

Zusammenfassung für dieses Projekt

HARDWARE:

  • Board: ESP32-3248S035R (CYD 3.5″, lcdwiki 3.5inch ESP32-32E)
  • Display: ST7796S, 480×320, 4-Wire SPI, TFT_eSPI-Treiber
  • Pins: MOSI 13, MISO 12, SCLK 14, CS 15, DC 2, RST -1, BL 27
  • Touch: XPT2046, CS 33, IRQ 36 (input only)
  • RGB-LEDs: 22 (rot), 16 (grün), 17 (blau) – common anode
  • Flash 4 MB, Partition „Huge APP“ (3 MB App-Partition)
  • Kein PSRAM

ARDUINO-Librarys:

  • TFT_eSPI mit ST7796_DRIVER, TFT_INVERSION_ON, TFT_RGB_ORDER TFT_BGR,
    SPI_FREQUENCY 40000000
  • LVGL 9.5.x, lv_conf.h abgespeckt:
    LV_USE_LABEL 1, LV_USE_IMAGE 1, LV_USE_SCALE 1, LV_USE_LINE 1
    LV_FONT_MONTSERRAT_14 1, LV_FONT_MONTSERRAT_28 1
    LV_COLOR_DEPTH 16, LV_USE_LOG 0, LV_USE_LODEPNG 0
  • Eigener flush_cb, draw_buf 480×10 (partial render mode)
  • Logo als C-Array in logo.c eingebettet, über logo.h mit
    extern „C“ eingebunden

UI-INHALT (Wetter Anzeige):

  1. Prilchen Logo oben links (ca. 100×100 px, PNG mit Alpha,
    konvertiert zu RGB565A8,transparente Außenform)
  2. Tacho-Skala rechts (lv_scale, ROUND_INNER, 250×250 px):
    • Bereich -20 bis +50 °C, 71 Ticks, Major alle 10
    • 270° Sweep, Start bei 135° (7:30-Position)
    • Nadel als lv_line, gesetzt via
      lv_scale_set_line_needle_value(scale, line, 100, wert)
    • Zahlenwert mittig darunter (Montserrat 28, gelb)
  3. Links untereinander (grau, Montserrat 14):
    • „Stadt: Frechen“
    • „Regenrisiko: XX %“
    • „Wind: XX km/h NW“
    • „Luftfeuchtigkeit: XX %“
  4. Status unten links („online“ in grün / „WLAN…“ in weiß)

DATENQUELLE:

  • Open-Meteo API: URL siehe Setch http://api.open-meteo.com/…
  • Ausgewertet mit ArduinoJson (JsonDocument).
  • Regenrisiko: precipitation_probability (aktuelle Stunde).

FUNKTIONEN:

  • WLAN verbinden beim Start, Status-Label aktualisieren
  • Alle 15 min neue Daten holen
  • Nach 60 s Anzeige → Deep Sleep
    (Backlight aus, ST7796 Sleep In, RGB-LEDs auf INPUT, RTC-Pull-Up auf GPIO 36,
    esp_sleep_enable_ext0_wakeup auf fallende Flanke)
  • Aufwachen durch Touch → kompletter Neustart

Auch hat der Kompiler öfters das Problem bekommen zu wenig RAM vorzufinden. Gelöst wurde das mit Anpassung des Partition Scheme auf Huge App (3MB NoOTA/1MB SPIFFS) und kleineren Draw Buffer, aber für komplexere, mehrseitige Projekte empfiehlt sich eine CYD‑Version zB. mit ESP32‑S3 ab N8R8 mit mehr RAM.



Bei Akku Betrieb

Laut Herstellerdatenblatt hast du auf dem Board bereits eine Ladeverwaltung integriert. Du musst nichts extra verdrahten – Akku anstecken, USB-Kabel dran, fertig. Diesen habe ich verwendet: LiPo-Akku 3,7 V 3000 mAh mit PCM-Schutz, Micro-JST-1,25-mm-2-Pin

Was das Datenblatt sagt

ParameterWert
Akku-Anschluss1,25 mm 2P (JST-SH)
Akku-Typ3,7 V Polymer-Lithium
Ladespannung5 V (typisch)
Ladestrommax. 500 mA, Modul real ~290 mA
Ladeschlussspannung4,24 V

Quelle: 3.5inch ESP32-32E Display – LCD wiki

Was du beim Akku beachten musst

1. Nur 3,7 V LiPo-Akkus – keine LiFePO4, keine 1S-Zellen mit anderer Chemie, keine zwei Zellen in Serie. Die Ladeschaltung ist fest auf 4,2 V Ladeschluss ausgelegt.

2. Polarität prüfen! Das Datenblatt warnt ausdrücklich: „Note the positive and negative terminals of the interface.“ Ein verpolt angeschlossener Akku kann die Ladeelektronik zerstören. Prüf die Belegung des Steckers vor dem Einstecken mit einem Multimeter durch.

3. USB liefert gleichzeitig – das Board kann laden und betreiben parallel. Das ist der Normalfall, wenn du es am USB-Kabel betreibst.

4. Kein Überschutz? Viele kleine Boards haben keinen Tiefentladeschutz. Ein 3,7 V LiPo mit integrierter Schutzplatine (PCM) ist die sichere Wahl.



3D Druck Gehäuse

Das 3D Gehäuse habe ich von SaltyAce – Case for Hosyond ESP32 3.5 Inch Display (CYD) verwendet. Es hat je eine Öffnung für USB-C Anschluss und SD Card. (Ein Betrieb mit angesteckten Aktoren und Sensoren ist bei diesem Gehäuse nicht vorgesehen)
Verschraubt habe ich es mit 4 M3x12mm Schrauben.


Vorschau:

Es wird noch einen Teil 3 geben: Wie das ganze mit dem kostenlose GUI Erstellungstool EEZ Studio zum laufen gebracht werden kann.

, ,
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.