Skip to content

Technik Blog

Programmieren | Arduino | ESP32 | MicroPython | Python | Raspberry Pi | Raspberry Pi Pico

Menu
  • Smarthome
  • Gartenautomation
  • Mikrocontroller
    • Arduino
    • ESP32 & Co.
    • Calliope Mini
    • Raspberry Pi & Pico
  • Solo Mining
  • Deutsch
  • English
Menu

ESP32-Display als Edelmetall-Monitor: Gold- und Silberpreis in Echtzeit

Veröffentlicht am 20. Januar 202620. Januar 2026 von Stefan Draeger

In diesem LVGL Dashboard ESP32 Tutorial zeige ich dir, wie du mit einem ESP32-Display ein eigenes Touch-Dashboard für aktuelle Gold- und Silberpreise aufbaust. Die Idee zu diesem Projekt kam diesmal nicht aus meiner eigenen Bastelkiste, sondern direkt aus der Community: Über WhatsApp wurde ich gefragt, ob und wie man auf einem ESP32-Display die Preise übersichtlich, automatisch aktualisiert und ohne großen Aufwand anzeigen kann. Auf den ersten Blick wirkt das wie ein kleines Projekt, doch dahinter steckt ein spannender Mix aus API-Anbindung, Datenverarbeitung und einer sauberen Darstellung auf einem TFT-Display.

Playlist: ESP32 CYD Display mit LVGL programmieren - Komplettkurs
Diese Wiedergabeliste ansehen auf YouTube

Genau diesen Ansatz greife ich in diesem Beitrag auf. Wir bauen gemeinsam einen Edelmetall-Monitor auf Basis eines ESP32-2432S028, der den aktuellen Gold- und Silberpreis aus einer öffentlichen API abruft und grafisch auf dem Display darstellt. Neben einem klaren Layout mit Icons für Gold und Silber wird auch ein Zeitstempel der letzten Aktualisierung angezeigt – so ist jederzeit ersichtlich, wie aktuell die Daten sind.

Edelmetall Dashboard mit ESP32-2432S028 - deutsch
Edelmetall Dashboard mit ESP32-2432S028 - englisch

Das Projekt eignet sich hervorragend, um zu zeigen, wie einfach sich externe Datenquellen in Mikrocontroller-Projekte integrieren lassen. Gleichzeitig ist der Anwendungsfall bewusst praxisnah gewählt und lässt sich später problemlos erweitern – etwa um weitere Metalle, Preisalarme oder eine automatische Aktualisierung in festen Intervallen.

Community-Hinweis
Du hast Fragen zu eigenen Projekten, brauchst Hilfe oder eine Idee, die ich mir anschauen soll? Schreib mir gern per E-Mail, über das Support-Ticket auf dem Blog oder direkt per WhatsApp. Viele Beiträge entstehen genau aus solchen Community-Anfragen.

Inhaltsverzeichnis

  • TL;DR / Ergebnis (Download)
    • Was du am Ende bekommst
    • Download
  • Hardware & Voraussetzungen
  • Eingesetzte Software
  • Datenquelle: Edelmetallpreise per JSON abrufen
    • API-Test mit Postman
  • Einrichten in der Arduino IDE
  • Einstieg in die Programmierung mit LVGL
    • Installieren von LVGL, TFT_eSPI & XPT2046_Touchscreen
    • Konfiguration für das Board ESP32-2432S028 für LVGL & TFT_eSPI
  • Schritt-für-Schritt Anleitung zum erstellen des Dashboards
    • Labels
    • Buttons
    • Laden der Daten von der API
      • Aufbau der WiFi Verbindung
      • Abrufen des JSON via HTTP Request
    • Laden des Zeitstempels
    • Quellcode
  • Problem – Textbereich überschreitet den verfügbaren Platz auf der Platine
    • Schritt 1 – nur benötigte Fonts laden
    • Schritt 2 – Setzen des Partion Scheme
    • Ergebniss
  • Fazit

TL;DR / Ergebnis (Download)

Wenn du nicht erst alles Schritt für Schritt nachbauen willst: Hier bekommst du das fertige Edelmetall-Dashboard als Download.

Edelmetall Dashboard auf Cheap Yello Display

Was du am Ende bekommst

  • ESP32-2432S028 Dashboard mit Gold- & Silberpreis in Echtzeit
  • Anzeige in EUR und USD
  • Zeitstempel der letzten Aktualisierung
  • Touch-Buttons für Sprache (DE/US) und „Aktualisieren“
  • Erweiterbar um Platin & Palladium (im Code bereits vorbereitet/integriert)

Download

fertiges Projekt – Edelmetall Dashboard am Cheap Yellow DisplayHerunterladen

So startest du in 3 Minuten (inkl. Libraries & Setup)

  1. Download & Sketch öffnen
  • Download entpacken und den Projektordner in deinen Arduino-Sketchbook kopieren
  • Sketch PreciousMetalDashboard.ino öffnen
  1. Benötigte Bibliotheken installieren (Bibliotheksverwalter)
    Installiere diese Libraries:
  • ArduinoJson
  • lvgl
  • TFT_eSPI
  • XPT2046_Touchscreen
  1. Board & Partition Scheme einstellen (Arduino IDE)
  • Board: ESP32 Dev Module
  • Partition Scheme: Huge APP (3MB No OTA/1MB SPIFFS)
  1. TFT_eSPI konfigurieren (User_Setup.h)
    In ...\Documents\Arduino\libraries\TFT_eSPI\User_Setup.h die Konfiguration für das ESP32-2432S028 setzen (Treiber, Pins, Frequenzen).
    Den exakten Block findest du weiter unten im Beitrag (zum Kopieren).
  2. LVGL konfigurieren (lv_conf.h)
    Die passende lv_conf.h für das Board verwenden (und bei Bedarf Fonts reduzieren, um Speicher zu sparen).
    Auch hier: Datei/Block findest du weiter unten im Beitrag.
  3. Flashen
    Sketch kompilieren und auf das Board hochladen. Fertig.

Hardware & Voraussetzungen

Für dieses Projekt benötigen wir nicht viele Komponenten, da wir alles Nötige bereits auf dem ESP32-2432S028 vorfinden. Display, Touch-Funktion und WLAN sind hier bereits integriert, wodurch sich der Aufbau auf ein Minimum reduziert und der Fokus klar auf der Software liegt.

  • ESP32-2432S028*
    ESP32-Board mit integriertem 2,8″ TFT-Display (240 × 320 Pixel) und Touch-Funktion.
    Ideal für Dashboard- und Visualisierungsprojekte, da keine zusätzliche Display-Hardware erforderlich ist.
  • USB-Kabel* (je nach Board Micro-USB oder USB-C)
    Wird zum Flashen des Codes und zur Stromversorgung des Boards benötigt.
  • WLAN-Zugang
    Die Edelmetallpreise werden über eine öffentliche REST-API abgerufen, daher ist eine aktive WLAN-Verbindung erforderlich.
Lieferumfang - ESP32 mit 2.8
Lieferumfang – ESP32 mit 2.8″ TFT-Display & Touch (ESP32-2432S028R)

Hinweis von mir: Die mit einem Sternchen (*) markierten Links sind Affiliate-Links. Wenn du über diese Links einkaufst, erhalte ich eine kleine Provision, die dazu beiträgt, diesen Blog zu unterstützen. Der Preis für dich bleibt dabei unverändert. Vielen Dank für deine Unterstützung!

Eingesetzte Software

Für dieses Projekt nutze ich die Arduino IDE in Kombination mit der LVGL-Library. Gerade auf dem ESP32-2432S028 spielt LVGL seine Stärken aus, da sich damit strukturierte und moderne Benutzeroberflächen für TFT-Displays umsetzen lassen.

Auf den ersten Blick wirkt LVGL recht komplex, doch nach einer kurzen Einarbeitung macht die Arbeit damit richtig Spaß. Layouts, Texte und grafische Elemente lassen sich sauber aufbauen und später leicht anpassen – ideal für ein Dashboard wie in diesem Projekt.
Auf die Einrichtung und Programmierung gehe ich im weiteren Verlauf des Beitrags Schritt für Schritt ein.

Datenquelle: Edelmetallpreise per JSON abrufen

Als Datenquelle nutze ich edelmetalle.de, da sich die aktuellen Preise dort sehr einfach über eine öffentliche JSON-Schnittstelle abrufen lassen. Die API ist frei zugänglich und kann direkt per HTTP-Request abgefragt werden: https://api.edelmetalle.de/public.json

Über diesen Endpunkt erhalten wir die aktuellen Preise für verschiedene Edelmetalle – unter anderem Gold und Silber – jeweils in US-Dollar und Euro. Zusätzlich liefert die API den aktuellen Wechselkurs zwischen USD und EUR sowie einen Zeitstempel des API-Abrufs.

Ein gekürztes Beispiel der zurückgelieferten Daten sieht so aus:

{
  "gold_usd": 4631.5,
  "gold_eur": 3976.45,
  "silber_usd": 90.0455,
  "silber_eur": 77.3055,
  "platin_usd": 2401.66,
  "platin_eur": 2061.91,
  "palladium_usd": 1856.54,
  "palladium_eur": 1593.9,
  "timestamp": 1768380127,
  "wechselkurs_usd_eur": 1.16473236177998
}

Wichtig zu wissen:
Der enthaltene timestamp gibt nicht den Zeitpunkt der Preiserhebung an, sondern lediglich den Zeitpunkt, zu dem die Daten von der API ausgeliefert wurden. Wann die Preise ursprünglich ermittelt wurden, geht aus den Daten selbst leider nicht hervor. Für dieses Projekt ist das jedoch unkritisch, da wir den Zeitstempel bewusst als „letzte Aktualisierung“ auf dem Display anzeigen.

Im nächsten Schritt kümmern wir uns darum, welche Werte wir konkret verwenden und wie diese später sauber auf dem Display dargestellt werden.

API-Test mit Postman

Um die API schnell zu testen, eignet sich Postman hervorragend. Damit lässt sich der Abruf der Daten prüfen, ohne direkt Code zu schreiben.

Dazu wird in Postman einfach die folgende Adresse eingetragen: https://api.edelmetalle.de/public.json

Als HTTP-Methode kann GET oder POST verwendet werden – beide liefern dieselbe Antwort. Weitere Header oder Parameter sind nicht erforderlich.
Nach dem Absenden der Anfrage gibt die API direkt das JSON mit den aktuellen Edelmetallpreisen, dem Wechselkurs sowie dem Zeitstempel zurück.

Postman - Abruf der Daten von edelmetalle-de

Einrichten in der Arduino IDE

Damit das Board ausgewählt werden kann muss zunächst der Boardtreiber installiert werden, dieses kann man bequem über den Boardverwalter der Arduino IDE erledigen.
Hier sucht man zunächst nach „esp32“ und wählt anschließend die Schaltfläche „INSTALL“ am Eintrag „esp32 by Espressif Systems“.

Arduino IDE - Boardverwalter - ESP32
ESP32-2432S028 - CYD - Einstellungen in der Arduino IDE

Sobald der Treiber installiert wurde (das kann etwas dauern) können wir aus der Liste den Eintrag „ESP32 Dev Module“ für das Board ESP32-2432S028 auswählen und setzen zusätzlich noch das Partition Scheme auf „Huge APP (3MB No CTA/1MB SPIFFS)“.
Auf letzteren Eintrag gehe später nochmal genauer drauf ein.

Einstieg in die Programmierung mit LVGL

Für die Umsetzung nutzen wir LVGL – und genau hier kommt auch der erste Punkt, der mir an der Library nicht ganz so gut gefällt: Nach der Installation ist man nicht sofort startklar. LVGL ist zwar für extrem viele Boards und Displays geeignet, aber genau das bedeutet auch: Jedes Board ist anders aufgebaut – und deshalb müssen wir einmalig festlegen, wie Display und Touch überhaupt angesteuert werden.

Konkret heißt das: Wir konfigurieren manuell, welche Treiber genutzt werden, welche Pins belegt sind und wie LVGL mit dem Display sowie der Touch-Funktion kommuniziert. Hat man diesen Teil einmal sauber erledigt, lässt sich später aber sehr angenehm damit arbeiten.

Damit du diesen Setup-Teil nicht zweimal suchen musst: In meinem Beitrag „ESP32 Development Board mit 2,8 Zoll Touch Display: Programmieren für Anfänger“ habe ich die komplette Einrichtung bereits Schritt für Schritt gezeigt:
https://draeger-it.blog/esp32-development-board-mit-2-8-zoll-touch-display-programmieren-fuer-anfaenger/

Zusätzlich wiederhole ich diese Schritte im oben verlinkten YouTube-Video noch einmal, damit du alle Informationen direkt zur Hand hast und ohne Umwege mit diesem Projekt starten kannst.

Installieren von LVGL, TFT_eSPI & XPT2046_Touchscreen

Für das ansteuern des Displays sowie das auswerten der Touchaktion benötigen wir zusätzliche Bibliotheken welche wir über den Bibliotheksverwalter installieren.

Bibliothek lvgl im Bibliotheksverwalter
Bibliothek TFT_eSPI im Bibliotheksverwalter
Bibliothek XPT2046_Touchscreen im Bibliotheksverwalter

Konfiguration für das Board ESP32-2432S028 für LVGL & TFT_eSPI

In der Datei C:\Users\<Benutzername>\Documents\Arduino\libraries\TFT_eSPI\User_Setup.h werden die nachfolgenden Daten ersetzt.

//#define ILI9341_DRIVER
#define ILI9341_2_DRIVER 
// #define TFT_WIDTH  80
// #define TFT_WIDTH  128
// #define TFT_WIDTH  172 // ST7789 172 x 320
// #define TFT_WIDTH  170 // ST7789 170 x 320
#define TFT_WIDTH  240 // ST7789 240 x 240 and 240 x 320
// #define TFT_HEIGHT 160
// #define TFT_HEIGHT 128
// #define TFT_HEIGHT 240 // ST7789 240 x 240
#define TFT_HEIGHT 320 // ST7789 240 x 320
// #define TFT_HEIGHT 240 // GC9A01 240 x 240
//#define TFT_MISO  PIN_D6  // Automatically assigned with ESP8266 if not defined
//#define TFT_MOSI  PIN_D7  // Automatically assigned with ESP8266 if not defined
//#define TFT_SCLK  PIN_D5  // Automatically assigned with ESP8266 if not defined

//#define TFT_CS    PIN_D8  // Chip select control pin D8
//#define TFT_DC    PIN_D3  // Data Command control pin
//#define TFT_RST   PIN_D4  // Reset pin (could connect to NodeMCU RST, see next line)
//#define TFT_RST  -1     // Set TFT_RST to -1 if the display RESET is connected to NodeMCU RST or 3.3V

#define TFT_BL   21           
#define TFT_BACKLIGHT_ON HIGH  
#define TFT_MOSI 13 
#define TFT_SCLK 14
#define TFT_CS   15  
#define TFT_DC   2  
#define TFT_RST  12  
#define TFT_BL   21 
//#define SPI_FREQUENCY  27000000
#define SPI_FREQUENCY  55000000 
#define SPI_READ_FREQUENCY  20000000
...
//#define SPI_READ_FREQUENCY  20000000

Alternativ kannst du auch den kompletten Inhalt der Datei mit nachfolgendem ersetzen.

#define ILI9341_2_DRIVER    
#define TFT_WIDTH  240
#define TFT_HEIGHT 320
#define TFT_BL   21           
#define TFT_BACKLIGHT_ON HIGH  
#define TFT_MOSI 13 
#define TFT_SCLK 14
#define TFT_CS   15  
#define TFT_DC   2  
#define TFT_RST  12  
#define TFT_BL   21 
#define TOUCH_CS 33  
#define LOAD_GLCD   
#define LOAD_FONT2  
#define LOAD_FONT4  
#define LOAD_FONT6  
#define LOAD_FONT7  
#define LOAD_FONT8  
#define LOAD_GFXFF  
#define SMOOTH_FONT
#define SPI_FREQUENCY  55000000 
#define SPI_READ_FREQUENCY  20000000
//Touch Screen: ?????????
#define XPT2046_IRQ 36
#define XPT2046_MOSI 32
#define XPT2046_MISO 39
#define XPT2046_CLK 25
#define XPT2046_CS 33
#define SPI_TOUCH_FREQUENCY  2500000

Für die Bibliothek LVGL müssen wir eine Konfiguration anlegen, hier habe ich mich aus dem englischen Tutorial LVGL with ESP32 Cheap Yellow Display Board bedient.

Datei lv_conf.h für das ESP32-2432S028 BoardHerunterladen

Schritt-für-Schritt Anleitung zum erstellen des Dashboards

Das Dashboard ist grafisch bewusst schlicht gehalten – der eigentliche Schwerpunkt liegt nicht auf aufwendiger Optik, sondern auf der sauberen technischen Umsetzung im Hintergrund. Im Backend passiert deutlich mehr, als man auf den ersten Blick sieht: Wir bauen eine WLAN-Verbindung auf, laden die aktuellen Preise per HTTP von der API, parsen das JSON und ergänzen die Anzeige um einen korrekt formatierten Zeitstempel.

In den folgenden Abschnitten zeige ich dir Schritt für Schritt, wie du Labels und Buttons mit LVGL erstellst und positionierst, wie du die Daten von der API abrufst und wie du diese anschließend dynamisch im UI aktualisierst.

Alle gezeigten Codebeispiele sind dabei für sich lauffähig. Du kannst sie direkt testen, einzeln anpassen und daraus nach und nach dein eigenes Dashboard aufbauen – ganz ohne alles auf einmal verstehen zu müssen.

Beispiele zum aufbauen des Dashboard am ESP32-2432S028 – CYDHerunterladen

Labels

Als erstes zeige ich dir wie man Labels auf dem Display erstellt und anzeigt. Dabei können wir uns aus vielen Möglichkeiten der Formatierung bedienen.

#include <lvgl.h>       // LVGL: Grafik-/UI-Library (Buttons, Labels, Layout, Styles, usw.)
#include <TFT_eSPI.h>   // Display-Treiber-Library (wird von lv_tft_espi_create intern genutzt)

/* ------------------------------------------------------------
   Display-/LVGL-Grundkonfiguration
   ------------------------------------------------------------ */

// Physische Auflösung deines Displays (hier: 240x320)
#define TFT_HOR_RES 240
#define TFT_VER_RES 320

// LVGL-Rotation (270° entspricht i.d.R. „quer“, je nach Board/Display)
#define TFT_ROTATION LV_DISPLAY_ROTATION_270

/* LVGL zeichnet nicht direkt auf das Display, sondern in einen „Draw Buffer“.
   Dieser Buffer wird anschließend vom Display-Treiber auf das Display übertragen.

   Hier wird die Buffergröße auf 1/10 der Displayfläche gesetzt:
   TFT_HOR_RES * TFT_VER_RES / 10

   Zusätzlich hängt es von der Farbtiefe ab (LV_COLOR_DEPTH), deshalb:
   * (LV_COLOR_DEPTH / 8)  -> Bytes pro Pixel
*/
#define DRAW_BUF_SIZE (TFT_HOR_RES * TFT_VER_RES / 10 * (LV_COLOR_DEPTH / 8))

/* Der Buffer ist hier als uint32_t Array angelegt.
   LVGL erwartet nur einen Speicherbereich; die Umrechnung auf /4 ist,
   weil uint32_t = 4 Bytes.
*/
uint32_t draw_buf[DRAW_BUF_SIZE / 4];

/* ------------------------------------------------------------
   Farben als Hex-Werte definieren (RGB in 0xRRGGBB)
   ------------------------------------------------------------ */

#define COLOR_BG     lv_color_hex(0x000000)  // Hintergrund: Schwarz
#define COLOR_RED    lv_color_hex(0xE53935)  // kräftiges Rot
#define COLOR_GREEN  lv_color_hex(0x43A047)  // sattes Grün
#define COLOR_BLUE   lv_color_hex(0x1E88E5)  // klares Blau
#define COLOR_ORANGE lv_color_hex(0xFB8C00)  // Orange
#define COLOR_PURPLE lv_color_hex(0x8E24AA)  // Violett
#define COLOR_CYAN   lv_color_hex(0x00ACC1)  // Cyan
#define COLOR_YELLOW lv_color_hex(0xFDD835)  // Gelb
#define COLOR_PINK   lv_color_hex(0xD81B60)  // Pink

/* ------------------------------------------------------------
   Funktionsprototyp
   (empfohlen, damit Arduino/C++ die Funktion schon kennt, bevor setup() sie aufruft)
   ------------------------------------------------------------ */
static lv_obj_t *createLabel(lv_obj_t *screen, lv_color_t color, const lv_font_t *font, int index);

/* ------------------------------------------------------------
   setup(): läuft genau 1x nach dem Start/Reset
   Hier wird LVGL initialisiert und die UI aufgebaut.
   ------------------------------------------------------------ */
void setup() {
  Serial.begin(9600);  // Serielle Ausgabe für Debugging (z.B. Index-Ausgaben)

  lv_init();           // Initialisiert LVGL (muss immer vor LVGL-Aufrufen passieren)

  /* Erstellt ein LVGL-Display-Objekt, das TFT_eSPI als Backend verwendet.
     - TFT_HOR_RES / TFT_VER_RES: Auflösung
     - draw_buf / sizeof(draw_buf): Zeichenpuffer + Größe in Bytes
  */
  lv_display_t *disp = lv_tft_espi_create(
    TFT_HOR_RES,
    TFT_VER_RES,
    draw_buf,
    sizeof(draw_buf)
  );

  // Setzt die gewünschte Displayrotation (z.B. 270°)
  lv_display_set_rotation(disp, TFT_ROTATION);

  /* LVGL arbeitet mit Screens (Bildschirmen).
     lv_screen_active() liefert den aktuell aktiven Screen.
     Auf diesem Screen platzieren wir unsere Labels.
  */
  lv_obj_t *scr = lv_screen_active();

  // Hintergrundfarbe des Screens setzen
  lv_obj_set_style_bg_color(scr, COLOR_BG, 0);

  // Hintergrund vollständig deckend zeichnen
  lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);

  // Innenabstand (Padding) des Screens: sorgt für Randabstand zur Displaykante
  lv_obj_set_style_pad_all(scr, 12, 0);

  // Scrollbars ausschalten (weil wir einen festen Screen ohne Scrollen wollen)
  lv_obj_set_scrollbar_mode(scr, LV_SCROLLBAR_MODE_OFF);

  /* ------------------------------------------------------------
     UI-Elemente erzeugen: hier 8 Labels
     Jedes Label bekommt je nach Index eine eigene Farbe und Fontgröße.
     ------------------------------------------------------------ */
  for (int index = 0; index < 8; index++) {

    // Default-Werte (werden im Switch ggf. überschrieben)
    lv_color_t color = COLOR_RED;
    const lv_font_t *font = &lv_font_montserrat_8;

    // Je nach index: Farbe + Fontgröße auswählen
    switch (index) {
      case 0: color = COLOR_RED;    font = &lv_font_montserrat_8;  break;
      case 1: color = COLOR_GREEN;  font = &lv_font_montserrat_10; break;
      case 2: color = COLOR_BLUE;   font = &lv_font_montserrat_12; break;
      case 3: color = COLOR_ORANGE; font = &lv_font_montserrat_14; break;
      case 4: color = COLOR_PURPLE; font = &lv_font_montserrat_16; break;
      case 5: color = COLOR_CYAN;   font = &lv_font_montserrat_18; break;
      case 6: color = COLOR_YELLOW; font = &lv_font_montserrat_20; break;
      case 7: color = COLOR_PINK;   font = &lv_font_montserrat_22; break;
    }

    // Debug-Ausgabe: zeigt, dass wir wirklich 0..7 einmal durchlaufen
    Serial.println(index);

    // Erzeugt ein Label mit den gewählten Parametern
    createLabel(scr, color, font, index);
  }
}

/* ------------------------------------------------------------
   createLabel(): Hilfsfunktion, die ein LVGL Label anlegt und formatiert
   ------------------------------------------------------------ */
static lv_obj_t *createLabel(lv_obj_t *screen, lv_color_t color, const lv_font_t *font, int index) {

  // Neues Label-Objekt erstellen (Parent = screen)
  lv_obj_t *lbl = lv_label_create(screen);

  // Text des Labels setzen
  lv_label_set_text(lbl, "Hallo Welt!");

  // Textfarbe setzen
  lv_obj_set_style_text_color(lbl, color, 0);

  // Schriftart (Font) setzen
  // Wichtig: font ist bereits ein Pointer auf einen const-Font -> KEIN & davor!
  lv_obj_set_style_text_font(lbl, font, 0);

  /* ------------------------------------------------------------
     Dynamische Positionierung:
     Statt fester Abstände (index * 10) nutzen wir die echte Font-Zeilenhöhe.
     font->line_height ist die empfohlene Zeilenhöhe in Pixeln für diese Schrift.
     ------------------------------------------------------------ */

  int line_height = font->line_height;        // Zeilenhöhe der gewählten Schrift
  int y_offset = index * (line_height + 3);   // +3 = kleiner zusätzlicher Abstand zwischen Labels

  // Label oben mittig ausrichten und mit y_offset nach unten schieben
  lv_obj_align(lbl, LV_ALIGN_TOP_MID, 0, y_offset);

  // Rückgabe: falls du später das Label-Objekt speichern oder ändern willst
  return lbl;
}

/* ------------------------------------------------------------
   loop(): läuft permanent
   LVGL braucht regelmäßige „Ticks“ und muss Timer/Animationen abarbeiten.
   ------------------------------------------------------------ */
void loop() {
  /* lv_tick_inc(x) sagt LVGL: „Es sind x Millisekunden vergangen.“
     Sinnvoll ist es, das vor lv_timer_handler() aufzurufen.
  */
  lv_tick_inc(5);

  /* Arbeitet LVGL-interne Timer, Animationen, Input-Events, Redraws ab.
     Muss regelmäßig aufgerufen werden, sonst reagiert UI nicht korrekt.
  */
  lv_timer_handler();

  // Kurze Pause, damit wir ~ alle 5ms einen Tick haben
  delay(5);
}

Der obige Code erzeugt die Ausgabe das der Text „Hallo Welt!“ in verschiedenen Textgrößen und Farben auf dem CYD angezeigt werden

Labels mit LVGL auf dem ESP32 CYD (ESP32-2432S028)
Labels mit LVGL auf dem ESP32 CYD (ESP32-2432S028)

Buttons

Unter den Footer möchte ich zwei Buttons bereitstellen, einen zum wechsel der Sprache und einen zum aktualisieren der Daten.

Nachfolgend ein einfaches Beispiel welches einen Button darstellt, eine Funktion aufruft und einen Counter hochzählt und anzeigt.

/***************************************************************
 *  LVGL + TFT_eSPI + XPT2046 Touch auf ESP32-2432S028 (o.ä.)
 *  ------------------------------------------------------------
 *  Ziel:
 *   - Display initialisieren (LVGL zeichnet über TFT_eSPI)
 *   - Touch-Controller XPT2046 anbinden und LVGL als Pointer-Input geben
 *   - Button auf dem Screen anzeigen
 *   - Beim Touch/Press auf den Button wird ein Zähler hochgezählt
 *   - Zähler wird als großes Label angezeigt
 ***************************************************************/

#include <lvgl.h>                 // LVGL UI-Library (Labels, Buttons, Styles, Layout, Timer, etc.)
#include <TFT_eSPI.h>             // Display-Treiber-Library (LVGL nutzt sie im Backend)
#include <XPT2046_Touchscreen.h>  // Touchscreen-Library für XPT2046 (liefert Rohwerte x/y)

/* ------------------------------------------------------------
   Touchscreen Pinbelegung (XPT2046)
   ------------------------------------------------------------ */
#define XPT2046_IRQ  36   // IRQ-Pin (Touch Interrupt) -> signalisiert Berührung
#define XPT2046_MOSI 32   // SPI MOSI
#define XPT2046_MISO 39   // SPI MISO
#define XPT2046_CLK  25   // SPI Clock
#define XPT2046_CS   33   // Chip Select für Touch-Controller

/* Wir nutzen den HSPI-Bus (ESP32 hat i.d.R. VSPI und HSPI).
   touchSPI kapselt den SPI-Bus für den Touch-Controller.
*/
SPIClass touchSPI(HSPI);

/* Touchscreen-Objekt:
   - erster Parameter: CS-Pin
   - zweiter Parameter: IRQ-Pin
*/
XPT2046_Touchscreen ts(XPT2046_CS, XPT2046_IRQ);

/* ------------------------------------------------------------
   Display-/LVGL-Konfiguration
   ------------------------------------------------------------ */
#define TFT_HOR_RES  240                        // horizontale Auflösung
#define TFT_VER_RES  320                        // vertikale Auflösung
#define TFT_ROTATION LV_DISPLAY_ROTATION_270    // Displayrotation in LVGL (hier 270°)

/* Forward Declaration (Funktions-Prototyp) für den Button-Callback.
   LVGL ruft diese Funktion später bei Events auf.
*/
static void on_btn_press(lv_event_t *e);

/* ------------------------------------------------------------
   Touch-Kalibrierwerte (Rohwerte -> Pixel)
   ------------------------------------------------------------
   Der XPT2046 liefert Rohwerte (z.B. 200..3700).
   Diese Werte musst du auf 0..240 bzw. 0..320 mappen.
*/
#define TS_X_MIN 200
#define TS_X_MAX 3700
#define TS_Y_MIN 240
#define TS_Y_MAX 3800

/* ------------------------------------------------------------
   LVGL Draw Buffer
   ------------------------------------------------------------
   LVGL zeichnet nicht direkt auf das Display, sondern in einen Buffer.
   Hier: 1/10 der Displayfläche als Buffergröße.
   LV_COLOR_DEPTH bestimmt die Farbtiefe (z.B. 16 Bit).
*/
#define DRAW_BUF_SIZE (TFT_HOR_RES * TFT_VER_RES / 10 * (LV_COLOR_DEPTH / 8))

/* draw_buf ist als uint32_t-Array angelegt.
   /4, weil uint32_t 4 Bytes hat.
*/
uint32_t draw_buf[DRAW_BUF_SIZE / 4];

/* ------------------------------------------------------------
   Farbdefinitionen (RGB in 0xRRGGBB)
   ------------------------------------------------------------ */
#define COLOR_BLACK  lv_color_hex(0x000000)
#define COLOR_GOLD   lv_color_hex(0xD4AF37)
#define COLOR_SILVER lv_color_hex(0xD9D9D9)
#define COLOR_WHITE  lv_color_hex(0xFFFFFF)
#define COLOR_BLUE   lv_color_hex(0x1E88E5)

/* ------------------------------------------------------------
   Globale Variablen
   ------------------------------------------------------------ */

/* tapCount zählt, wie oft der Button gedrückt wurde.
   volatile ist hier "okay", weil der Wert aus Callback-Kontext geändert wird.
   (Der LVGL Callback ist zwar keine echte ISR, aber volatile schadet nicht.)
*/
volatile int tapCount = 0;

/* Pointer auf das Label, das später den Zähler anzeigt.
   Muss global sein, weil wir im Button-Callback darauf zugreifen.
*/
lv_obj_t *lbl_counter;

/* ------------------------------------------------------------
   Touch Read Callback für LVGL
   ------------------------------------------------------------
   LVGL fragt regelmäßig über diese Funktion den Touch-Status ab.
   Wir müssen LVGL sagen:
     - ob gedrückt wird (PRESSED/RELEASED)
     - welche Koordinate (x/y) gedrückt wurde
*/
static void my_touch_read(lv_indev_t *indev, lv_indev_data_t *data) {

  // Prüfen, ob IRQ aktiv ist UND der Touch-Controller eine Berührung meldet
  if (ts.tirqTouched() && ts.touched()) {

    // Rohwerte vom Touchcontroller holen (nicht in Pixeln!)
    TS_Point p = ts.getPoint();

    /* Rohwerte -> Pixel umrechnen
       map() bildet [TS_X_MIN..TS_X_MAX] auf [1..TFT_HOR_RES] ab.
       Achtung: je nach Rotation kann X/Y vertauscht sein - das wird über
       ts.setRotation(...) und TFT_ROTATION beeinflusst.
    */
    int px = map(p.x, TS_X_MIN, TS_X_MAX, 1, TFT_HOR_RES);
    int py = map(p.y, TS_Y_MIN, TS_Y_MAX, 1, TFT_VER_RES);

    // Sicherheit: Koordinaten auf gültigen Bereich clampen
    if (px < 0) px = 0;
    if (px > TFT_HOR_RES - 1) px = TFT_HOR_RES - 1;
    if (py < 0) py = 0;
    if (py > TFT_VER_RES - 1) py = TFT_VER_RES - 1;

    // LVGL mitteilen: Touch ist gedrückt + Koordinate setzen
    data->state = LV_INDEV_STATE_PRESSED;
    data->point.x = px;
    data->point.y = py;

  } else {
    // Nicht berührt -> LVGL mitteilen: RELEASED
    data->state = LV_INDEV_STATE_RELEASED;
  }
}

/* ------------------------------------------------------------
   setup(): wird 1x nach Start/Reset ausgeführt
   ------------------------------------------------------------ */
void setup() {
  Serial.begin(9600);   // Debug-Ausgabe
  lv_init();            // LVGL initialisieren (muss als erstes passieren)

  /* Touch SPI initialisieren:
     - begin(CLK, MISO, MOSI, CS) legt die Pins für HSPI fest
  */
  touchSPI.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS);

  // Touch-Controller initialisieren und auf dem SPI-Bus starten
  ts.begin(touchSPI);

  // Touch-Rotation (separat vom Display!)
  // Je nach Einbaulage/Rotation musst du hier evtl. 0..3 testen.
  ts.setRotation(2);

  /* LVGL Display-Backend erstellen:
     lv_tft_espi_create nutzt TFT_eSPI intern, um LVGLs Buffer auf das Display zu flushen.
  */
  lv_display_t *disp = lv_tft_espi_create(
    TFT_HOR_RES,
    TFT_VER_RES,
    draw_buf,
    sizeof(draw_buf)
  );

  // Rotation in LVGL setzen (muss zu deiner Display-Ausichtung passen)
  lv_display_set_rotation(disp, TFT_ROTATION);

  /* Input-Device (Touch) in LVGL registrieren:
     - indev = neues Input-Device
     - Type = Pointer (Touch/Maus)
     - Read Callback = unsere my_touch_read Funktion
  */
  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);

  /* Aktiven Screen holen und Styles setzen */
  lv_obj_t *scr = lv_screen_active();

  // Schwarzer Hintergrund, voll deckend
  lv_obj_set_style_bg_color(scr, COLOR_BLACK, 0);
  lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);

  // Padding: Inhalt nicht direkt am Rand
  lv_obj_set_style_pad_all(scr, 12, 0);

  // Scrollbar deaktivieren
  lv_obj_set_scrollbar_mode(scr, LV_SCROLLBAR_MODE_OFF);

  /* ------------------------------------------------------------
     Button erstellen und stylen
     ------------------------------------------------------------ */
  lv_obj_t *btn = lv_btn_create(scr);            // Button als Kind des Screens
  lv_obj_set_size(btn, 80, 25);                  // Größe des Buttons
  lv_obj_set_pos(btn, 0, 0);                     // Position (x=0,y=0) relativ zum Screen/Padding

  // Event Callback: bei PRESSED (also beim "Drücken") wird on_btn_press aufgerufen
  lv_obj_add_event_cb(btn, on_btn_press, LV_EVENT_PRESSED, NULL);

  // Button-Style: Hintergrund Gold, Rand Silber
  lv_obj_set_style_bg_color(btn, COLOR_GOLD, 0);
  lv_obj_set_style_border_width(btn, 1, 0);
  lv_obj_set_style_border_color(btn, COLOR_SILVER, 0);

  /* Button-Beschriftung (Label im Button) */
  lv_obj_t *lbl_btn = lv_label_create(btn);      // Label als Kind des Buttons
  lv_obj_center(lbl_btn);                        // Label im Button zentrieren
  lv_label_set_text(lbl_btn, "Klick mich!");     // Text setzen
  lv_obj_set_style_text_font(lbl_btn, &lv_font_montserrat_10, 0);  // Font
  lv_obj_set_style_text_color(lbl_btn, COLOR_BLACK, 0);            // Textfarbe

  /* ------------------------------------------------------------
     Zähler-Label (große Zahl) erstellen
     ------------------------------------------------------------ */
  lbl_counter = lv_label_create(scr);            // Label direkt auf dem Screen
  lv_obj_set_style_text_font(lbl_counter, &lv_font_montserrat_28, 0); // große Schrift
  lv_obj_set_style_text_color(lbl_counter, COLOR_BLUE, 0);            // blaue Textfarbe
  lv_obj_set_pos(lbl_counter, 120, 100);          // Position (x,y) auf dem Screen

  /* Initial einmal anzeigen (tapCount startet bei 0, wird hier aber hochgezählt)
     -> handlePressBtn() erhöht sofort auf 1.
     Wenn du beim Start "0" sehen willst, setze tapCount nicht hoch, sondern:
        lv_label_set_text(lbl_counter, "0");
  */
  handlePressBtn();
}

/* ------------------------------------------------------------
   loop(): läuft dauerhaft
   LVGL benötigt regelmäßige Aufrufe, um:
     - Events zu verarbeiten
     - Animationen/Timer zu bedienen
     - Redraws auszuführen
   ------------------------------------------------------------ */
void loop() {
  /* Hinweis: Üblicher ist die Reihenfolge:
       lv_tick_inc(5);
       lv_timer_handler();
     Bei dir ist sie vertauscht – funktioniert oft trotzdem,
     aber ich würde es der Konvention nach tauschen.
  */
  lv_timer_handler();   // LVGL "arbeiten lassen"
  lv_tick_inc(5);       // LVGL sagen: 5ms sind vergangen
  delay(5);             // kleine Pause, damit die Tick-Rate passt
}

/* ------------------------------------------------------------
   handlePressBtn(): zentrale Logik, die beim Button-Press ausgeführt wird
   ------------------------------------------------------------ */
void handlePressBtn() {
  tapCount++;  // Zähler erhöhen

  /* LVGL erwartet const char* (C-String).
     Arduino String muss deshalb in einen C-String umgewandelt werden.
     Achtung: s.c_str() ist nur gültig solange 's' lebt.
     In der Praxis kopiert LVGL den Text sofort ins Label, daher ok.
     Noch robuster wäre snprintf() mit char-Buffer.
  */
  String s = String(tapCount, DEC);

  // Label-Text aktualisieren
  lv_label_set_text(lbl_counter, s.c_str());
}

/* ------------------------------------------------------------
   LVGL Button Callback
   ------------------------------------------------------------ */
static void on_btn_press(lv_event_t *e) {
  // Bei jedem Druck rufen wir die zentrale Logik auf
  handlePressBtn();
}

Wenn der Button betätigt wird, wird ein Zähler incrementiert und auf dem Display angezeigt. Diese Funktionalität kann man für das Projekt adaptieren da hier der Text mit dem neuen Wert ersetzt wird.

Der Vorteil ist wir aktualisieren nicht das komplette Display sondern lediglich den Text somit entsteht kein flackern oder blinken wie sonst.

Laden der Daten von der API

Die Daten für die Edelmetalle laden wir von https://api.edelmetalle.de/public.json.

Für das parsen der Daten von der API welche wir im JSON Format empfangen verwende ich die Arduino JSON Bibliothek diese habe ich hier auf dem Blog schon ausführlich behandelt und auch in dem ein oder anderen Projekt benutzt.

Duf findest zu dieser unter https://arduinojson.org/v6/example/ viele Beispiele wie du auf dem Mikrocontroller in der Arduino IDE JSON verarbeiten kannst.

Mit dem installieren des Boardtreibers für den ESP32 erhalten wir alle Bibliotheken zum aufbau der WiFi Verbindung sowie dem absenden des Requests an die oben genannte URL um das JSON zu empfangen.

Aufbau der WiFi Verbindung

Zunächst bauen wir die WiFi Verbindung auf, dazu benötigen wir die SSID sowie das Passwort des lokalen WLANs zu welchem wir uns verbinden wollen.

#include <WiFi.h>  
// Bindet die ESP32-WiFi-Library ein.
// Sie stellt alle Funktionen bereit, um sich mit einem WLAN zu verbinden,
// den Status abzufragen und IP-Adressen zu lesen.

// ------------------------------------------------------------
// WLAN-Zugangsdaten
// ------------------------------------------------------------

// Name (SSID) deines WLAN-Netzwerks
const char* ssid = "abc";

// Passwort deines WLAN-Netzwerks
const char* password = "12345678";

// ------------------------------------------------------------
// setup(): wird genau einmal nach dem Start/Reset ausgeführt
// ------------------------------------------------------------
void setup() {
  Serial.begin(9600);
  // Initialisiert die serielle Schnittstelle mit 9600 Baud.
  // Darüber geben wir Debug- und Statusmeldungen im Serial Monitor aus.

  Serial.print("Verbinde mit WiFi ");
  Serial.println(ssid);
  // Gibt aus, mit welchem WLAN wir uns verbinden wollen.

  WiFi.begin(ssid, password);
  // Startet den Verbindungsaufbau zum WLAN mit den angegebenen Zugangsdaten.

  // ------------------------------------------------------------
  // Warten, bis die Verbindung hergestellt ist
  // ------------------------------------------------------------
  while (WiFi.status() != WL_CONNECTED) {
    // Solange der Status NICHT "connected" ist, bleiben wir in dieser Schleife.

    delay(500);
    // 500 ms warten, damit wir nicht zu schnell pollen.

    Serial.print(".");
    // Gibt bei jedem Durchlauf einen Punkt aus,
    // damit man im Serial Monitor sieht, dass noch verbunden wird.
  }

  // Wenn wir hier ankommen, ist WiFi.status() == WL_CONNECTED

  Serial.println("\nVerbunden!");
  // Zeilenumbruch + Meldung: Verbindung erfolgreich.

  Serial.print("IP Adresse: ");
  Serial.println(WiFi.localIP());
  // Gibt die vom Router vergebene lokale IP-Adresse aus.
  // Diese brauchst du z.B. für:
  //  - Webserver auf dem ESP32
  //  - Zugriff per Browser
  //  - Debugging im Netzwerk
}

// ------------------------------------------------------------
// loop(): läuft dauerhaft, hier aber leer
// ------------------------------------------------------------
void loop() {
  // bleibt leer
  // In diesem Beispiel machen wir nach der Verbindung nichts mehr.
  // Typischerweise würde hier z.B. stehen:
  //  - Webserver bearbeiten
  //  - HTTP-Anfragen senden
  //  - Sensorwerte übertragen
}

Verbinde mit WiFi FRITZBox7590GI24
..............
Verbunden!
IP Adresse: 192.168.178.111

Abrufen des JSON via HTTP Request

Da die Daten aus erfahrung nur alle 5 Minuten aktualisiert werden reicht es das wir im Code ebenso machen, daher wird der Timer auf 300000 Millisekunden gesetzt.

#include <ArduinoJson.h>
#include <ArduinoJson.hpp>
// ArduinoJson: Library zum Parsen und Erzeugen von JSON-Daten.
// Wir nutzen sie hier, um die API-Antwort auszulesen.

#include <WiFi.h>        // ESP32 WiFi-Funktionen
#include <HTTPClient.h> // HTTP-Client für GET/POST-Anfragen

// ------------------------------------------------------------
// WLAN-Zugangsdaten
// ------------------------------------------------------------
const char* ssid = "abc";         // Name deines WLANs (SSID)
const char* password = "12345678"; // WLAN-Passwort

// ------------------------------------------------------------
// API-URL für Edelmetallpreise
// ------------------------------------------------------------
String metalApiUrl = "https://api.edelmetalle.de/public.json";
// Diese URL liefert ein JSON-Dokument mit aktuellen Metallpreisen.

// ------------------------------------------------------------
// Timer-Variablen für periodisches Nachladen
// ------------------------------------------------------------
unsigned long lastTime = 0;          // Zeitpunkt des letzten erfolgreichen Abrufs
unsigned long timerDelay = 300000;   // Intervall in Millisekunden (300000 ms = 5 Minuten)

// ------------------------------------------------------------
// setup(): wird einmal nach dem Start ausgeführt
// ------------------------------------------------------------
void setup() {
  Serial.begin(9600);
  // Initialisiert die serielle Schnittstelle für Debug-Ausgaben.

  Serial.print("Verbinde mit WiFi ");
  Serial.println(ssid);
  // Ausgabe, mit welchem WLAN wir uns verbinden wollen.

  WiFi.begin(ssid, password);
  // Startet den Verbindungsaufbau zum WLAN.

  // Blockierende Warteschleife, bis WLAN verbunden ist
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);           // 500 ms warten
    Serial.print(".");   // Fortschritt anzeigen
  }

  Serial.println("\nVerbunden!");
  Serial.print("IP Adresse: ");
  Serial.println(WiFi.localIP());
  // Gibt die lokale IP-Adresse des ESP32 aus.

  // Nach erfolgreicher Verbindung sofort einmal Daten laden
  loadData();
}

// ------------------------------------------------------------
// loop(): läuft permanent
// Hier wird nur geprüft, ob das Zeitintervall abgelaufen ist.
// ------------------------------------------------------------
void loop() {

  // Prüfen, ob seit dem letzten Abruf mehr als timerDelay vergangen ist
  if ((millis() - lastTime) > timerDelay) {

    loadData();           // Neue Daten von der API holen
    lastTime = millis(); // Zeitpunkt aktualisieren
  }
}

// ------------------------------------------------------------
// loadData(): Lädt die JSON-Daten von der API per HTTP GET
// ------------------------------------------------------------
void loadData() {

  // Nur versuchen, wenn WLAN verbunden ist
  if (WiFi.status() == WL_CONNECTED) {

    HTTPClient http;
    // HTTPClient-Objekt anlegen (kapselt Verbindung + Request)

    http.begin(metalApiUrl.c_str());
    // Initialisiert die Verbindung mit der API-URL.
    // .c_str(), weil HTTPClient const char* erwartet.

    int httpResponseCode = http.GET();
    // Führt einen HTTP GET Request aus.
    // Rückgabewert ist der HTTP-Statuscode (z.B. 200, 404, 500, ...)

    if (httpResponseCode == 200) {
      // 200 = OK → gültige Antwort mit Daten

      String payload = http.getString();
      // Gesamten Response-Body als String auslesen.

      handleJsonPayload(payload);
      // JSON-Text an die Auswertefunktion übergeben.

    } else if (httpResponseCode == 304) {
      // 304 = Not Modified → keine neuen Daten seit letztem Abruf
      Serial.println("Keine neuen Daten!");

    } else {
      // Alle anderen Fehlercodes
      Serial.print("HTTP Error: ");
      Serial.println(httpResponseCode);
    }

    http.end();
    // Wichtig: Verbindung schließen und Ressourcen freigeben.

  } else {
    // WLAN ist nicht verbunden → kein Abruf möglich
    Serial.println("WiFi nicht verbunden – kein Refresh möglich.");
  }
}

// ------------------------------------------------------------
// handleJsonPayload(): Parst das JSON und liest die Metallpreise aus
// ------------------------------------------------------------
void handleJsonPayload(String json) {

  Serial.println(json);
  // Gibt das komplette JSON-Dokument im Serial Monitor aus (Debug-Zwecke).

  /* StaticJsonDocument:
     - Statischer Speicher auf dem Stack
     - Größe 400 Bytes → muss groß genug für das JSON sein
     - Zu klein = deserializeJson() schlägt fehl
  */
  StaticJsonDocument<400> doc;

  DeserializationError error = deserializeJson(doc, json);
  // Parst den JSON-Text und schreibt die Struktur in 'doc'.

  if (error) {
    // Wenn beim Parsen ein Fehler auftritt (z.B. zu wenig Speicher, ungültiges JSON)
    Serial.print(F("deserializeJson() failed: "));
    Serial.println(error.f_str());
    return;  // Abbruch, keine weiteren Zugriffe auf doc!
  }

  // ------------------------------------------------------------
  // Auslesen der USD-Werte aus dem JSON
  // ------------------------------------------------------------
  double gold_usd      = doc["gold_usd"];
  double silber_usd    = doc["silber_usd"];
  double platin_usd   = doc["platin_usd"];
  double palladium_usd = doc["palladium_usd"];

  // ------------------------------------------------------------
  // Auslesen der EUR-Werte aus dem JSON
  // ------------------------------------------------------------
  double gold_eur      = doc["gold_eur"];
  double silber_eur    = doc["silber_eur"];
  double platin_eur   = doc["platin_eur"];
  double palladium_eur = doc["palladium_eur"];

  // Ab hier könntest du:
  //  - die Werte auf ein Display schreiben
  //  - in Variablen speichern
  //  - per MQTT/HTTP weiterleiten
  //  - vergleichen (z.B. Alarm bei Grenzwerten)
}

Der Code lässt das JSON zunächst auf der seriellen Schnittstelle ausgeben. Zusätzlich werden die Daten schon in Variablen geparst und für den späteren Zugriff abgelegt.

Verbinde mit WiFi FRITZBox7590GI24
...
Verbunden!
IP Adresse: 192.168.178.111
{"gold_usd":4668.4,"gold_eur":4016.3,
"silber_usd":93.0583,"silber_eur":80.12,"platin_usd":2373.44,"platin_eur":2041.89,"palladium_usd":1815.86,"palladium_eur":1562.2,"timestamp":1768818427,"wechselkurs_usd_eur":1.1623633692702238}

Laden des Zeitstempels

Im Footer stelle ich zusätzlich den Zeitstempel bereit wann das letzte Mal die Daten abgerufen wurden. Theoretisch könnte ich auch den Zeitstempel aus dem zuvor ermittelten JSON verwenden jedoch muss dieser UNIX Zeitstempel noch in das korrekte Datumsformat umgewandelt werden, hier finde ich die Lösung einfacher an ein NTP Server zu gehen und sich die Daten von dort zu holen.

#include <WiFi.h>
// Stellt die WLAN-Funktionen des ESP32 bereit:
// - Verbindung aufbauen
// - Status abfragen
// - IP-Adresse lesen

#include "time.h"
// Stellt die Zeitfunktionen bereit:
// - NTP-Abfrage
// - struct tm
// - getLocalTime(), strftime(), configTzTime()

// ------------------------------------------------------------
// WLAN-Zugangsdaten
// ------------------------------------------------------------
const char* ssid = "abc";        // Name deines WLAN-Netzwerks (SSID)
const char* password = "12345678"; // Passwort des WLANs

// ------------------------------------------------------------
// Timer-Variablen für periodische Ausgabe
// ------------------------------------------------------------
unsigned long lastTime = 0;        // Zeitpunkt der letzten Zeit-Ausgabe (in ms seit Start)
unsigned long timerDelay = 60000; // Intervall: 60.000 ms = 60 Sekunden

// ------------------------------------------------------------
// NTP- und Zeitzonen-Konfiguration
// ------------------------------------------------------------
const char* ntpServer = "pool.ntp.org";
// Öffentlicher NTP-Server, von dem die Uhrzeit abgefragt wird.

/* Zeitzonen-String für Europe/Berlin:

   "CET-1CEST,M3.5.0/2,M10.5.0/3"

   Bedeutung:
   - CET-1      -> Basis-Zeitzone: Central European Time = UTC+1
   - CEST       -> Name der Sommerzeit (UTC+2)
   - M3.5.0/2   -> Letzter Sonntag im März, 02:00 Uhr: Wechsel auf Sommerzeit
   - M10.5.0/3  -> Letzter Sonntag im Oktober, 03:00 Uhr: Wechsel zurück auf Winterzeit

   Vorteil:
   -> Sommer-/Winterzeit wird automatisch korrekt berechnet.
*/
const char* tzInfo = "CET-1CEST,M3.5.0/2,M10.5.0/3";

// ------------------------------------------------------------
// Puffer für formatierte Zeitstrings
// ------------------------------------------------------------

// Deutsches Datumsformat: "DD.MM.YYYY - HH:MM"
char deTimeStr[25];

// US/ISO-Format: "YYYY-MM-DD - HH:MM"
char usTimeStr[25];

// ------------------------------------------------------------
// setup(): wird einmal nach Start/Reset ausgeführt
// ------------------------------------------------------------
void setup() {
  Serial.begin(9600);
  // Initialisiert die serielle Schnittstelle für Debug-Ausgaben.

  Serial.print("Verbinde mit WiFi ");
  Serial.println(ssid);
  // Gibt aus, mit welchem WLAN wir uns verbinden wollen.

  WiFi.begin(ssid, password);
  // Startet den Verbindungsaufbau zum WLAN.

  // Blockierende Warteschleife, bis WLAN verbunden ist
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);         // 500 ms warten
    Serial.print("."); // Fortschritt anzeigen
  }

  Serial.println("\nVerbunden!");
  Serial.print("IP Adresse: ");
  Serial.println(WiFi.localIP());
  // Gibt die lokale IP-Adresse aus, die der Router dem ESP32 zugewiesen hat.

  /* configTzTime():
     - Setzt Zeitzone + Sommerzeit-Regeln
     - Startet im Hintergrund die NTP-Synchronisation

     Ab jetzt liefert getLocalTime() automatisch:
       - korrekte Ortszeit
       - korrekte Sommer-/Winterzeit
  */
  configTzTime(tzInfo, ntpServer);

  // Direkt nach dem Start einmal die aktuelle Zeit ausgeben
  printTimestamp();
}

// ------------------------------------------------------------
// loop(): läuft dauerhaft
// Hier prüfen wir nur, ob 60 Sekunden vergangen sind.
// ------------------------------------------------------------
void loop() {

  // millis() liefert die Millisekunden seit Programmstart.
  // Wenn Differenz > timerDelay, dann ist eine Minute vorbei.
  if ((millis() - lastTime) > timerDelay) {

    printTimestamp();        // Aktuelle Uhrzeit erneut ausgeben
    lastTime = millis();    // Zeitpunkt speichern
  }
}

// ------------------------------------------------------------
// printTimestamp(): Holt aktuelle Zeit vom System und formatiert sie
// ------------------------------------------------------------
void printTimestamp() {

  struct tm timeinfo;
  // struct tm enthält:
  //  - tm_year, tm_mon, tm_mday
  //  - tm_hour, tm_min, tm_sec
  //  - tm_wday, tm_yday, tm_isdst
  // Diese Struktur füllt getLocalTime().

  if (!getLocalTime(&timeinfo)) {
    // Falls noch keine Zeit vom NTP-Server empfangen wurde
    Serial.println("Failed to obtain time");
    return;
  }

  /* strftime():
     Formatiert struct tm in einen lesbaren String.

     "%d.%m.%Y - %H:%M"
       %d = Tag (01-31)
       %m = Monat (01-12)
       %Y = Jahr (4-stellig)
       %H = Stunde (00-23)
       %M = Minute (00-59)
  */
  strftime(deTimeStr, sizeof(deTimeStr),
           "%d.%m.%Y - %H:%M", &timeinfo);

  /* ISO/US-Format:
       %Y-%m-%d - %H:%M
       z.B.: 2026-01-19 - 14:37
  */
  strftime(usTimeStr, sizeof(usTimeStr),
           "%Y-%m-%d - %H:%M", &timeinfo);

  // Ausgabe beider Formate im Serial Monitor
  Serial.println(deTimeStr);
  Serial.println(usTimeStr);
}

Der obige Code liefert einen aktuellen Zeitstempel und dieser wird mit der Funktion strftime jeweils ins deutsche und amerikanische Format formatiert.

Verbinde mit WiFi FRITZBox7590GI24
............
Verbunden!
IP Adresse: 192.168.178.111
19.01.2026 - 11:51
2026-01-19 - 11:51

Quellcode

Nachfolgend der Quellcode für das Dashboard als Download und zum kopieren.

Quellcode – für das Edelmetall DashboardHerunterladen
fertiger Quellcode

Die Zugangsdaten zum WiFi Netzwerk liegen in der Datei secrects.h.

// Deine WLAN-Zugangsdaten
const char* ssid     = "abc";
const char* password = "12345678";

Der gesamte Code ist wie folgt, dieser ist nachfolgend Kommentiert. Wichtige passagen erwähne ich im oben verlinkten YouTube Video.

/*
Titel: ESP32 Edelmetall-Dashboard (LVGL + Touch + WiFi + JSON + NTP)

Beschreibung:
Dieses Projekt zeigt ein kompaktes Edelmetall-Dashboard auf einem ESP32 mit ILI9341-Display
(z. B. "Cheap Yellow Display" / ESP32-2432S028). Die Oberfläche wird mit LVGL gerendert,
Touch-Eingaben erfolgen über einen XPT2046-Controller. Die Edelmetallpreise werden zyklisch
per HTTP als JSON von [https://api.edelmetalle.de/public.json](https://api.edelmetalle.de/public.json) geladen und anschließend auf
dem Display angezeigt. Zusätzlich wird über NTP die aktuelle Uhrzeit bezogen und als
Zeitstempel im Footer ausgegeben. Per Touch-Buttons kann zwischen DE/US Anzeige sowie
einem manuellen Refresh gewechselt werden.

Autor: Stefan Draeger
Blogbeitrag: [https://draeger-it.blog/esp32-display-als-edelmetall-monitor-gold-und-silberpreis-in-echtzeit/](https://draeger-it.blog/esp32-display-als-edelmetall-monitor-gold-und-silberpreis-in-echtzeit/)
*/

#include <ArduinoJson.h>
#include <ArduinoJson.hpp>

// LVGL GUI Library
#include <lvgl.h>

// TFT Display Treiber (Bodmer TFT_eSPI)
#include <TFT_eSPI.h>

// Touchscreen Library (XPT2046)
#include <XPT2046_Touchscreen.h>

// WiFi + HTTP für API Abruf
#include <WiFi.h>
#include <HTTPClient.h>

// Zeit (NTP)
#include "time.h"

// WLAN Zugangsdaten (lokal ausgelagert)
#include "secrets.h"

/* ------------------------------------------------------------
Touch-Pins (XPT2046) – passend zu deinem Board-Setup
------------------------------------------------------------ */
#define XPT2046_IRQ 36
#define XPT2046_MOSI 32
#define XPT2046_MISO 39
#define XPT2046_CLK 25
#define XPT2046_CS 33

// Touch läuft über HSPI (separater SPI-Bus vom Display möglich)
SPIClass touchSPI(HSPI);
XPT2046_Touchscreen ts(XPT2046_CS, XPT2046_IRQ);

/* ------------------------------------------------------------
Display / LVGL Einstellungen
------------------------------------------------------------ */
#define TFT_HOR_RES 240
#define TFT_VER_RES 320
#define TFT_ROTATION LV_DISPLAY_ROTATION_270

// LVGL zeichnet in einen Puffer. 1/10 Bildschirm ist oft ein guter Kompromiss
#define DRAW_BUF_SIZE (TFT_HOR_RES * TFT_VER_RES / 10 * (LV_COLOR_DEPTH / 8))
uint32_t draw_buf[DRAW_BUF_SIZE / 4];

// Tick-Zeit (hier aktuell nicht genutzt, da du lv_tick_inc(5) nutzt)
static uint32_t last_tick = 0;

/* ------------------------------------------------------------
Farben (Dark Theme)
------------------------------------------------------------ */
#define COLOR_BG lv_color_hex(0x000000)
#define COLOR_TITLE lv_color_hex(0xFFFFFF)
#define COLOR_GOLD lv_color_hex(0xD4AF37)
#define COLOR_SILVER lv_color_hex(0xD9D9D9)
#define COLOR_PLATIN lv_color_hex(0xE5E4E2)     // platin: leicht warmes hellgrau
#define COLOR_PALLADIUM lv_color_hex(0xC0C0C0)  // palladium: etwas dunkler
#define COLOR_FOOTER lv_color_hex(0xAAAAAA)

/* ------------------------------------------------------------
Sprache (DE/US)
------------------------------------------------------------ */
enum Lang { LANG_DE,
            LANG_US };
static Lang g_lang = LANG_DE;

/* ------------------------------------------------------------
LVGL Objekt-Handles (werden später im Code gesetzt)
Damit apply_language() jederzeit Labels aktualisieren kann.
------------------------------------------------------------ */
static lv_obj_t *lbl_title;
static lv_obj_t *lbl_gold;
static lv_obj_t *lbl_silver;
static lv_obj_t *lbl_platinum;
static lv_obj_t *lbl_palladium;
static lv_obj_t *lbl_stamp;
static lv_obj_t *btn_lang;
static lv_obj_t *lbl_btn;
static lv_obj_t *btn_update;
static lv_obj_t *lbl_btn_update;

/* ------------------------------------------------------------
Forward Declarations (Prototypen)
------------------------------------------------------------ */
static void on_lang_btn(lv_event_t *e);
static void on_update_btn(lv_event_t *e);
static void apply_language();
static lv_obj_t *createLabel(lv_obj_t *parent, const char *text, lv_color_t color, int32_t posX, int32_t posY);

static lv_obj_t *create_gold_bar_icon(lv_obj_t *parent);
static lv_obj_t *create_silver_coin_icon(lv_obj_t *parent);
static lv_obj_t *create_platinum_bar_icon(lv_obj_t *parent);
static lv_obj_t *create_palladium_coin_icon(lv_obj_t *parent);

/* ------------------------------------------------------------
Touch-Kalibrierwerte
Diese Werte stammen aus einem Beispiel und können je nach Panel
leicht abweichen. Damit map() Rohwerte -> Pixelwerte umrechnet.
------------------------------------------------------------ */
#define TS_X_MIN 200
#define TS_X_MAX 3700
#define TS_Y_MIN 240
#define TS_Y_MAX 3800

/* ------------------------------------------------------------
API / Timing / NTP
------------------------------------------------------------ */
String metalApiUrl = "https://api.edelmetalle.de/public.json";

// Timer für regelmäßige Updates (900000 ms = 15 Minuten)
unsigned long lastTime = 0;
unsigned long timerDelay = 900000;

// NTP
const char *ntpServer = "pool.ntp.org";
// Hinweis: gmtOffset_sec=60 wirkt ungewöhnlich (entspricht 1 Minute).
// Für Deutschland wäre im Winter 3600 (UTC+1) und im Sommer 7200 (UTC+2) üblich.
// Da du zusätzlich daylightOffset_sec=3600 setzt, funktioniert es evtl. dennoch.
// Wenn du absolute Korrektheit willst, stelle gmtOffset_sec=3600 und daylightOffset_sec=3600.
const long gmtOffset_sec = 60;
const int daylightOffset_sec = 3600;

// Zeitstrings für Footer (DE/US Format)
char deTimeStr[25];
char usTimeStr[25];

/* ------------------------------------------------------------
Daten-Container (werden aus JSON befüllt)
------------------------------------------------------------ */
double gold_usd = 0.0d;
double silber_usd = 0.0d;
double platin_usd = 0.0d;
double palladium_usd = 0.0d;
double gold_eur = 0.0d;
double silber_eur = 0.0d;
double platin_eur = 0.0d;
double palladium_eur = 0.0d;

/* ------------------------------------------------------------
Touch-Callback für LVGL
Wird bei jedem LVGL Input Poll aufgerufen.
------------------------------------------------------------ */
static void my_touch_read(lv_indev_t *indev, lv_indev_data_t *data) {
  // Prüfen, ob Touch-IRQ aktiv und Touch gültig
  if (ts.tirqTouched() && ts.touched()) {
    TS_Point p = ts.getPoint();  // Rohwerte vom Touchcontroller


    // Rohwerte -> Pixel (Mapping anhand Kalibrierwerte)
    int px = map(p.x, TS_X_MIN, TS_X_MAX, 1, TFT_HOR_RES);
    int py = map(p.y, TS_Y_MIN, TS_Y_MAX, 1, TFT_VER_RES);

    // Sicherheit: innerhalb Bildschirm bleiben
    if (px < 0) px = 0;
    if (px > TFT_HOR_RES - 1) px = TFT_HOR_RES - 1;
    if (py < 0) py = 0;
    if (py > TFT_VER_RES - 1) py = TFT_VER_RES - 1;

    // LVGL mitteilen: Touch pressed + Koordinate
    data->state = LV_INDEV_STATE_PRESSED;
    data->point.x = px;
    data->point.y = py;


  } else {
    // Nicht berührt
    data->state = LV_INDEV_STATE_RELEASED;
  }
}

/* ------------------------------------------------------------
Forward: Daten-Funktionen
------------------------------------------------------------ */
void refreshData();
void handleJsonPayload(String json);
void setTimestamps();

/* ------------------------------------------------------------
Setup: Hardware + GUI initialisieren
------------------------------------------------------------ */
void setup() {
  Serial.begin(9600);

  // ---------- WiFi verbinden ----------
  Serial.println("\nVerbinde mit WiFi...");
  WiFi.begin(ssid, password);

  // Blockierend warten, bis verbunden
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nVerbunden!");
  Serial.print("IP Adresse: ");
  Serial.println(WiFi.localIP());

  // ---------- NTP Zeit initialisieren ----------
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);

  // ---------- LVGL initialisieren ----------
  lv_init();

  // ---------- Touch SPI + Touch init ----------
  touchSPI.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS);
  ts.begin(touchSPI);
  ts.setRotation(2);

  // ---------- Display init (TFT_eSPI über LVGL Helper) ----------
  lv_display_t *disp = lv_tft_espi_create(TFT_HOR_RES, TFT_VER_RES, draw_buf, sizeof(draw_buf));
  lv_display_set_rotation(disp, TFT_ROTATION);

  // ---------- Input Device (Touch) an LVGL binden ----------
  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);

  // ---------- Screen Style ----------
  lv_obj_t *scr = lv_screen_active();
  lv_obj_set_style_bg_color(scr, COLOR_BG, 0);
  lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
  lv_obj_set_style_pad_all(scr, 12, 0);
  lv_obj_set_scrollbar_mode(scr, LV_SCROLLBAR_MODE_OFF);

  // ---------- Titel ----------
  lbl_title = lv_label_create(scr);
  lv_label_set_text(lbl_title, "Edelmetall Dashboard");
  lv_obj_set_style_text_color(lbl_title, COLOR_TITLE, 0);
  lv_obj_set_style_text_font(lbl_title, &lv_font_montserrat_20, 0);
  lv_obj_align(lbl_title, LV_ALIGN_TOP_MID, 0, 0);

  // ---------- Footer Container (unten) ----------
  lv_obj_t *footer = lv_obj_create(scr);
  lv_obj_remove_style_all(footer);
  lv_obj_set_height(footer, LV_SIZE_CONTENT);
  lv_obj_set_style_pad_all(footer, 0, 0);
  lv_obj_set_width(footer, LV_PCT(100));
  lv_obj_set_style_pad_row(footer, 4, 0);
  lv_obj_set_flex_flow(footer, LV_FLEX_FLOW_COLUMN);
  lv_obj_set_flex_align(footer, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
  lv_obj_align(footer, LV_ALIGN_BOTTOM_MID, 0, 0);

  // Zeitstempel-Label (wird später dynamisch gesetzt)
  lbl_stamp = lv_label_create(footer);
  lv_label_set_text(lbl_stamp, "Stand: 16.01.2026 - 16:46 Uhr");
  lv_obj_set_style_text_color(lbl_stamp, COLOR_FOOTER, 0);
  lv_obj_set_style_text_font(lbl_stamp, &lv_font_montserrat_12, 0);

  // Credit-Label
  lv_obj_t *credit = lv_label_create(footer);
  lv_label_set_text(credit, "Stefan Draeger - https://draeger-it.blog");
  lv_obj_set_style_text_color(credit, COLOR_FOOTER, 0);
  lv_obj_set_style_text_font(credit, &lv_font_montserrat_12, 0);

  // ---------- Toolbar im Footer (Buttons) ----------
  lv_obj_t *toolbar = lv_obj_create(footer);
  lv_obj_remove_style_all(toolbar);
  lv_obj_set_style_pad_column(toolbar, 10, 0);  // Abstand zwischen den Buttons (Flex Row)
  lv_obj_set_height(toolbar, LV_SIZE_CONTENT);
  lv_obj_set_style_pad_all(toolbar, 0, 0);
  lv_obj_set_width(toolbar, LV_PCT(100));
  lv_obj_set_style_pad_row(toolbar, 4, 0);
  lv_obj_set_flex_flow(toolbar, LV_FLEX_FLOW_ROW);
  lv_obj_set_flex_align(toolbar, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
  lv_obj_align(toolbar, LV_ALIGN_BOTTOM_MID, 0, 0);

  // ---------- Language Button ----------
  btn_lang = lv_btn_create(toolbar);
  lv_obj_set_size(btn_lang, 52, 18);
  lv_obj_set_pos(btn_lang, 0, 0);
  lv_obj_add_event_cb(btn_lang, on_lang_btn, LV_EVENT_PRESSED, NULL);
  lv_obj_set_style_bg_color(btn_lang, lv_color_hex(0x202020), 0);
  lv_obj_set_style_border_width(btn_lang, 1, 0);
  lv_obj_set_style_border_color(btn_lang, lv_color_hex(0x444444), 0);

  lbl_btn = lv_label_create(btn_lang);
  lv_obj_center(lbl_btn);  // Label zentrieren
  lv_obj_set_style_text_font(lbl_btn, &lv_font_montserrat_10, 0);
  lv_obj_set_style_text_color(lbl_btn, lv_color_hex(0xFFFFFF), 0);

  // ---------- Update Button ----------
  btn_update = lv_btn_create(toolbar);
  lv_obj_set_size(btn_update, 105, 18);
  lv_obj_set_pos(btn_update, 0, 0);
  lv_obj_add_event_cb(btn_update, on_update_btn, LV_EVENT_PRESSED, NULL);
  lv_obj_set_style_bg_color(btn_update, lv_color_hex(0x202020), 0);
  lv_obj_set_style_border_width(btn_update, 1, 0);
  lv_obj_set_style_border_color(btn_update, lv_color_hex(0x444444), 0);

  lbl_btn_update = lv_label_create(btn_update);
  lv_obj_center(lbl_btn_update);  // Label zentrieren
  lv_obj_set_style_text_font(lbl_btn_update, &lv_font_montserrat_10, 0);
  lv_obj_set_style_text_color(lbl_btn_update, lv_color_hex(0xFFFFFF), 0);

  // Layout updaten, damit Größen/Positionen sauber berechnet sind
  lv_obj_update_layout(btn_lang);
  lv_obj_update_layout(btn_update);
  lv_obj_update_layout(lbl_title);
  lv_obj_update_layout(footer);

  // ---------- Content Bereich (zwischen Title und Footer) ----------
  int y_content = lv_obj_get_y(lbl_title) + lv_obj_get_height(lbl_title) + 12;
  int h_content = lv_obj_get_y(footer) - y_content - 2;

  lv_obj_t *content = lv_obj_create(scr);
  lv_obj_remove_style_all(content);
  lv_obj_set_pos(content, 15, y_content + 8);
  lv_obj_set_size(content, lv_obj_get_width(scr), h_content);
  lv_obj_set_scroll_dir(content, LV_DIR_NONE);
  lv_obj_set_scrollbar_mode(content, LV_SCROLLBAR_MODE_OFF);

  // ---------- Zeilen: Icons + Labels ----------
  lv_obj_t *gold_icon = create_gold_bar_icon(content);
  lv_obj_set_pos(gold_icon, 0, 2);
  lbl_gold = createLabel(content, "Goldpreis: 3976,16 EUR", COLOR_GOLD, 24, 0);

  lv_obj_t *silver_icon = create_silver_coin_icon(content);
  lv_obj_set_pos(silver_icon, 2, 30);
  lbl_silver = createLabel(content, "Silberpreis: 79,5325 EUR", COLOR_SILVER, 24, 28);

  lv_obj_t *platinum_icon = create_platinum_bar_icon(content);
  lv_obj_set_pos(platinum_icon, 0, 58);
  lbl_platinum = createLabel(content, "Platinpreis: 0,00 EUR", COLOR_PLATIN, 24, 56);

  lv_obj_t *palladium_icon = create_palladium_coin_icon(content);
  lv_obj_set_pos(palladium_icon, 2, 86);
  lbl_palladium = createLabel(content, "Palladiumpreis: 0,00 EUR", COLOR_PALLADIUM, 24, 84);

  // ---------- Initialer Datenabruf + UI refresh ----------
  refreshData();
  apply_language();
}

/* ------------------------------------------------------------
Helper: Label erstellen (Text, Farbe, Font, Position)
------------------------------------------------------------ */
static lv_obj_t *createLabel(lv_obj_t *parent, const char *text, lv_color_t color, int32_t posX, int32_t posY) {
  lv_obj_t *label = lv_label_create(parent);
  lv_label_set_text(label, text);
  lv_obj_set_style_text_color(label, color, 0);
  lv_obj_set_style_text_font(label, &lv_font_montserrat_18, 0);
  lv_obj_set_pos(label, posX, posY);
  return label;
}

/* ------------------------------------------------------------
Loop: LVGL + regelmäßige Updates
------------------------------------------------------------ */
void loop() {
  // LVGL "arbeiten lassen"
  lv_timer_handler();

  // Wichtig: LVGL Zeit fortschreiben (Tick)
  // Du verwendest hier ein fixes 5ms Raster.
  lv_tick_inc(5);
  delay(5);

  // Periodischer API Refresh
  if ((millis() - lastTime) > timerDelay) {
    refreshData();
  }
}

/* ------------------------------------------------------------
refreshData(): Zeitstempel setzen + HTTP Request + JSON parsen
Danach UI aktualisieren (apply_language)
------------------------------------------------------------ */
void refreshData() {
  if (WiFi.status() == WL_CONNECTED) {
    setTimestamps();


    HTTPClient http;
    http.begin(metalApiUrl.c_str());
    int httpResponseCode = http.GET();

    if (httpResponseCode == 200) {
      String payload = http.getString();
      handleJsonPayload(payload);
    } else if (httpResponseCode == 304) {
      Serial.println("Keine neuen Daten!");
    } else {
      Serial.print("HTTP Error: ");
      Serial.println(httpResponseCode);
    }
    http.end();

  } else {
    Serial.println("WiFi nicht verbunden – kein Refresh möglich.");
  }

  lastTime = millis();
  apply_language();  // UI immer neu setzen (DE/US)
}

/* ------------------------------------------------------------
Zeitstempel (NTP) in DE/US Format erzeugen
------------------------------------------------------------ */
void setTimestamps() {
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) {
    Serial.println("Failed to obtain time");
    return;
  }

  // DE: 16.01.2026 - 16:46
  strftime(deTimeStr, sizeof(deTimeStr), "%d.%m.%Y - %H:%M", &timeinfo);

  // US: 2026-01-16 - 16:46
  strftime(usTimeStr, sizeof(usTimeStr), "%Y-%m-%d - %H:%M", &timeinfo);
}

/* ------------------------------------------------------------
JSON Payload verarbeiten
Achtung: StaticJsonDocument Größe ggf. erhöhen, falls API wächst.
------------------------------------------------------------ */
void handleJsonPayload(String json) {
  StaticJsonDocument<400> doc;
  DeserializationError error = deserializeJson(doc, json);

  if (error) {
    Serial.print(F("deserializeJson() failed: "));
    Serial.println(error.f_str());
    return;
  }

  // USD Werte
  gold_usd = doc["gold_usd"];
  silber_usd = doc["silber_usd"];
  platin_usd = doc["platin_usd"];
  palladium_usd = doc["palladium_usd"];

  // EUR Werte
  gold_eur = doc["gold_eur"];
  silber_eur = doc["silber_eur"];
  platin_eur = doc["platin_eur"];
  palladium_eur = doc["palladium_eur"];
}

/* ------------------------------------------------------------
Sprache anwenden: Labels dynamisch setzen (DE/US)
Wichtig: nutzt die aktuell gespeicherten Variablen (EUR/USD)
------------------------------------------------------------ */
static void apply_language() {
  if (g_lang == LANG_DE) {
    lv_label_set_text(lbl_title, "Edelmetall Dashboard");

    char goldEurlabelText[64];
    snprintf(goldEurlabelText, sizeof(goldEurlabelText), "Goldpreis: %.2f EUR", gold_eur);
    lv_label_set_text(lbl_gold, goldEurlabelText);

    char silberEurlabelText[64];
    snprintf(silberEurlabelText, sizeof(silberEurlabelText), "Silberpreis: %.2f EUR", silber_eur);
    lv_label_set_text(lbl_silver, silberEurlabelText);

    char platinumEurlabelText[64];
    snprintf(platinumEurlabelText, sizeof(platinumEurlabelText), "Platinpreis: %.2f EUR", platin_eur);
    lv_label_set_text(lbl_platinum, platinumEurlabelText);

    char palladiumEurlabelText[64];
    snprintf(palladiumEurlabelText, sizeof(palladiumEurlabelText), "Palladiumpreis: %.2f EUR", palladium_eur);
    lv_label_set_text(lbl_palladium, palladiumEurlabelText);

    char labelText[40];
    snprintf(labelText, sizeof(labelText), "Stand: %s", deTimeStr);
    lv_label_set_text(lbl_stamp, labelText);

    lv_label_set_text(lbl_btn, "DE");
    lv_label_set_text(lbl_btn_update, "Aktualisieren");

  } else {
    lv_label_set_text(lbl_title, "Precious Metals Dashboard");

    char goldUsdlabelText[64];
    snprintf(goldUsdlabelText, sizeof(goldUsdlabelText), "Gold price: %.2f USD", gold_usd);
    lv_label_set_text(lbl_gold, goldUsdlabelText);

    char silberUsdlabelText[64];
    snprintf(silberUsdlabelText, sizeof(silberUsdlabelText), "Silver price: %.2f USD", silber_usd);
    lv_label_set_text(lbl_silver, silberUsdlabelText);

    char platinumUsdlabelText[64];
    snprintf(platinumUsdlabelText, sizeof(platinumUsdlabelText), "Platinum price: %.2f USD", platin_usd);
    lv_label_set_text(lbl_platinum, platinumUsdlabelText);

    char palladiumUsdlabelText[64];
    snprintf(palladiumUsdlabelText, sizeof(palladiumUsdlabelText), "Palladium price: %.2f USD", palladium_usd);
    lv_label_set_text(lbl_palladium, palladiumUsdlabelText);

    char labelText[40];
    snprintf(labelText, sizeof(labelText), "Updated: %s", usTimeStr);
    lv_label_set_text(lbl_stamp, labelText);

    lv_label_set_text(lbl_btn, "US");
    lv_label_set_text(lbl_btn_update, "Update");
  }
}

/* ------------------------------------------------------------
Event: Sprachbutton
------------------------------------------------------------ */
static void on_lang_btn(lv_event_t *e) {
  // Sprache toggeln
  if (g_lang == LANG_DE) g_lang = LANG_US;
  else g_lang = LANG_DE;

  apply_language();
}

/* ------------------------------------------------------------
Event: Update Button (manueller Refresh)
------------------------------------------------------------ */
static void on_update_btn(lv_event_t *e) {
  refreshData();
}

/* ------------------------------------------------------------
Icons: Gold / Silber / Platin / Palladium (simple shapes)
------------------------------------------------------------ */
static lv_obj_t *create_gold_bar_icon(lv_obj_t *parent) {
  lv_obj_t *bar = lv_obj_create(parent);
  lv_obj_remove_style_all(bar);
  lv_obj_set_size(bar, 16, 12);

  lv_obj_set_style_bg_color(bar, lv_color_hex(0xD4AF37), 0);
  lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, 0);
  lv_obj_set_style_radius(bar, 3, 0);

  lv_obj_set_style_border_width(bar, 1, 0);
  lv_obj_set_style_border_color(bar, lv_color_hex(0xB38B2E), 0);

  lv_obj_t *shine = lv_obj_create(bar);
  lv_obj_remove_style_all(shine);
  lv_obj_set_size(shine, 10, 3);
  lv_obj_align(shine, LV_ALIGN_TOP_LEFT, 2, 2);
  lv_obj_set_style_bg_color(shine, lv_color_hex(0xFFE08A), 0);
  lv_obj_set_style_bg_opa(shine, LV_OPA_COVER, 0);
  lv_obj_set_style_radius(shine, 2, 0);

  lv_obj_t *shadow = lv_obj_create(bar);
  lv_obj_remove_style_all(shadow);
  lv_obj_set_size(shadow, 12, 3);
  lv_obj_align(shadow, LV_ALIGN_BOTTOM_RIGHT, -2, -2);
  lv_obj_set_style_bg_color(shadow, lv_color_hex(0xA77A1E), 0);
  lv_obj_set_style_bg_opa(shadow, LV_OPA_COVER, 0);
  lv_obj_set_style_radius(shadow, 2, 0);

  return bar;
}

static lv_obj_t *create_silver_coin_icon(lv_obj_t *parent) {
  lv_obj_t *coin = lv_obj_create(parent);
  lv_obj_remove_style_all(coin);
  lv_obj_set_size(coin, 12, 12);

  lv_obj_set_style_bg_color(coin, lv_color_hex(0xD9D9D9), 0);
  lv_obj_set_style_bg_opa(coin, LV_OPA_COVER, 0);
  lv_obj_set_style_radius(coin, LV_RADIUS_CIRCLE, 0);

  lv_obj_set_style_border_width(coin, 1, 0);
  lv_obj_set_style_border_color(coin, lv_color_hex(0xA9A9A9), 0);

  lv_obj_t *shine = lv_obj_create(coin);
  lv_obj_remove_style_all(shine);
  lv_obj_set_size(shine, 6, 3);
  lv_obj_align(shine, LV_ALIGN_TOP_LEFT, 2, 2);
  lv_obj_set_style_bg_color(shine, lv_color_hex(0xFFFFFF), 0);
  lv_obj_set_style_bg_opa(shine, LV_OPA_60, 0);
  lv_obj_set_style_radius(shine, LV_RADIUS_CIRCLE, 0);

  lv_obj_t *shadow = lv_obj_create(coin);
  lv_obj_remove_style_all(shadow);
  lv_obj_set_size(shadow, 7, 3);
  lv_obj_align(shadow, LV_ALIGN_BOTTOM_RIGHT, -2, -2);
  lv_obj_set_style_bg_color(shadow, lv_color_hex(0x8C8C8C), 0);
  lv_obj_set_style_bg_opa(shadow, LV_OPA_50, 0);
  lv_obj_set_style_radius(shadow, LV_RADIUS_CIRCLE, 0);

  lv_obj_t *ring = lv_obj_create(coin);
  lv_obj_remove_style_all(ring);
  lv_obj_set_size(ring, 8, 8);
  lv_obj_center(ring);
  lv_obj_set_style_radius(ring, LV_RADIUS_CIRCLE, 0);
  lv_obj_set_style_border_width(ring, 1, 0);
  lv_obj_set_style_border_color(ring, lv_color_hex(0xBEBEBE), 0);
  lv_obj_set_style_bg_opa(ring, LV_OPA_TRANSP, 0);

  return coin;
}

static lv_obj_t *create_platinum_bar_icon(lv_obj_t *parent) {
  lv_obj_t *bar = lv_obj_create(parent);
  lv_obj_remove_style_all(bar);
  lv_obj_set_size(bar, 16, 12);

  lv_obj_set_style_bg_color(bar, lv_color_hex(0xE5E4E2), 0);
  lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, 0);
  lv_obj_set_style_radius(bar, 3, 0);

  lv_obj_set_style_border_width(bar, 1, 0);
  lv_obj_set_style_border_color(bar, lv_color_hex(0xAFAEAD), 0);

  lv_obj_t *shine = lv_obj_create(bar);
  lv_obj_remove_style_all(shine);
  lv_obj_set_size(shine, 10, 3);
  lv_obj_align(shine, LV_ALIGN_TOP_LEFT, 2, 2);
  lv_obj_set_style_bg_color(shine, lv_color_hex(0xFFFFFF), 0);
  lv_obj_set_style_bg_opa(shine, LV_OPA_70, 0);
  lv_obj_set_style_radius(shine, 2, 0);

  lv_obj_t *shadow = lv_obj_create(bar);
  lv_obj_remove_style_all(shadow);
  lv_obj_set_size(shadow, 12, 3);
  lv_obj_align(shadow, LV_ALIGN_BOTTOM_RIGHT, -2, -2);
  lv_obj_set_style_bg_color(shadow, lv_color_hex(0x8F8E8D), 0);
  lv_obj_set_style_bg_opa(shadow, LV_OPA_60, 0);
  lv_obj_set_style_radius(shadow, 2, 0);

  return bar;
}

static lv_obj_t *create_palladium_coin_icon(lv_obj_t *parent) {
  lv_obj_t *coin = lv_obj_create(parent);
  lv_obj_remove_style_all(coin);
  lv_obj_set_size(coin, 12, 12);

  lv_obj_set_style_bg_color(coin, lv_color_hex(0xC0C0C0), 0);
  lv_obj_set_style_bg_opa(coin, LV_OPA_COVER, 0);
  lv_obj_set_style_radius(coin, LV_RADIUS_CIRCLE, 0);

  lv_obj_set_style_border_width(coin, 1, 0);
  lv_obj_set_style_border_color(coin, lv_color_hex(0x8A8A8A), 0);

  lv_obj_t *shine = lv_obj_create(coin);
  lv_obj_remove_style_all(shine);
  lv_obj_set_size(shine, 6, 3);
  lv_obj_align(shine, LV_ALIGN_TOP_LEFT, 2, 2);
  lv_obj_set_style_bg_color(shine, lv_color_hex(0xFFFFFF), 0);
  lv_obj_set_style_bg_opa(shine, LV_OPA_50, 0);
  lv_obj_set_style_radius(shine, LV_RADIUS_CIRCLE, 0);

  lv_obj_t *ring = lv_obj_create(coin);
  lv_obj_remove_style_all(ring);
  lv_obj_set_size(ring, 8, 8);
  lv_obj_center(ring);
  lv_obj_set_style_radius(ring, LV_RADIUS_CIRCLE, 0);
  lv_obj_set_style_border_width(ring, 1, 0);
  lv_obj_set_style_border_color(ring, lv_color_hex(0xB0B0B0), 0);
  lv_obj_set_style_bg_opa(ring, LV_OPA_TRANSP, 0);

  return coin;
}

Problem – Textbereich überschreitet den verfügbaren Platz auf der Platine

Ich möchte neben den Daten der Edelmetalpreise noch den aktuellen Zeitstempel anzeigen. Hier kam ich schnell an die Speichergrenze des ESP32.

Der Sketch verwendet 1440427 Bytes (109%) des Programmspeicherplatzes. Das Maximum sind 1310720 Bytes. Globale Variablen verwenden 64256 Bytes (19%) des dynamischen Speichers, 263424 Bytes für lokale Variablen verbleiben. Das Maximum sind 327680 Bytes. Textbereich überschreitet den verfügbaren Platz auf der Platine Compilation error: Textbereich überschreitet den verfügbaren Platz auf der Platine

Es musste also optimiert werden. In diesem Abschnitt zeige ich dir was ich getan habe um den Code sowie die Bibliothek LVGL zu optimieren.

Schritt 1 – nur benötigte Fonts laden

Im Code verwende ich lediglich 3 Schriften

  • lv_font_montserrat_12
  • lv_font_montserrat_18
  • lv_font_montserrat_20

per Default sind in LVGL jedoch deutlich mehr Schriften verfügbar und werden mit auf den Mikrocontroller geladen. Diese können wir jedoch einfach in der Datei lv_conf.h ausschalten.
Hier müssen wir statt eine 1 hinter der Schriftart eine 0 setzen.

/*Montserrat fonts with ASCII range and some symbols using bpp = 4
 *https://fonts.google.com/specimen/Montserrat*/
#define LV_FONT_MONTSERRAT_8  1
#define LV_FONT_MONTSERRAT_10 1
#define LV_FONT_MONTSERRAT_12 1
#define LV_FONT_MONTSERRAT_14 1
#define LV_FONT_MONTSERRAT_16 1
#define LV_FONT_MONTSERRAT_18 1
#define LV_FONT_MONTSERRAT_20 1
#define LV_FONT_MONTSERRAT_22 1
#define LV_FONT_MONTSERRAT_24 0
#define LV_FONT_MONTSERRAT_26 0
#define LV_FONT_MONTSERRAT_28 0
#define LV_FONT_MONTSERRAT_30 0
#define LV_FONT_MONTSERRAT_32 0
#define LV_FONT_MONTSERRAT_34 0
#define LV_FONT_MONTSERRAT_36 0
#define LV_FONT_MONTSERRAT_38 0
#define LV_FONT_MONTSERRAT_40 0
#define LV_FONT_MONTSERRAT_42 0
#define LV_FONT_MONTSERRAT_44 0
#define LV_FONT_MONTSERRAT_46 0
#define LV_FONT_MONTSERRAT_48 0

Schritt 2 – Setzen des Partion Scheme

Als Standard ist „Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS)“ (in der Grafik grün markiert) ausgewählt, dieses ist jedoch für den Code nicht ausreichend und wir müssen hier auf „Huge APP (2MB No OTA/1MB SPIFFS)“ wechseln.

ESP32 set partition scheme

Ergebniss

Diese beiden Optimierungen haben ergeben das nun statt 109% nunmehr 45% des Programmspeicherplatzes belegt werden.

Der Sketch verwendet 1439431 Bytes (45%) des Programmspeicherplatzes. Das Maximum sind 3145728 Bytes.
Globale Variablen verwenden 64248 Bytes (19%) des dynamischen Speichers, 263432 Bytes für lokale Variablen verbleiben. Das Maximum sind 327680 Bytes.

Fazit

In diesem Beitrag hast du gesehen, wie aus einer einfachen Community-Idee ein vollwertiges kleines Dashboard-Projekt entstehen kann. Auf Basis des ESP32-2432S028 haben wir ein kompaktes System aufgebaut, das aktuelle Edelmetallpreise per WLAN aus einer öffentlichen API lädt, als JSON verarbeitet und übersichtlich auf einem TFT-Display darstellt.

Neben der eigentlichen Anzeige standen vor allem die technischen Grundlagen im Fokus:

  • saubere Einbindung von LVGL für das UI,
  • strukturierter Aufbau von Labels und Buttons,
  • zuverlässiger HTTP-Abruf der Daten,
  • sowie die Zeitstempel-Synchronisation über NTP.

Besonders wichtig war dabei auch der Umgang mit den Grenzen des Systems. Durch das Reduzieren der Fonts und das Anpassen des Partition Schemes konnten wir den Speicherverbrauch deutlich senken und das Projekt stabil lauffähig machen – ein Punkt, der in vielen ESP32-Projekten früher oder später eine Rolle spielt.

Das Ergebnis ist ein bewusst schlichtes, aber technisch sauberes Dashboard, das sich sehr gut als Grundlage für eigene Erweiterungen eignet. Genau das ist auch der eigentliche Mehrwert dieses Projekts: Du kannst den Code als Startpunkt nutzen und Schritt für Schritt an deine eigenen Ideen anpassen – sei es für weitere Metalle, andere Datenquellen oder zusätzliche Funktionen.

Im nächsten Projekt lässt sich dieses Dashboard problemlos weiterentwickeln. Ideen wie Alarme, zusätzliche Sensoren oder eine Konfiguration über externe Schnittstellen bieten hier viele spannende Möglichkeiten für die Zukunft.

Letzte Aktualisierung am: 20. Januar 2026

Foto von Stefan Draeger
Über den Autor

Stefan Draeger — Entwickler & Tech-Blogger

Ich zeige praxisnah, wie du Projekte mit Arduino, ESP32 und Smarthome-Komponenten umsetzt – Schritt für Schritt, mit Code und Schaltplänen.

Mehr Artikel von Stefan →

1 thought on “ESP32-Display als Edelmetall-Monitor: Gold- und Silberpreis in Echtzeit”

  1. Pingback: ESP32 Touch-GUI mit LVGL: RGB-LED per Slider und Buttons steuern - Technik Blog

Schreibe einen Kommentar Antwort abbrechen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Fragen oder Feedback?

Du hast eine Idee, brauchst Hilfe oder möchtest Feedback loswerden?
Support-Ticket erstellen

Newsletter abonnieren

Bleib auf dem Laufenden: Erhalte regelmäßig Updates zu neuen Projekten, Tutorials und Tipps rund um Arduino, ESP32 und mehr – direkt in dein Postfach.

Jetzt Newsletter abonnieren

Unterstütze meinen Blog

Wenn dir meine Inhalte gefallen, freue ich mich über deine Unterstützung auf Tipeee.
So hilfst du mit, den Blog am Leben zu halten und neue Beiträge zu ermöglichen.

draeger-it.blog auf Tipeee unterstützen

Vielen Dank für deinen Support!
– Stefan Draeger

Kategorien

Tools

  • QR-Code Generator
  • Unix-Zeitstempel-Rechner
  • ASCII Tabelle
  • Spannung, Strom, Widerstand und Leistung berechnen
  • Widerstandsrechner
  • 8×8 LED Matrix Tool
  • 8×16 LED Matrix Modul von Keyestudio
  • 16×16 LED Matrix – Generator

Links

Blogverzeichnis Bloggerei.de TopBlogs.de das Original - Blogverzeichnis | Blog Top Liste Blogverzeichnis trusted-blogs.com

Stefan Draeger
Königsberger Str. 13
38364 Schöningen
Tel.: 015565432686
E-Mail: info@draeger-it.blog

Folge mir auf

link zu Fabook
link zu LinkedIn
link zu YouTube
link zu TikTok
link zu Pinterest
link zu Instagram
  • Impressum
  • Datenschutzerklärung
  • Disclaimer
  • Cookie-Richtlinie (EU)
©2026 Technik Blog | Built using WordPress and Responsive Blogily theme by Superb
Cookie-Zustimmung verwalten
Wir verwenden Technologien wie Cookies, um Geräteinformationen zu speichern und/oder darauf zuzugreifen. Wir tun dies, um das Surferlebnis zu verbessern und um personalisierte Werbung anzuzeigen. Wenn Sie diesen Technologien zustimmen, können wir Daten wie das Surfverhalten oder eindeutige IDs auf dieser Website verarbeiten. Wenn Sie Ihre Zustimmung nicht erteilen oder zurückziehen, können bestimmte Funktionen beeinträchtigt werden.
Funktional Immer aktiv
Die technische Speicherung oder der Zugang ist unbedingt erforderlich für den rechtmäßigen Zweck, die Nutzung eines bestimmten Dienstes zu ermöglichen, der vom Teilnehmer oder Nutzer ausdrücklich gewünscht wird, oder für den alleinigen Zweck, die Übertragung einer Nachricht über ein elektronisches Kommunikationsnetz durchzuführen.
Vorlieben
Die technische Speicherung oder der Zugriff ist für den rechtmäßigen Zweck der Speicherung von Präferenzen erforderlich, die nicht vom Abonnenten oder Benutzer angefordert wurden.
Statistiken
Die technische Speicherung oder der Zugriff, der ausschließlich zu statistischen Zwecken erfolgt. Die technische Speicherung oder der Zugriff, der ausschließlich zu anonymen statistischen Zwecken verwendet wird. Ohne eine Vorladung, die freiwillige Zustimmung deines Internetdienstanbieters oder zusätzliche Aufzeichnungen von Dritten können die zu diesem Zweck gespeicherten oder abgerufenen Informationen allein in der Regel nicht dazu verwendet werden, dich zu identifizieren.
Marketing
Die technische Speicherung oder der Zugriff ist erforderlich, um Nutzerprofile zu erstellen, um Werbung zu versenden oder um den Nutzer auf einer Website oder über mehrere Websites hinweg zu ähnlichen Marketingzwecken zu verfolgen.
  • Optionen verwalten
  • Dienste verwalten
  • Verwalten von {vendor_count}-Lieferanten
  • Lese mehr über diese Zwecke
Einstellungen anzeigen
  • {title}
  • {title}
  • {title}