Zum Hauptinhalt springen Zur Suche springen Zur Hauptnavigation springen
Menü

ESP32 Börsen-Ticker mit 2,4" TFT-Display


Dieses Projekt verwandelt ein ESP32-Entwicklungsboard und ein 2,4" TFT-Display in einen Börsen-Ticker für den Schreibtisch. Das Gerät holt sich Aktienkurse über WLAN aus dem Internet und zeigt sie fortlaufend an: Firmenname, aktueller Kurs, die Veränderung zum Vortag und darunter der Verlauf des Handelstages als Kurve.

Live-Kurse von Apple, Nvidia & Co. auf dem Schreibtisch – inklusive Chart. Mit einem ESP32, einem ILI9341-Display und zwei kostenlosen APIs lässt sich ein kompakter Ticker realisieren. Ein Blick auf den Bildschirm genügt, um die eigene Watchlist im Auge zu behalten: Firmenname, aktueller Kurs, Tagesveränderung und darunter der Verlauf des heutigen Handelstages als Kurve. Ganz ohne Browser-Tab, App oder kostenpflichtiges Abonnement. Der Aufbau kostet inklusive Kabel unter 20 € und ist an einem Nachmittag fertiggestellt.

Benötigte Komponente

  • ESP32 NodeMcu Entwicklungsboard
  • 2,4" TFT LCD Display Modul 
  • Jumper-Kabel Buchse-Buchse
  • Micro-USB-Kabel
  • 1x Kabelverbinder
  • Optional: 3D-gedrucktes Gehäuse


Software & Zugänge

  • Arduino IDE 2.x
  • ESP32-Boardpaket für die Arduino IDE
  • Bibliotheken: Adafruit GFX, Adafruit ILI9341, ArduinoJson
  • Kostenloser Account bei finnhub.io
  • Kostenloser Account bei twelvedata.com 
  • 2,4-GHz-WLAN-Netzwerk

    Downloads und mehr

Produktgalerie überspringen
Universal Kabelverbinder 1-polig PCT-1-1

Durchschnittliche Bewertung von 0 von 5 Sternen

Universal Kabelverbinder 1-polig PCT-1-1
RBS17322

Sofort verfügbar

Regulärer Preis: Ab 0,69 €
40 Pin Dupont / Jumper Kabel Buchse-Buchse 10 cm

Durchschnittliche Bewertung von 5 von 5 Sternen

(11)
40 Pin Jumper Kabel Buchse-Buchse 10 cm
RBS10525

Sofort verfügbar

Regulärer Preis: Ab 1,05 €
2,4" TFT LCD Display Modul ILI9341 240x320 2,4" TFT LCD Display Modul ILI9341 240x320

Durchschnittliche Bewertung von 4.88 von 5 Sternen

(8)
2,4" TFT LCD Display Modul ILI9341 240x320
RBS13179

Sofort verfügbar

Regulärer Preis: Ab 12,64 €
ESP32 NodeMcu Entwicklungsboard ESP-WROOM-32 ESP32 NodeMcu Entwicklungsboard ESP-WROOM-32

Durchschnittliche Bewertung von 5 von 5 Sternen

(3)
ESP32 NodeMcu Entwicklungsboard ESP-WROOM-32
RBS12259

Sofort verfügbar

Regulärer Preis: Ab 6,27 €

Welche Börsen-API ist die richtige?

Der Ticker benötigt zwei verschiedene Datenquellen: einen aktuellen Kurs, der möglichst oft aktualisiert wird, und eine Reihe historischer Werte für die Verlaufskurve.

Unsere Wahl fällt auf die Kombination aus Finnhub und Twelve Data:

  • Finnhub liefert den Live-Kurs. 60 Anfragen pro Minute reichen für mehrere Symbole im Minutentakt problemlos aus.

  • Twelve Data liefert die Kurshistorie. Mit 800 Anfragen pro Tag lässt sich bei einer Aktualisierung alle vier Minuten reihum der Intraday-Verlauf stabil anzeigen.


Funktionsweise des Tickers

  1. Der ESP32 verbindet sich mit dem WLAN.
  2. Jede Minute fragt er bei Finnhub die aktuellen Kurse aller hinterlegten Symbole ab.
  3. Alle vier Minuten holt er bei Twelve Data den Intraday-Verlauf eines Symbols – reihum, um das Tageslimit einzuhalten.

  4. Die Daten werden auf dem per SPI angeschlossenen TFT-Display dargestellt. Alle acht Sekunden wechselt die Anzeige zum nächsten Wert.

zum Vergrößern auf Bilder klicken

Schritt 1 | API-Keys holen

Finnhub (Live-Kurse)

  1. Auf finnhub.io gehen, Get free API key klicken und registrieren. Keine Zahlungsdaten nötig.
  2. Der Key steht danach im Dashboard (oben rechts) und sieht ungefähr so aus: cq7f2h9r01qk3v8b1p2g.

Direkt im Browser testen (DEIN_KEY ersetzen)

https://finnhub.io/api/v1/quote?symbol=AAPL&token=DEIN_KEY 
Die Antwort: {"c":214.29,"d":1.87,"dp":0.8801,"h":215.15,"l":211.83,"o":212.41,"pc":212.42,"t":1721764801}


Feld

Bedeutung
ccurrent – aktueller Kurs
ddelta – Veränderung zum Vortagesschluss, absolut
dpdelta percent – Veränderung in Prozent
h / lTageshoch / Tagestief
oEröffnungskurs
pc previous close – Schlusskurs des Vortags
tZeitstempel (Unix)


Twelve Data (Kursverlauf)

  1.  Auf twelvedata.com registrieren und den Basic-Plan wählen – kostenlos, ohne Zahlungsdaten.
  2. Der API-Key steht im Dashboard.

Auch diesen im Browser testen:

https://api.twelvedata.com/time_series?symbol=AAPL&interval=15min&outputsize=26&order=ASC&apikey=DEIN_KEY

Zurück kommt:

(Aktienkurs) anzeigen / einklappen
{
  "meta": {
    "symbol": "AAPL",
    "interval": "15min"
  },
  "values": [
    {
      "datetime": "2026-07-24 09:30:00",
      "open": "212.41",
      "high": "213.02",
      "low": "212.20",
      "close": "212.88",
      "volume": "1840233"
    }
  ],
  "status": "ok"
}

Uns interessiert nur close aus jedem Eintrag – 26 Werte im 15-Minuten-Takt ergeben genau einen US-Handelstag von 6,5 Stunden. order=ASC sorgt dafür, dass der älteste Wert zuerst kommt, dann können wir die Kurve direkt von links nach rechts zeichnen.


Schritt 2 | Arduino IDE vorbereiten
ESP32-Boardpaket installieren (detailierte Anleitung)

  1. Datei → Einstellungen öffnen.
  2. Bei Zusätzliche Boardverwalter-URLs eintragen: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  3. Werkzeuge → Board → Boardverwalter öffnen, nach esp32 suchen, esp32 by Espressif Systems installieren.
  4. Board auswählen: ESP32 Dev Module.


Treiber prüfen

Unser Board nutzt einen CP2102 als USB-Seriell-Wandler. Windows 11, macOS und die meisten Linux-Distributionen erkennen ihn automatisch. Erscheint unter Werkzeuge → Port kein Eintrag, installiere den CP2102-Treiber von Silicon Labs.

Bibliotheken installieren

Werkzeuge → Bibliotheken verwalten, dann suchen und installieren:
Beim Installieren der Adafruit-ILI9341-Bibliothek fragt die IDE, ob Abhängigkeiten mitinstalliert werden sollen – hier „Install all" wählen. WiFi, HTTPClient, SPI und WiFiClientSecure bringt das ESP32-Boardpaket bereits mit.


Schritt 3 | Display verkabeln

Beide Bauteile arbeiten mit 3,3 V – ein Pegelwandler ist nicht nötig. Zieh vor dem Verkabeln immer das USB-Kabel ab. Das Display hat oben eine Stiftleiste mit neun Pins. Die vier Pins an der Unterseite gehören zum microSD-Slot und bleiben in diesem Projekt frei.

Display-Pin ESP32-Pin Funktion
VCC 3V3 Versorgung
GND GND Masse
CS GPIO 15 Chip Select
RESET GPIO 4 Reset
DC (auch RS) GPIO 2 Data / Command
SDI (MOSI) GPIO 23 Daten zum Display
SCK GPIO 18 Takt
LED 3V3 Hintergrundbeleuchtung
SDO (MISO) GPIO 19 Daten vom Display

GPIO 23, 18 und 19 sind die Hardware-SPI-Pins des ESP32 (VSPI). Die Adafruit-Bibliothek nutzt sie automatisch – im Sketch geben wir nur CS, DC und RESET an.


Zwei Pins, eine Quelle

VCC und LED brauchen beide 3,3 V, der ESP32 hat aber nur einen 3V3-Pin. Dafür sind die Stromschienen des Breadboards da – ein Kabel vom 3V3-Pin auf die rote Schiene, von dort zwei Kabel zum Display. Beide Pins sind schlicht parallele Verbraucher an derselben Quelle. GND gibt es am ESP32 zweimal, kann aber genauso über die blaue Schiene laufen. Wenn direkt verkabelt wird, muss ein Y-Kabel hergestellt werden. Alternativ kannst du auch einen Kabelverbinder verwenden.

Check vor dem Einschalten

  • Sitzt VCC wirklich auf 3V3 und nicht auf VIN/5V?
  •  Ist GND verbunden?
  •  Sitzen alle Stecker fest? Wackelkontakte auf dem SPI-Bus erzeugen die kuriosesten Fehlerbilder.


Schritt 4 | Kurztest - Läuft das Display?

Bevor WLAN und APIs dazukommen, prüfen wir isoliert die Anzeige:

Code anzeigen / einklappen
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>

#define TFT_CS 15
#define TFT_DC 2
#define TFT_RST 4

Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);

void setup() {
  tft.begin();
  tft.setRotation(3); // Querformat 320 x 240; bei 1 steht das Bild kopf
  tft.fillScreen(ILI9341_BLACK);
  tft.setTextColor(ILI9341_GREEN);
  tft.setTextSize(3);
  tft.setCursor(20, 100);
  tft.print("Display laeuft!");
}

void loop() {}

Steht der grüne Text im Querformat auf dem Schirm? Dann ist die Hardware in Ordnung. 

Falls nicht, spring kurz zur Fehlerbehebung am Ende.


Schritt 5 | Der Ticker-Sketch

Das Programm komplett in einen neuen Sketch kopieren und oben die markierten Zeilen anpassen: WLAN-Name, WLAN-Passwort, die beiden API-Keys und deine Watchlist.

Code anzeigen / einklappen
/* ============================================================
ESP32 Börsen-Ticker mit Kursverlauf
Display : 2,4" TFT ILI9341 (Adafruit_GFX / Adafruit_ILI9341)
Kurse   : finnhub.io – Live-Kurs, 60 Anfragen/Minute
Verlauf : twelvedata.com – Intraday-Kerzen für die Grafik
Anleitung: Roboter-Bausatz.de
============================================================ */

#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <time.h>
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSans12pt7b.h>
#include <Fonts/FreeSansBold24pt7b.h>

// ---------- Display-Pins ----------
#define TFT_CS 15
#define TFT_DC 2
#define TFT_RST 4

// MOSI 23, SCK 18, MISO 19 = Hardware-SPI, wird automatisch genutzt
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);

// ============ HIER ANPASSEN =================================
const char* WIFI_SSID   = "MEIN_WLAN";
const char* WIFI_PASS   = "MEIN_PASSWORT";
const char* FINNHUB_KEY = "MEIN_FINNHUB_KEY";
const char* TWELVE_KEY  = "MEIN_TWELVEDATA_KEY";

struct Symbol { const char* ticker; const char* name; };

// Deine Watchlist – Zeilen ergänzen oder löschen, der Rest passt sich an
const Symbol watchlist[] = {
  { "AAPL", "Apple" },
  { "MSFT", "Microsoft" },
  { "NVDA", "NVIDIA" },
  { "AMZN", "Amazon" },
  { "TSLA", "Tesla" }
};

// ============================================================
const uint8_t ANZAHL = sizeof(watchlist) / sizeof(watchlist[0]);

// Zeitzone Deutschland inkl. automatischer Sommerzeit
const char* TZ_INFO = "CET-1CEST,M3.5.0/02,M10.5.0/03";
const char* NTP_SERVER = "pool.ntp.org";

const uint32_t KURS_INTERVALL    = 60000;  // 1 min – Live-Kurse (Finnhub)
const uint32_t CHART_INTERVALL   = 240000; // 4 min – ein Chart (Twelve Data)
const uint32_t WECHSEL_INTERVALL = 8000;   // 8 s – nächster Wert der Liste

#define MAX_PUNKTE 26 // Datenpunkte im Chart (26 x 15 min = 1 Handelstag)

// Laufzeitdaten, werden vom Programm gefüllt
struct Daten {
  float kurs;
  float diff;
  float proz;
  bool gueltig;
  float verlauf[MAX_PUNKTE];
  uint8_t punkte;
};

Daten daten[ANZAHL]; // global -> startet automatisch mit Nullen

// ---------- Farben (RGB565) ----------
#define FARBE_BG       0x0862 // fast schwarz
#define FARBE_BAR      0x1105 // Kopfzeile
#define FARBE_TEXT     0xFFFF // weiß
#define FARBE_GRAU     0x7C52 // Sekundärtext
#define FARBE_LINIE    0x39C7 // Hilfslinie im Chart
#define FARBE_GRUEN    0x2610 // im Plus
#define FARBE_ROT      0xE267 // im Minus
#define FARBE_GRUEN_BG 0x09C5 // Füllfläche unter der Kurve
#define FARBE_ROT_BG   0x38A2

// ---------- Layout ----------
#define BREITE  320
#define HOEHE   240
#define KOPF_H  28
#define CHART_X 12
#define CHART_Y 152
#define CHART_W 296
#define CHART_H 60

uint32_t tKurse = 0;
uint32_t tChart = 0;
uint32_t tWechsel = 0;
uint8_t anzeigeIndex = 0; // aktuell angezeigter Wert
uint8_t chartIndex = 0;   // als nächstes zu aktualisierender Chart
int letzteSekunde = -1;

// ===========================================================
void setup() {
  Serial.begin(115200);
  delay(200);

  tft.begin();
  tft.setSPISpeed(40000000); // 40 MHz; bei Bildstörungen auf 24000000 senken
  tft.setRotation(3);        // Querformat 320 x 240 (1 = um 180 Grad gedreht)

  statusText("Verbinde mit WLAN ...");
  wifiVerbinden();

  configTzTime(TZ_INFO, NTP_SERVER);

  statusText("Hole Kurse ...");
  kurseAktualisieren();

  statusText("Lade Charts ...");
  for (uint8_t i = 0; i < ANZAHL; i++) {
    chartAktualisieren(i);
    delay(1000); // Twelve Data erlaubt 8 Anfragen pro Minute
  }

  tft.fillScreen(FARBE_BG);
  kopfzeileZeichnen();
  wertZeichnen(anzeigeIndex);

  tKurse = tChart = tWechsel = millis();
}

// ===========================================================
void loop() {
  uint32_t jetzt = millis();

  // Live-Kurse aller Symbole
  if (jetzt - tKurse >= KURS_INTERVALL) {
    tKurse = jetzt;
    kurseAktualisieren();
    wertZeichnen(anzeigeIndex);
  }

  // Reihum einen Chart nachladen
  if (jetzt - tChart >= CHART_INTERVALL) {
    tChart = jetzt;
    chartAktualisieren(chartIndex);
    if (chartIndex == anzeigeIndex) wertZeichnen(anzeigeIndex);
    chartIndex = (chartIndex + 1) % ANZAHL;
  }

  // Zum nächsten Wert blättern
  if (jetzt - tWechsel >= WECHSEL_INTERVALL) {
    tWechsel = jetzt;
    anzeigeIndex = (anzeigeIndex + 1) % ANZAHL;
    wertZeichnen(anzeigeIndex);
  }

  uhrzeitZeichnen();
  delay(50);
}

// ===========================================================
// WLAN
// ===========================================================
void wifiVerbinden() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  uint8_t versuche = 0;
  while (WiFi.status() != WL_CONNECTED && versuche < 40) {
    delay(500);
    Serial.print(".");
    versuche++;
  }
  Serial.println();
  if (WiFi.status() == WL_CONNECTED) {
    Serial.print("Verbunden. IP: ");
    Serial.println(WiFi.localIP());
  } else {
    Serial.println("WLAN-Verbindung fehlgeschlagen.");
    statusText("Kein WLAN!");
  }
}

// ===========================================================
// Live-Kurse von Finnhub
// ===========================================================
void kurseAktualisieren() {
  if (WiFi.status() != WL_CONNECTED) {
    wifiVerbinden();
    if (WiFi.status() != WL_CONNECTED) return;
  }

  for (uint8_t i = 0; i < ANZAHL; i++) {
    String url = String("https://finnhub.io/api/v1/quote?symbol=") 
               + watchlist[i].ticker + "&token=" + FINNHUB_KEY;

    WiFiClientSecure client;
    client.setInsecure(); // ohne Zertifikatsprüfung – hier ausreichend

    HTTPClient https;
    https.setTimeout(8000);

    if (https.begin(client, url)) {
      int code = https.GET();
      if (code == HTTP_CODE_OK) {
        JsonDocument doc;
        DeserializationError fehler = deserializeJson(doc, https.getString());
        if (!fehler) {
          daten[i].kurs   = doc["c"] | 0.0f;
          daten[i].diff   = doc["d"] | 0.0f;
          daten[i].proz   = doc["dp"] | 0.0f;
          daten[i].gueltig = (daten[i].kurs > 0);
          Serial.printf("%-6s %8.2f (%+.2f%%)\n", watchlist[i].ticker, daten[i].kurs, daten[i].proz);
        } else {
          Serial.printf("JSON-Fehler bei %s: %s\n", watchlist[i].ticker, fehler.c_str());
        }
      } else {
        Serial.printf("Finnhub HTTP %d bei %s\n", code, watchlist[i].ticker);
      }
      https.end();
    }
    delay(250);
  }
}

// ===========================================================
// Kursverlauf von Twelve Data
// ===========================================================
void chartAktualisieren(uint8_t i) {
  if (WiFi.status() != WL_CONNECTED) return;

  String url = String("https://api.twelvedata.com/time_series?symbol=") 
             + watchlist[i].ticker 
             + "&interval=15min&outputsize=" + String(MAX_PUNKTE) 
             + "&order=ASC&apikey=" + TWELVE_KEY;

  WiFiClientSecure client;
  client.setInsecure();

  HTTPClient https;
  https.setTimeout(10000);

  if (!https.begin(client, url)) {
    Serial.println("TwelveData: Verbindung fehlgeschlagen");
    return;
  }

  int code = https.GET();
  if (code != HTTP_CODE_OK) {
    Serial.printf("TwelveData HTTP %d bei %s\n", code, watchlist[i].ticker);
    https.end();
    return;
  }

  // Komplett einlesen statt direkt aus dem Stream zu parsen: Twelve Data
  // antwortet je nach Größe mit "chunked transfer encoding", woran ein
  // direkt lesender Parser scheitern würde.
  String antwort = https.getString();
  https.end();

  // Filter: nur "close" je Eintrag sowie Status und Fehlermeldung behalten
  JsonDocument filter;
  filter["values"][0]["close"] = true;
  filter["status"] = true;
  filter["message"] = true;

  JsonDocument doc;
  DeserializationError fehler = deserializeJson(doc, antwort, DeserializationOption::Filter(filter));

  if (fehler) {
    Serial.printf("Chart-JSON-Fehler bei %s: %s\n", watchlist[i].ticker, fehler.c_str());
    return;
  }

  // Wichtig: Twelve Data meldet Fehler oft mit HTTP 200 im JSON-Body
  const char* status = doc["status"] | "";
  if (strcmp(status, "ok") != 0) {
    Serial.printf("TwelveData meldet Fehler bei %s: %s\n", watchlist[i].ticker, doc["message"] | "unbekannt");
    return;
  }

  uint8_t k = 0;
  for (JsonObject eintrag : doc["values"].as<JsonArray>()) {
    if (k >= MAX_PUNKTE) break;
    daten[i].verlauf[k++] = eintrag["close"].as<float>();
  }
  daten[i].punkte = k;

  Serial.printf("Chart %s: %d Punkte\n", watchlist[i].ticker, k);
}

// ===========================================================
// Anzeige
// ===========================================================
void kopfzeileZeichnen() {
  tft.fillRect(0, 0, BREITE, KOPF_H, FARBE_BAR);
  textAn(12, 19, "MARKT-TICKER", &FreeSans9pt7b, FARBE_TEXT);
}

void wertZeichnen(uint8_t i) {
  char puffer[32];
  tft.fillRect(0, KOPF_H, BREITE, HOEHE - KOPF_H, FARBE_BG);

  // Firmenname und Börsenkürzel
  textAn(12, 52, watchlist[i].name, &FreeSans12pt7b, FARBE_TEXT);
  textAn(12, 74, watchlist[i].ticker, &FreeSans9pt7b, FARBE_GRAU);

  if (!daten[i].gueltig) {
    textAn(12, 118, "keine Daten", &FreeSans12pt7b, FARBE_GRAU);
    punkteZeichnen(i);
    return;
  }

  uint16_t farbe = (daten[i].diff >= 0) ? FARBE_GRUEN : FARBE_ROT;

  // Kurs, groß
  snprintf(puffer, sizeof(puffer), "%.2f", daten[i].kurs);
  textAn(12, 122, puffer, &FreeSansBold24pt7b, FARBE_TEXT);

  // Veränderung, farbig
  snprintf(puffer, sizeof(puffer), "%+.2f (%+.2f%%)", daten[i].diff, daten[i].proz);
  textAn(12, 146, puffer, &FreeSans12pt7b, farbe);

  // Dreieck als Richtungspfeil
  if (daten[i].diff >= 0) 
    tft.fillTriangle(292, 92, 278, 116, 306, 116, farbe);
  else 
    tft.fillTriangle(292, 116, 278, 92, 306, 92, farbe);

  chartZeichnen(i);
  punkteZeichnen(i);
}

// ---------- Der Kursverlauf ----------
void chartZeichnen(uint8_t i) {
  tft.fillRect(CHART_X, CHART_Y, CHART_W, CHART_H, FARBE_BG);

  uint8_t n = daten[i].punkte;
  if (n < 2) {
    textAn(CHART_X, CHART_Y + 34, "Verlauf nicht verfuegbar", &FreeSans9pt7b, FARBE_GRAU);
    return;
  }

  // Wertebereich bestimmen – der Vortagesschluss soll immer sichtbar sein,
  // damit die Nulllinie nicht aus dem Bild läuft
  float vortag = daten[i].kurs - daten[i].diff;
  float minWert = daten[i].verlauf[0];
  float maxWert = daten[i].verlauf[0];

  for (uint8_t k = 1; k < n; k++) {
    if (daten[i].verlauf[k] < minWert) minWert = daten[i].verlauf[k];
    if (daten[i].verlauf[k] > maxWert) maxWert = daten[i].verlauf[k];
  }

  if (vortag < minWert) minWert = vortag;
  if (vortag > maxWert) maxWert = vortag;

  float spanne = maxWert - minWert;
  if (spanne < 0.0001f) spanne = 1.0f; // Division durch null vermeiden

  // Gestrichelte Linie auf Höhe des Vortagesschlusses
  int yVortag = CHART_Y + CHART_H - 1 - (int)((vortag - minWert) / spanne * (CHART_H - 1));
  for (int x = CHART_X; x < CHART_X + CHART_W; x += 6) {
    tft.drawFastHLine(x, yVortag, 3, FARBE_LINIE);
  }

  // Bildschirmkoordinaten aller Punkte berechnen
  int px[MAX_PUNKTE], py[MAX_PUNKTE];
  for (uint8_t k = 0; k < n; k++) {
    px[k] = CHART_X + (int)((float)k * (CHART_W - 1) / (n - 1));
    py[k] = CHART_Y + CHART_H - 1 - (int)((daten[i].verlauf[k] - minWert) / spanne * (CHART_H - 1));
  }

  uint16_t linie  = (daten[i].diff >= 0) ? FARBE_GRUEN : FARBE_ROT;
  uint16_t flaeche = (daten[i].diff >= 0) ? FARBE_GRUEN_BG : FARBE_ROT_BG;

  // Durchgang 1: Fläche unter der Kurve füllen
  for (uint8_t k = 1; k < n; k++) {
    for (int x = px[k - 1]; x <= px[k]; x++) {
      int y = py[k - 1];
      if (px[k] != px[k - 1]) {
        y += (py[k] - py[k - 1]) * (x - px[k - 1]) / (px[k] - px[k - 1]);
      }
      tft.drawFastVLine(x, y, CHART_Y + CHART_H - y, flaeche);
    }
  }

  // Durchgang 2: Kurve darüber zeichnen
  for (uint8_t k = 1; k < n; k++) {
    tft.drawLine(px[k - 1], py[k - 1], px[k], py[k], linie);
  }
}

void punkteZeichnen(uint8_t aktiv) {
  int x = BREITE / 2 - ((ANZAHL - 1) * 14) / 2;
  for (uint8_t k = 0; k < ANZAHL; k++) {
    tft.fillCircle(x + k * 14, 228, 3, (k == aktiv) ? FARBE_TEXT : FARBE_GRAU);
  }
}

void uhrzeitZeichnen() {
  struct tm zeit;
  if (!getLocalTime(&zeit, 0)) return;
  if (zeit.tm_sec == letzteSekunde) return; // nur einmal pro Sekunde neu
  letzteSekunde = zeit.tm_sec;

  char puffer[12];
  strftime(puffer, sizeof(puffer), "%H:%M:%S", &zeit);

  // Eingebaute Schrift: kann den Hintergrund gleich mitfüllen -> kein Flackern
  tft.setFont();
  tft.setTextSize(2);
  tft.setTextColor(FARBE_TEXT, FARBE_BAR);
  tft.setCursor(BREITE - 88 - 12, 6);
  tft.print(puffer);
  tft.setTextSize(1);
}

// ---------- Kleine Helfer ----------
void textAn(int x, int grundlinie, const char* txt, const GFXfont* font, uint16_t farbe) {
  tft.setFont(font);
  tft.setTextSize(1);
  tft.setTextColor(farbe);
  tft.setCursor(x, grundlinie); // bei GFX-Schriften ist y die Grundlinie
  tft.print(txt);
}

void statusText(const char* txt) {
  tft.fillScreen(FARBE_BG);
  textAn(20, 125, txt, &FreeSans12pt7b, FARBE_TEXT);
}

Schritt 6: Hochladen und starten

  1. Werkzeuge → Board: ESP32 Dev Module
  2. Werkzeuge → Port: den Port deines Boards wählen
  3. Upload Speed: 921600 (bei Problemen auf 115200 reduzieren)
  4. Auf Hochladen klicken.

Bleibt der Upload bei Connecting........___ hängen: BOOT-Taste am ESP32 gedrückt halten, bis der Upload anläuft. Manche Boards brauchen diesen Anstupser. Danach wird folgendes der Reihe nach sichtbar:

  • Verbinde mit WLAN …"
  • Hole Kurse …"
  • Lade Charts …" (dauert wegen der Wartezeit zwischen den Anfragen etwa fünf Sekunden)
  • und schließlich den ersten Wert mit Kurve

Parallel den Seriellen Monitor bei 115200 Baud öffnen. Dort läuft mit, welche Kurse und wie viele Chartpunkte abgerufen wurden – ideal zur Fehlersuche.


Optional | 3D-gedrucktes Gehäuse

Das Gehäuse besteht aus 3 Teilen und benötigt 4 5x2mm Magnete. Den Download gibts hier.

Fehlerbehebung

Symptom Ursache & Lösung
Display bleibt schwarz Hintergrundbeleuchtung prüfen: liegt LED an 3V3? Danach CS, DC und RESET kontrollieren.
Display bleibt weiß Meist DC oder RESET vertauscht. Prüfe, ob die #define-Werte oben im Sketch zur Verkabelung passen.
Bild verzerrt, gestreift oder mit Artefakten tft.setSPISpeed() auf 24000000 reduzieren. Lange oder billige Jumper-Kabel vertragen 40 MHz oft nicht.
Farben vertauscht (Rot / Blau) Manche Module verwenden BGR statt RGB. Abhilfe: nach tft.begin() die Zeile tft.sendCommand(ILI9341_MADCTL, (uint8_t[]) {0x28}, 1); einfügen und den Wert variieren.
Bild steht auf dem Kopf tft.setRotation() in setup() zwischen 1 und 3 wechseln – beides ist Querformat, die beiden Werte unterscheiden sich um 180°. Welcher passt, hängt davon ab, ob die Stiftleiste des Displays links oder rechts sitzt. (0 und 2 ergeben Hochformat, dafür müsstest du das Layout anpassen.)
FreeSans12pt7b nicht gefunden Die Adafruit-GFX-Bibliothek ist nicht (vollständig) installiert. Über den Bibliotheksverwalter nachinstallieren.
redeclared as different kind of entity Eine eigene Variable trägt denselben Namen wie eine Funktion der Standardbibliothek. Berüchtigte Kandidaten: index, time, link, div, remove, y0/y1. Variable umbenennen, z. B. anzeigeIndex statt index.
WLAN verbindet nicht Der ESP32 kann kein 5 GHz. Prüfe, ob dein Router ein 2,4-GHz-Netz anbietet und das Passwort exakt stimmt.
Finnhub HTTP 401 API-Key falsch oder noch nicht aktiv. Im Browser testen (siehe Schritt 1).
Finnhub HTTP 429 Zu viele Anfragen. KURS_INTERVALL erhöhen oder Watchlist verkleinern.
Finnhub HTTP 403 Das Symbol gehört zu einer Börse außerhalb des Free-Tarifs. Bei US-Aktien bleiben.
TwelveData HTTP 429 / „API credits“ Das Tageslimit von 800 ist erreicht. CHART_INTERVALL erhöhen – bei 6 Minuten sind es nur noch 240 Abrufe am Tag.
Chart zeigt „Verlauf nicht verfuegbar“ Der Abruf ist fehlgeschlagen – das ist kein Ladezustand, alle Charts werden beim Start geholt. Serieller Monitor verrät die Ursache. Häufig: falscher Twelve-Data-Key oder aufgebrauchte Credits. Achtung, Twelve Data meldet beides mit HTTP 200 und "status":"error" im Body. Die Test-URL aus Schritt 1 im Browser aufrufen, dort steht der Grund im Klartext.
Chart ist eine flache Linie Normal außerhalb der Handelszeiten – es gibt dann keine neuen Kurse.
Kurs ist 0,00 Symbolschreibweise prüfen. Beide APIs erwarten exakt das Börsenkürzel, z. B. BRK.B statt BRK-B.
Upload schlägt fehl BOOT-Taste beim Verbindungsaufbau gedrückt halten. Kein Port sichtbar? CP2102-Treiber installieren und ein echtes Datenkabel verwenden.
ESP32 startet ständig neu Meist die Stromversorgung: USB-Ports an Monitoren oder Hubs liefern oft zu wenig. Netzteil mit mindestens 1 A nutzen.