Ter­min­ka­len­der mit ESP32 mit 2,8 Zoll TFT (CYD)

Lese­zeit: 13 Minu­ten

Ziel des Projekts

Pas­send zu jedem Tag sol­len die anste­hen­den Ter­mi­ne auf dem TFT ange­zeigt wer­den. Die Daten befin­den sich auf einer SD-Kar­te und wer­den täg­lich um Mit­ter­nacht aktua­li­siert. Zusätz­lich wer­den Datum und Uhr­zeit angezeigt.

Die Hard­ware

Für die­se Pro­jekt wird ein CYD (Cheap Yel­low Dis­play) ver­wen­det. Auf der Rück­sei­te des TFTs mit 2,8-Zoll und 320×240 Pixeln befin­den sich ein ESP32-Wroom, ein SD-Kar­ten-Modul, eine RGB-LED und Anschluss­mög­lich­kei­ten für Peri­phe­rie­ge­rä­te mit­hil­fe von JST-Steckern. 

Es gibt meh­re­re Ver­sio­nen des CYD: ⇒ESP32-2432S028 und ESP32-JC2432W328C. Sie unter­schei­den sich im ver­wen­de­ten Grafiktreiber:

  • mit Mikro-USB-Anschluss (ESP32-2432S028)
    Gra­fik­trei­ber ⇒ILI9341
  • mit USB-C-Anschluss (ESP32-2432S028 und ESP32-JC2432W328)
    Gra­fik­trei­ber ⇒ST7789

Kon­fi­gu­ra­ti­on des Mikrocontrollers

Der Daten­satz auf der SD-Karte

Die Daten müs­sen im For­mat tt.mm./Name des Ein­trags vor­lie­gen. Der Datei­na­me ist zwin­gend Kalender.txt. Wenn Tag oder Monat ein­stel­lig sind, muss eine 0 vor­an­ge­stellt wer­den. Die Rei­hen­fol­ge der Ein­trä­ge darf belie­big sein. Es wer­den maxi­mal vier Ein­trä­ge für den jewei­li­gen Tag auf dem Dis­play dar­ge­stellt. Für jeden Ein­trag muss eine neue Zei­le erstellt wer­den.
Das gilt auch für meh­re­re Ter­mi­ne an einem Tag.
Die Datei wird nur ein­mal um Mit­ter­nacht neu gele­sen, daher kann die SD-Kar­te zwi­schen­durch her­aus­ge­nom­men und neu beschrie­ben werden.

Als Trenn­zei­chen zwi­schen Datum und Kalen­der­ein­trag dient der /. 

Auf­bau der Datei

19.07./Theresa Geburtstag
20.07./10:15 Zahnarzt
21.07./Thomas Geburtstag
22.07./10:30 Physio

Kalen­der­da­ten schreiben

Mit die­sem Pro­gramm kannst du Daten auf die SD-Kar­te schrei­ben, sie wird auto­ma­tisch als Kalender.txt gespei­chert. Wenn du eine neue Datei erstel­len willst, musst du in Zei­le 52 die // entfernen.

Du musst nach jeder Zei­le die Ein­ga­be-Tas­te drü­cken.
Zum Spei­chern musst du nach der letz­ten Zei­le die Tas­te # drü­cken.
Anschlie­ßend wird die Datei angezeigt.

So sieht es dann aus:

#include "SdFat.h"
SdFs SD;

// 3 = FAT32
#define SD_FAT_TYPE 3

// SPI-Geschwindigkeit
#define SPI_SPEED SD_SCK_MHZ(4)

/*
  Pinbelegung:
  CIPO -> 19
  COPI -> 23
  SCK  -> 18
  CS   ->  5
*/

// CSPin der SD-Karte
int CSPin = 5;

bool Schreiben = false;

void setup() 
{
  // Bezeichner für die Datei
  File Datei;

  Serial.begin(9600);

  // auf serielle Verbindung warten
  while (!Serial);
  delay(1000);

  /*
     SD-Karte mit Angabe des Datenpins starten
     wenn die Intialisierung fehlschlägt
     - keine SD-Karte vorhanden
     - falsche Pinbelegung
     -> es wird eine Fehlermeldung angezeigt
  */
  if (!SD.begin(CSPin, SPI_SPEED)) 
  {
    Serial.println("Initialisierung fehlgeschlagen!");
  } 
  else Serial.println("Initialisierung abgeschlossen");

  // char-Array für den Dateinamen erstellen
  char Dateiname[20] = "Kalender.txt";

  if (SD.exists("Kalender.txt")) 
  {
    SD.remove("Kalender.txt");
  }

  String DatenSchreiben;

  /*
    O_CREAT -> Datei erstellen, wenn sie nicht existiert
    O_WRITE -> in die Datei schreiben
    O_AT_END -> Startposition zum Schreiben an das Ende der Datei setzen
  */
  Datei.open(Dateiname, O_CREAT | O_WRITE | O_AT_END);

  // Daten in Datei schreiben
  Serial.println("Daten eingeben und Eingabe mit # beenden.");
  Serial.println("Format: tt.mm./Eintrag:");
  Serial.println("Beispiel: 02.07./Thomas Geburtstag");

  // solange kein # eingegeben wurde und Schreiben false ist
  while (DatenSchreiben != "#" && !Schreiben) 
  {
    while (Serial.available() > 0) 
    {
      DatenSchreiben = Serial.readStringUntil('\n');
      Serial.println(DatenSchreiben);

      // wenn das erste eingegebene Zeichen # ist 
      // -> Schreiben auf true setzen -> while verlassen
      if (DatenSchreiben.startsWith("#")) Schreiben = true;

      // ansonsten Datensatz in Datei schreiben
      else Datei.print(DatenSchreiben + "\n");
    }
  }

  // Datei schließen
  Datei.close();
  Serial.println("\nDatei " + String(Dateiname) + " erfolgreich geschrieben!");
  
  // Datei Zahlen.txt öffnen
  Serial.println("Zeige " + String(Dateiname));
  if (Datei.open(Dateiname, O_RDONLY)) 
  {
    while (Datei.available()) 
    {
      // ... werden sie gelesen und im Seriellen Monitor ausgegeben
      Serial.write(Datei.read());
    }
   }

   // wenn die Datei Kalender.txt nicht existiert ... 
   else 
   {
     // char-Array Dateiname muss in String umgewandelt werden
     Serial.print("Datei " + String(Dateiname) + " nicht gefunden!");
   }

  // Datei schließen
  Datei.close();
}

void loop() 
{
  // bleibt leer, das Programm läuft nur einmal
}

Natür­lich kannst du die Datei auch mit einem belie­bi­gen Text­edi­tor erstel­len oder bear­bei­ten. Weil das Durch­su­chen der Datei eini­ge Zeit in Anspruch nimmt, emp­fiehlt es sich abge­lau­fe­ne Ter­mi­ne zu löschen.

Das Pro­gramm

Vor­be­mer­kun­gen

  • Das Pro­gramm ver­wen­det die Schrift­ar­ten von ⇒u8g2. Wenn du grö­ße­re Schrift­ar­ten ver­wen­den willst, musst du aus­pro­bie­ren, ob die Ein­trä­ge noch auf das Dis­play pas­sen.
    Eini­ge Bei­spie­le:
    14pt: u8g2_font_luRS14_tf
    16pt: u8g2_fnt_logisoso16_tf
    18pt: u8g2_font_luBS18_tf
    20pt: u8g2_font_fub20_tf
    22pt: u8g2_font_logisoso22_tf
    24pt: u8g2_font_helvB24_tf
    26pt: u8g2_font_logisoso26_tf
    28pt: u8g2_font_logisoso28_tf
    Die Schrif­ten wer­den mit setFont(Name_der_Schrift) definiert.
  • Vor­der­grund- und Hin­ter­grund­far­be kön­nen aus der Lis­te im Kopf des Pro­gramms in den ent­spre­chen­den Varia­blen getrennt für den Bereich von Datum und Zeit und für den Bereich der Ter­mi­ne fest­ge­legt wer­den:
    Far­ben Datum/Zeit:
    int Hin­ter­grund­Far­be­Da­tum = SCHWARZ;
    int Vor­der­grund­Far­be­Da­tum = WEISS;
    Far­ben Ter­mi­ne:
    int Hin­ter­grund­Far­be­Ter­mi­ne = SCHWARZ;
    int Vor­der­grund­Far­be­Ter­mi­ne = WEISS;
  • Es kön­nen höchs­ten 50 Daten­sät­ze gele­sen wer­den. Wenn du mehr benö­tigst, musst du den Wert der Varia­blen Daten­Max erhöhen.
  • Bis zu 90 Sekun­den ver­sucht das Pro­gramm eine Ver­bin­dung zum Zeit­ser­ver auf­zu­bau­en und das kor­rek­te Datum und die aktu­el­le Zeit zu holen.
    Gelingt das nicht, wird das Pro­gramm been­det.
    Beim nächs­ten Start ant­wor­tet der Zeit­ser­ver zumeist in kur­zer Zeit.

Benö­tig­te Bibliotheken

Je nach Art des USB-Anschlus­ses wird ein ande­rer Gra­fik-Trei­ber benötigt:

TFT mit USB-C-Anschluss

oder:

TFT mit Mikro-USB-Anschluss
Zusätz­li­che Schriftarten

Wenn du zuvor die Biblio­thek SdFat instal­liert hast kann es zu Kon­flik­ten kom­men. In die­sem Fall musst du sie über die Biblio­theks­ver­wal­tung deinstal­lie­ren und statt­des­sen den Adafruit Fork installieren.

Ein­bin­dung der Biblio­the­ken und setup-Teil

Gra­fik­trei­ber ST7789

Für den ESP-JC2432W328 gibt es eine Beson­der­heit:
Der Wert der Varia­blen TFT_BL muss auf 27 gesetzt werden.

#include "WiFi.h"
#include "time.h"
#include "U8g2_for_Adafruit_GFX.h"
#include "SdFat.h"
#include "Adafruit_ST7789.h"   

// SPI-Pins TFT
#define TFT_BL   21
#define TFT_CS   15 
#define TFT_DC    2
#define TFT_MISO 12
#define TFT_MOSI 13
#define TFT_SCLK 14
#define TFT_RST  -1

// Objekt tft der Bibliothek Adafruit_ST7789 erstellen
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RST);

// Objekt SD der Bibliothek SdFat erstellen
SdFat SD;    

// 3 = FAT32
#define SD_FAT_TYPE 3

// SPI-Geschwindigkeit
#define SPI_SPEED SD_SCK_MHZ(4)

// CSPin der SD-Karte
int CSPin = 5;

// WiFi-Daten
char Router[] = "Router_SSID";
char Passwort[] = "xxxxxxxx";

// Variablen für die Zeit
int Stunden, Minuten, Sekunden;

// Objekt u8g2Schriften
U8G2_FOR_ADAFRUIT_GFX u8g2Schriften;

// Farben
#define SCHWARZ     0x0000
#define WEISS       0xFFFF
#define BLAU        0x001F
#define ROT         0xF800
#define GRUEN       0x07E0
#define CYAN        0x07FF
#define MAGENTA     0xF81F
#define GELB        0xFFE0
#define BRAUN       0x9A60
#define GRAU        0x7BEF
#define GRUENGELB   0xB7E0
#define DUNKELCYAN  0x03EF
#define ORANGE      0xFDA0
#define PINK        0xFE19
#define BORDEAUX    0xA000
#define HELLBLAU    0x867D
#define VIOLETT     0x915C
#define SILBER      0xC618
#define GOLD        0xFEA0

// farbliche Gestaltung
// schwarz/weiß
int HintergrundFarbeDatum = SCHWARZ;
int VordergrundFarbeDatum = WEISS;
int HintergrundFarbeTermine = SCHWARZ;
int VordergrundFarbeTermine = WEISS;

// farbig
// int HintergrundFarbeDatum = SILBER;
// int VordergrundFarbeDatum = BLAU;
// int HintergrundFarbeTermine = BLAU;
// int VordergrundFarbeTermine = WEISS;

// NTP-Server aus dem Pool
#define Zeitserver "de.pool.ntp.org"

/*
  Liste der Zeitzonen
  https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
  Zeitzone CET = Central European Time -1 -> 1 Stunde zurück
  CEST = Central European Summer Time von
  M3 = März, 5.0 = Sonntag 5. Woche, 02 = 2 Uhr
  bis M10 = Oktober, 5.0 = Sonntag 5. Woche 03 = 3 Uhr
*/
#define Zeitzone "CET-1CEST,M3.5.0/02,M10.5.0/03"

// time_t enthält die Anzahl der Sekunden seit dem 1.1.1970 0 Uhr
time_t aktuelleZeit;

/* 
  Struktur tm
  tm_hour -> Stunde: 0 bis 23
  tm_min -> Minuten: 0 bis 59
  tm_sec -> Sekunden 0 bis 59
  tm_mday -> Tag 1 bis 31
  tm_mon -> Monat: 0 (Januar) bis 11 (Dezember)
  tm_year -> Jahre seit 1900
  tm_yday -> vergangene Tage seit 1. Januar des Jahres
  tm_isdst -> Wert > 0 = Sommerzeit (dst = daylight saving time)
*/
tm Zeit;

unsigned long Zeitmessung = 0;  

// Anzahl der Datensätze
#define DatenMax 50

// Array für gelesene Zeile aus Datei Kalender.txt
String DatenLesen[DatenMax];

// nach Datum und Beschreibung getrenntes Array
String DatumEintrag[DatenMax];
String KalenderEintrag[DatenMax];

// Bezeichner für die Datei
File Datei;

// char-Array für den Dateinamen erstellen
char Dateiname[20] = "Kalender.txt";

void setup() 
{
  // Schriften von u8g2 tft zuordnen
  u8g2Schriften.begin(tft); 

  Serial.begin(9600);

  // Zeitzone: Parameter für die zu ermittelnde Zeit
  configTzTime(Zeitzone, Zeitserver);

  // WiFi starten
  WiFi.mode(WIFI_STA);
  WiFi.begin(Router, Passwort);

  Serial.println("------------------------");

  while (WiFi.status() != WL_CONNECTED) 
  {
    delay(200);
    Serial.print(".");
  }
  
  Serial.println();
  Serial.print("Verbunden mit ");
  Serial.println(WiFi.SSID());
  Serial.print("IP über DHCP: ");
  Serial.println(WiFi.localIP());

  // Zeit holen
  time(&aktuelleZeit);

  // localtime_r -> Zeit in die lokale Zeitzone setzen
  localtime_r(&aktuelleZeit, &Zeit);

  // Zeit in Stunden, Minuten und Sekunden
  Stunden = Zeit.tm_hour, Minuten = Zeit.tm_min, Sekunden = Zeit.tm_sec;

   // Hintergrundbeleuchtung einschalten
  pinMode(TFT_BL, OUTPUT);
  digitalWrite(TFT_BL, HIGH);

  // tft starten, Farben invertieren
  tft.init(240, 320);
  tft.invertDisplay(0);

  // Rotation anpassen
  tft.setRotation(1);

  // HintergrundFarbeer Hintergrund
  tft.fillScreen(HintergrundFarbeTermine);
  tft.setTextSize(2);
  tft.setTextColor(VordergrundFarbeTermine);
  tft.setCursor(1, 20);

  /*
    SD-Karte mit Angabe des CSPins und  der SPI-Geschwindigkeit starten
    wenn die Intialisierung fehlschlägt
    - keine SD-Karte vorhanden
    - falsche Pinbelegung
    -> es wird eine Fehlermeldung angezeigt
  */
  if (!SD.begin(CSPin, SPI_SPEED)) 
  {
    tft.println("Start der SD-Karte");
    tft.print("fehlgeschlagen!");
    Serial.println("Start der SD-Karte fehlgeschlagen!");
    Serial.println("Programm angehalten ...");

    // Programm anhalten
    while(1);;
  } 

  else 
  {
    Serial.println("SD-Karte gestartet ...");
    tft.print("SD-Karte gestartet!");
    tft.setCursor(1, 50);
    tft.print("auf Zeitserver warten ...");
  }

  // tft.fillScreen(HintergrundFarbe);

 // wenn die Datei existiert
  if (SD.exists(Dateiname)) 
  {
    // Datei Kalender.txt öffnen
    Serial.println("-------------------------");
    Serial.println("Zeige " + String(Dateiname));
    
    Datei.open(Dateiname, O_RDONLY);

    int Trenner;
    
    for (int i = 0; i < DatenMax; i++) 
    {
      // Zeile bis zum return ('\n') lesen
      DatenLesen[i] = Datei.readStringUntil('\n');

      // Position des Trennzeichens
      Trenner = DatenLesen[i].indexOf("/");

      // Leerzeichen entfernen
      DatenLesen[i].trim();

      // Zeile am Komma in Datum und Kalendereintrag trennnen
      DatumEintrag[i] = DatenLesen[i].substring(0, Trenner);
      KalenderEintrag[i] = DatenLesen[i].substring(Trenner + 1, DatenLesen[i].length());
        
      // Abbruch, wenn kein Datensatz nehr vorhanden
      if (DatenLesen[i] == "") break;
      Serial.println(DatumEintrag[i]);
      Serial.println(KalenderEintrag[i]);
    }
    
    // Datei schließen
    Datei.close(); 
  }

  // Datei existiert nicht
  else
  {
    Serial.println("Datei " + String(Dateiname) + " nicht gefunden!");
    Serial.println("Programm angehalten ...");

    // Programm beenden
    while(1);
  }

  // beim Start entspricht das Datum der Unixtime: 1.1.1970
  // Datum/Kalender sollen erst angezeigt werden, wenn das Datum korrekt ist
  String Jahr = String(Zeit.tm_year + 1900); 
  int Zaehler = 0;

  // String Jahr nach "1970" durchsuchen
  int Suche = Jahr.indexOf("1970");         

  Serial.println("-------------------------"); 
  Serial.println("Datum und Zeit holen (maximal 90 Sekunden)...");
  
  // solange die Suche nicht erfolgreich ist
  while (Suche != -1) 
  {
    // aktuelle Zeit holen
    time(&aktuelleZeit);

    // localtime_r -> Zeit in die lokale Zeitzone setzen
    localtime_r(&aktuelleZeit, &Zeit);
    Jahr = String(Zeit.tm_year + 1900); 
    
    // String Jahr nach "1970" durchsuchen
    Suche = Jahr.indexOf("1970");

    // Zeit in Stunden, Minuten und Sekunden
    Stunden = int(Zeit.tm_hour), Minuten = int(Zeit.tm_min), Sekunden = int(Zeit.tm_sec);
    delay(1000);
    Zaehler ++;

    if (Zaehler >= 90)
    {
      Serial.println();
      Serial.println("Datum und Zeit konnte innerhalb von " + String(Zaehler) + " Sekunden nicht geholt werden");
      Serial.println("Programm wird beendet");

      // Programm beenden
      while(1);
    }

    Serial.print(".");
  }   

  Serial.println();

  // Datum/Zeit erfolgreich synchronisiert
  if (Suche == -1) 
  {
    Serial.println("-------------------------");
    Serial.println("Datum/Zeit erfolgreich synchronisiert ...");
    
    if (Zeit.tm_mday < 10) Serial.print("0");
    Serial.print(Zeit.tm_mday);
    Serial.print(".");

    // Monat: führende 0 ergänzen
    if (Zeit.tm_mon < 9) Serial.print("0");
    
    // Zählung beginnt mit 0 -> +1
    Serial.print(Zeit.tm_mon + 1);
    Serial.print(".");

    // Anzahl Jahre seit 1900
    Serial.println(Zeit.tm_year + 1900);

    if (Zeit.tm_hour < 10) Serial.print("0");
    Serial.print(Zeit.tm_hour);
    Serial.print(":");
      
    if (Zeit.tm_min < 10) Serial.print("0");
    Serial.println(Zeit.tm_min);
    Serial.println("-------------------------");

    ZeigeDatum();

    // beim Start -> Kalender nach Terminen durchsuchen
    Serial.println("Kalender nach Terminen durchsuchen ...");
    KalenderDurchsuchen();
    
    // Zeit anzeigen
    tft.fillRect(1, 30, tft.width(), 45, HintergrundFarbeDatum);
    u8g2Schriften.setFont(u8g2_font_logisoso28_tf);   
    u8g2Schriften.setForegroundColor(VordergrundFarbeDatum);
    u8g2Schriften.setBackgroundColor(HintergrundFarbeDatum);
    u8g2Schriften.setCursor(1, 70);

    if (Zeit.tm_hour < 10) 
    {
      u8g2Schriften.print("0");
      Serial.print("0");
    }
    u8g2Schriften.print(Zeit.tm_hour);
    u8g2Schriften.print(":");
    Serial.print(Zeit.tm_hour);
    Serial.print(":");
      
    if (Zeit.tm_min < 10) 
    {
      u8g2Schriften.print("0");
      Serial.print("0");
    }

    u8g2Schriften.print(Zeit.tm_min);
    Serial.print(Zeit.tm_min);
    Serial.println(" Uhr");

    tft.drawLine(1, 80, tft.width(), 80, VordergrundFarbeDatum);
  }
  
  Zeitmessung = millis() + 1000; 
}

Gra­fik­trei­ber ILI9341

#include "WiFi.h"
#include "time.h"
#include "U8g2_for_Adafruit_GFX.h"
#include "SdFat.h"
#include "Adafruit_ILI9341.h"

// SPI-Pins TFT
#define TFT_BL   21
#define TFT_CS   15 
#define TFT_DC    2
#define TFT_MISO 12
#define TFT_MOSI 13
#define TFT_SCLK 14
#define TFT_RST  -1

// Objekt tft der Bibliothek Adafruit_ILI9341 erstellen
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RST, TFT_MISO);

// Objekt SD der Bibliothek SdFat erstellen
SdFat SD;    

// 3 = FAT32
#define SD_FAT_TYPE 3

// SPI-Geschwindigkeit
#define SPI_SPEED SD_SCK_MHZ(4)

// CSPin der SD-Karte
int CSPin = 5;

// WiFi-Daten
char Router[] = "Router_SSID";
char Passwort[] = "xxxxxxxx";

// Variablen für die Zeit
int Stunden, Minuten, Sekunden;

// Objekt u8g2Schriften
U8G2_FOR_ADAFRUIT_GFX u8g2Schriften;

// Farben
#define SCHWARZ     0x0000
#define WEISS       0xFFFF
#define BLAU        0x001F
#define ROT         0xF800
#define GRUEN       0x07E0
#define CYAN        0x07FF
#define MAGENTA     0xF81F
#define GELB        0xFFE0
#define BRAUN       0x9A60
#define GRAU        0x7BEF
#define GRUENGELB   0xB7E0
#define DUNKELCYAN  0x03EF
#define ORANGE      0xFDA0
#define PINK        0xFE19
#define BORDEAUX    0xA000
#define HELLBLAU    0x867D
#define VIOLETT     0x915C
#define SILBER      0xC618
#define GOLD        0xFEA0

// farbliche Gestaltung
// schwarz/weiß
int HintergrundFarbeDatum = SCHWARZ;
int VordergrundFarbeDatum = WEISS;
int HintergrundFarbeTermine = SCHWARZ;
int VordergrundFarbeTermine = WEISS;

// farbig
// int HintergrundFarbeDatum = SILBER;
// int VordergrundFarbeDatum = BLAU;
// int HintergrundFarbeTermine = BLAU;
// int VordergrundFarbeTermine = WEISS;

// NTP-Server aus dem Pool
#define Zeitserver "de.pool.ntp.org"

/*
  Liste der Zeitzonen
  https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
  Zeitzone CET = Central European Time -1 -> 1 Stunde zurück
  CEST = Central European Summer Time von
  M3 = März, 5.0 = Sonntag 5. Woche, 02 = 2 Uhr
  bis M10 = Oktober, 5.0 = Sonntag 5. Woche 03 = 3 Uhr
*/
#define Zeitzone "CET-1CEST,M3.5.0/02,M10.5.0/03"

// time_t enthält die Anzahl der Sekunden seit dem 1.1.1970 0 Uhr
time_t aktuelleZeit;

/* 
  Struktur tm
  tm_hour -> Stunde: 0 bis 23
  tm_min -> Minuten: 0 bis 59
  tm_sec -> Sekunden 0 bis 59
  tm_mday -> Tag 1 bis 31
  tm_mon -> Monat: 0 (Januar) bis 11 (Dezember)
  tm_year -> Jahre seit 1900
  tm_yday -> vergangene Tage seit 1. Januar des Jahres
  tm_isdst -> Wert > 0 = Sommerzeit (dst = daylight saving time)
*/
tm Zeit;

unsigned long Zeitmessung = 0;  

// Anzahl der Datensätze
#define DatenMax 50

// Array für gelesene Zeile aus Datei Kalender.txt
String DatenLesen[DatenMax];

// nach Datum und Beschreibung getrenntes Array
String DatumEintrag[DatenMax];
String KalenderEintrag[DatenMax];

// Bezeichner für die Datei
File Datei;

// char-Array für den Dateinamen erstellen
char Dateiname[20] = "Kalender.txt";

void setup() 
{
  // Schriften von u8g2 tft zuordnen
  u8g2Schriften.begin(tft); 

  Serial.begin(9600);

  // Zeitzone: Parameter für die zu ermittelnde Zeit
  configTzTime(Zeitzone, Zeitserver);

  // WiFi starten
  WiFi.mode(WIFI_STA);
  WiFi.begin(Router, Passwort);

  Serial.println("------------------------");

  while (WiFi.status() != WL_CONNECTED) 
  {
    delay(200);
    Serial.print(".");
  }
  
  Serial.println();
  Serial.print("Verbunden mit ");
  Serial.println(WiFi.SSID());
  Serial.print("IP über DHCP: ");
  Serial.println(WiFi.localIP());

  // Zeit holen
  time(&aktuelleZeit);

  // localtime_r -> Zeit in die lokale Zeitzone setzen
  localtime_r(&aktuelleZeit, &Zeit);

  // Zeit in Stunden, Minuten und Sekunden
  Stunden = Zeit.tm_hour, Minuten = Zeit.tm_min, Sekunden = Zeit.tm_sec;

  // Hintergrundbeleuchtung einschalten
  pinMode(TFT_BL, OUTPUT);
  digitalWrite(TFT_BL, HIGH);

  // tft starten, Farben invertieren
  tft.begin();
  tft.invertDisplay(0);

  // Rotation anpassen
  tft.setRotation(1);

  // HintergrundFarbeer Hintergrund
  tft.fillScreen(HintergrundFarbeTermine);
  tft.setTextSize(2);
  tft.setTextColor(VordergrundFarbeTermine);
  tft.setCursor(1, 20);

  /*
    SD-Karte mit Angabe des CSPins und  der SPI-Geschwindigkeit starten
    wenn die Intialisierung fehlschlägt
    - keine SD-Karte vorhanden
    - falsche Pinbelegung
    -> es wird eine Fehlermeldung angezeigt
  */
  if (!SD.begin(CSPin, SPI_SPEED)) 
  {
    tft.println("Start der SD-Karte");
    tft.print("fehlgeschlagen!");
    Serial.println("Start der SD-Karte fehlgeschlagen!");
    Serial.println("Programm angehalten ...");

    // Programm anhalten
    while(1);;
  } 

  else 
  {
    Serial.println("SD-Karte gestartet ...");
    tft.print("SD-Karte gestartet!");
    tft.setCursor(1, 50);
    tft.print("auf Zeitserver warten ...");
  }

  // tft.fillScreen(HintergrundFarbe);

 // wenn die Datei existiert
  if (SD.exists(Dateiname)) 
  {
    // Datei Kalender.txt öffnen
    Serial.println("-------------------------");
    Serial.println("Zeige " + String(Dateiname));
    
    Datei.open(Dateiname, O_RDONLY);

    int Trenner;
    
    for (int i = 0; i < DatenMax; i++) 
    {
      // Zeile bis zum return ('\n') lesen
      DatenLesen[i] = Datei.readStringUntil('\n');

      // Position des Trennzeichens
      Trenner = DatenLesen[i].indexOf("/");

      // Leerzeichen entfernen
      DatenLesen[i].trim();

      // Zeile am Komma in Datum und Kalendereintrag trennnen
      DatumEintrag[i] = DatenLesen[i].substring(0, Trenner);
      KalenderEintrag[i] = DatenLesen[i].substring(Trenner + 1, DatenLesen[i].length());
        
      // Abbruch, wenn kein Datensatz nehr vorhanden
      if (DatenLesen[i] == "") break;
      Serial.println(DatumEintrag[i]);
      Serial.println(KalenderEintrag[i]);
    }
    
    // Datei schließen
    Datei.close(); 
  }

  // Datei existiert nicht
  else
  {
    Serial.println("Datei " + String(Dateiname) + " nicht gefunden!");
    Serial.println("Programm angehalten ...");

    // Programm beenden
    while(1);
  }

  // beim Start entspricht das Datum der Unixtime: 1.1.1970
  // Datum/Kalender sollen erst angezeigt werden, wenn das Datum korrekt ist
  String Jahr = String(Zeit.tm_year + 1900); 
  int Zaehler = 0;

  // String Jahr nach "1970" durchsuchen
  int Suche = Jahr.indexOf("1970");         

  Serial.println("-------------------------"); 
  Serial.println("Datum und Zeit holen (maximal 90 Sekunden)...");
  
  // solange die Suche nicht erfolgreich ist
  while (Suche != -1) 
  {
    // aktuelle Zeit holen
    time(&aktuelleZeit);

    // localtime_r -> Zeit in die lokale Zeitzone setzen
    localtime_r(&aktuelleZeit, &Zeit);
    Jahr = String(Zeit.tm_year + 1900); 
    
    // String Jahr nach "1970" durchsuchen
    Suche = Jahr.indexOf("1970");

    // Zeit in Stunden, Minuten und Sekunden
    Stunden = int(Zeit.tm_hour), Minuten = int(Zeit.tm_min), Sekunden = int(Zeit.tm_sec);
    delay(1000);
    Zaehler ++;

    if (Zaehler >= 90)
    {
      Serial.println();
      Serial.println("Datum und Zeit konnte innerhalb von " + String(Zaehler) + " Sekunden nicht geholt werden");
      Serial.println("Programm wird beendet");

      // Programm beenden
      while(1);
    }

    Serial.print(".");
  }   

  Serial.println();

  // Datum/Zeit erfolgreich synchronisiert
  if (Suche == -1) 
  {
    Serial.println("-------------------------");
    Serial.println("Datum/Zeit erfolgreich synchronisiert ...");
    
    if (Zeit.tm_mday < 10) Serial.print("0");
    Serial.print(Zeit.tm_mday);
    Serial.print(".");

    // Monat: führende 0 ergänzen
    if (Zeit.tm_mon < 9) Serial.print("0");
    
    // Zählung beginnt mit 0 -> +1
    Serial.print(Zeit.tm_mon + 1);
    Serial.print(".");

    // Anzahl Jahre seit 1900
    Serial.println(Zeit.tm_year + 1900);

    if (Zeit.tm_hour < 10) Serial.print("0");
    Serial.print(Zeit.tm_hour);
    Serial.print(":");
      
    if (Zeit.tm_min < 10) Serial.print("0");
    Serial.println(Zeit.tm_min);
    Serial.println("-------------------------");

    ZeigeDatum();

    // beim Start -> Kalender nach Terminen durchsuchen
    Serial.println("Kalender nach Terminen durchsuchen ...");
    KalenderDurchsuchen();
    
    // Zeit anzeigen
    tft.fillRect(1, 30, tft.width(), 45, HintergrundFarbeDatum);
    u8g2Schriften.setFont(u8g2_font_logisoso28_tf);   
    u8g2Schriften.setForegroundColor(VordergrundFarbeDatum);
    u8g2Schriften.setBackgroundColor(HintergrundFarbeDatum);
    u8g2Schriften.setCursor(1, 70);

    if (Zeit.tm_hour < 10) 
    {
      u8g2Schriften.print("0");
      Serial.print("0");
    }
    u8g2Schriften.print(Zeit.tm_hour);
    u8g2Schriften.print(":");
    Serial.print(Zeit.tm_hour);
    Serial.print(":");
      
    if (Zeit.tm_min < 10) 
    {
      u8g2Schriften.print("0");
      Serial.print("0");
    }

    u8g2Schriften.print(Zeit.tm_min);
    Serial.print(Zeit.tm_min);
    Serial.println(" Uhr");

    tft.drawLine(1, 80, tft.width(), 80, VordergrundFarbeDatum);
  }
  
  Zeitmessung = millis() + 1000; 
}

Beim Start des Pro­gramms zei­gen die Mel­dun­gen, ob die SD-Kar­te gestar­tet wur­de, die Datei Kalender.txt gele­sen wer­den konn­te und ob Datum und Zeit kor­rekt sind.

Der loop-Teil

void loop() 
{
  // Sekunden weiter zählen
  if (Zeitmessung < millis()) 
  {
    Zeitmessung += 1000;
    Sekunden ++;

    if (Sekunden == 60) 
    { 
      Sekunden = 0; 
    
      // Zeit jede Minute mit Zeitserver synchronisieren
      // aktuelle Zeit holen
      time(&aktuelleZeit);

      // localtime_r -> Zeit in die lokale Zeitzone setzen
      localtime_r(&aktuelleZeit, &Zeit);

      // Zeit in Stunden, Minuten und Sekunden
      Stunden = int(Zeit.tm_hour), Minuten = int(Zeit.tm_min), Sekunden = int(Zeit.tm_sec);

      tft.fillRect(1, 30, tft.width(), 45, HintergrundFarbeDatum);
      u8g2Schriften.setFont(u8g2_font_logisoso28_tf);   
      u8g2Schriften.setForegroundColor(VordergrundFarbeDatum);
      u8g2Schriften.setBackgroundColor(HintergrundFarbeDatum);
      u8g2Schriften.setCursor(1, 70);

      if (Zeit.tm_hour < 10) 
      {
        u8g2Schriften.print("0");
        Serial.print("0");
      }
      u8g2Schriften.print(Zeit.tm_hour);
      u8g2Schriften.print(":");
      Serial.print(Zeit.tm_hour);
      Serial.print(":");
      
      if (Zeit.tm_min < 10) 
      {
        u8g2Schriften.print("0");
        Serial.print("0");
      }

      u8g2Schriften.print(Zeit.tm_min);
      Serial.print(Zeit.tm_min);

      Serial.println(" Uhr");

      // tft.drawLine(1, 80, tft.width(), 80, VordergrundFarbe);

      // Mitternacht 
      // -> Wechsel des Datums anzeigen
      // -> Kalender durchsuchen
      if (Stunden == 0 && Minuten == 0) 
      {
          ZeigeDatum();
          KalenderDurchsuchen();
      }
    }
  }
}

Die Funk­ti­on ZeigeDatum()

Das Datum wird beim Start des Pro­gramms und danach nur noch beim Wech­sel des Datums um Mit­ter­nacht angezeigt.

void ZeigeDatum()
{
  tft.fillRect(1, 1, tft.width(), 79, HintergrundFarbeDatum);
  u8g2Schriften.setFont(u8g2_font_logisoso20_tf);   
  u8g2Schriften.setForegroundColor(VordergrundFarbeDatum);
  u8g2Schriften.setBackgroundColor(HintergrundFarbeDatum);
  
  u8g2Schriften.setCursor(1, 25);

  // Wochentag anzeigen
  switch (Zeit.tm_wday) 
  {
    case 0:
      u8g2Schriften.print("Sonntag");
      break;
    case 1:
      u8g2Schriften.print("Montag");
      break;
    case 2:
      u8g2Schriften.print("Dienstag");
      break;
    case 3:
      u8g2Schriften.print("Mittwoch");
      break;
    case 4:
      u8g2Schriften.print("Donnerstag");
      break;
    case 5:
      u8g2Schriften.print("Freitag");
      break;
    case 6:
      u8g2Schriften.print("Samstag");
      break;
  }

  u8g2Schriften.print(", ");
      
  if (Zeit.tm_mday < 10) u8g2Schriften.print("0");
  u8g2Schriften.print(Zeit.tm_mday);
  u8g2Schriften.print(".");

  // Monat: führende 0 ergänzen
  if (Zeit.tm_mon < 9) u8g2Schriften.print("0");
    
  // Zählung beginnt mit 0 -> +1
  u8g2Schriften.print(Zeit.tm_mon + 1);
  u8g2Schriften.print(".");

  // Anzahl Jahre seit 1900
  u8g2Schriften.print(Zeit.tm_year + 1900);

  tft.drawLine(1, 80, tft.width(), 80, VordergrundFarbeDatum);
}

Die Funk­ti­on KalenderDurchsuchen()

Die Funk­ti­on wird ein­ma­lig beim Start des Pro­gramms und danach nur noch zum Datums­wech­sel um Mit­ter­nacht auf­ge­ru­fen. Für den Ver­gleich des aktu­el­len Datums mit einem Ein­trag auf der SD-Kar­te wird der String Datum aus der vom Zeit­ser­ver über­mit­tel­ten Zeit zusam­men gesetzt und dann mit dem ers­ten Teil des getrenn­ten Strings (Datum­Ein­trag) ver­gli­chen. Der zwei­te Teil des getrenn­ten Strings (Kalen­der­Ein­trag) wird auf dem Dis­play angezeigt.

void KalenderDurchsuchen()
{
  /*
    Datum = String für das aktuelle Datum
    Treffer = true wenn es einen oder mehrere Treffer beim Datum gibt
    Termin[] = zum aktuellen Datum gefundene Einträge
    Abstand = Abstand in Pixeln zwischen mehreren Einträgen
    Trenner = Position des Trennzeichens (/)
    AnzahlDaten = Anzahl aller Daten in der Datei
    AnzahlTreffer = Anzahl der zum aktuellen Datum passenden Einträge
  */
  String Datum;
  bool Treffer = false;
  String Termin[DatenMax];
  int Abstand;
  int Trenner;
  int AnzahlTreffer;
  int AnzahlDaten;
  
  // Datei lesen
  if (Datei.open(Dateiname, O_RDONLY)) 
  {
    while (Datei.available()) 
    {
       for (int i = 0; i < DatenMax; i++) 
       {
        // Zeile bis zum return ('\n') lesen
        DatenLesen[i] = Datei.readStringUntil('\n');

        // Position des Trennzeichens
        Trenner = DatenLesen[i].indexOf("/");

        // Leerzeichen entfernen
        DatenLesen[i].trim();

        // Zeile am Komma in Datum und Kalendereintrag trennnen
        DatumEintrag[i] = DatenLesen[i].substring(0, Trenner);
        KalenderEintrag[i] = DatenLesen[i].substring(Trenner + 1, DatenLesen[i].length());

        // Anzahl der Einträge feststellen
        AnzahlDaten = i;

        Serial.println(DatumEintrag[i]);
        Serial.println(KalenderEintrag[i]);

        // Abbruch, wenn kein Datensatz nehr vorhanden
        if (DatenLesen[i] == "") break;
      }
    }
  }

  // Datei schließen
  Datei.close(); 

  // Datum zusammensetzen: TT.MM.
  // bei einstelligen Tag/Monat 0 davor setzen
  if (Zeit.tm_mday < 10) Datum = "0" + String(Zeit.tm_mday);
  else Datum = String(Zeit.tm_mday);
  Datum += ".";
  
  if (Zeit.tm_mon < 10) Datum += "0" + String(Zeit.tm_mon + 1);
  else Datum += String(Zeit.tm_mon + 1);
  
  Datum += ".";
  
  AnzahlTreffer = 0;

  // Einträge nach aktuellen Datum durchsuchen
  for (int i = 0; i <= AnzahlDaten; i++)
  {
    // wenn das aktuelle Element des Arrays mit dem Datum übereinstimmt
    if (DatumEintrag[i] == Datum) 
    {
      // Übereinstimmung gefunden
      Treffer = true;
      AnzahlTreffer ++;

      // dem String KalenderEintrag das aktuelle Elent des Arrays hinzufügen
      Termin[AnzahlTreffer] = KalenderEintrag[i];
      Serial.println(Termin[AnzahlTreffer]);
    }
  }

  tft.fillRect(1, 80, tft.width(), tft.height(), HintergrundFarbeTermine);
  u8g2Schriften.setForegroundColor(VordergrundFarbeTermine);   
  u8g2Schriften.setBackgroundColor(HintergrundFarbeTermine);

  // Schriftgröße 12
  // u8g2Schriften.setFont(u8g2_font_luBS12_tf);

  // Schriftgröße 14
  u8g2Schriften.setFont(u8g2_font_helvB14_tf);

  // Schriftgröße 18
  // u8g2Schriften.setFont(u8g2_font_luRS18_tf);  
  
  // Schriftgröße 20
  // u8g2Schriften.setFont(u8g2_font_logisoso20_tf);  
  
  // Schriftgröße 24
  // u8g2Schriften.setFont(u8g2_font_helvB24_tf);  

  // wenn es einen/mehrere Treffer gab (Treffer = true)
  // Anzahl der Treffer
  Serial.println(String(AnzahlTreffer) + " Treffer");
  if (Treffer)
  {
    // Einträge anzeigen
    for(int i = 0; i <= AnzahlTreffer; i++)
    {
      u8g2Schriften.setCursor(1, 80 + Abstand);
      u8g2Schriften.print(Termin[i]);

      // je nach Schriftgröße kann der Abstand angepasst werden
      Abstand += 30;
    }
  }

  // keine Treffer
  else 
  {  
    u8g2Schriften.setCursor(1, 120);
    u8g2Schriften.print("Keine Termine heute!"); 
  }
}

Quel­len


Letzte Aktualisierung: Aug. 12, 2025 @ 14:44